diff --git a/.dockerignore b/.dockerignore index 72943ddcc0..ee5c23bed8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -65,6 +65,7 @@ README* .env .env.* !.env.example +!engine/.env # Misc *.swp diff --git a/.editorconfig b/.editorconfig index 872fe6c2ca..665a74a09a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,6 +15,7 @@ max_line_length = 100 [*.py] indent_size = 4 +max_line_length = 120 [*.gradle] indent_size = 4 diff --git a/.github/aur/stirling-pdf-bin/PKGBUILD b/.github/aur/stirling-pdf-bin/PKGBUILD new file mode 100644 index 0000000000..bc6f59cea3 --- /dev/null +++ b/.github/aur/stirling-pdf-bin/PKGBUILD @@ -0,0 +1,29 @@ +# Maintainer: Stirling PDF Inc +pkgname=stirling-pdf-bin +pkgver=2.7.3 +pkgrel=1 +pkgdesc="Locally hosted, web-based PDF manipulation tool (desktop app, prebuilt binary)" +arch=('x86_64') +url="https://www.stirling.com" +license=('MIT' 'LicenseRef-Stirling-PDF-Proprietary') +depends=('gtk3' 'webkit2gtk' 'libappindicator-gtk3') +provides=('stirling-pdf') +conflicts=('stirling-pdf' 'stirling-pdf-git') +options=('!strip') + +source_x86_64=("${pkgname}-${pkgver}.deb::https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v${pkgver}/Stirling-PDF-linux-x86_64.deb") +sha256sums_x86_64=('PLACEHOLDER_DEB_SHA256') + +package() { + # Extract the .deb archive + bsdtar -xf data.tar* -C "${pkgdir}" + + # Fix permissions + find "${pkgdir}" -type d -exec chmod 755 {} \; + + # Install license + install -Dm644 /dev/stdin "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" < +pkgname=stirling-pdf-server-bin +pkgver=2.7.3 +pkgrel=1 +pkgdesc="Locally hosted, web-based PDF manipulation tool (server JAR, prebuilt)" +arch=('any') +url="https://www.stirling.com" +license=('MIT' 'LicenseRef-Stirling-PDF-Proprietary') +depends=('java-runtime>=21') +provides=('stirling-pdf-server') +conflicts=('stirling-pdf-server' 'stirling-pdf-server-git') +backup=('etc/stirling-pdf-server/settings.yml') + +source=("Stirling-PDF-with-login-${pkgver}.jar::https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v${pkgver}/Stirling-PDF-with-login.jar" + "stirling-pdf-server.service" + "stirling-pdf-server.sysusers" + "stirling-pdf-server.tmpfiles") +sha256sums=('PLACEHOLDER_JAR_SHA256' + 'PLACEHOLDER_SERVICE_SHA256' + 'PLACEHOLDER_SYSUSERS_SHA256' + 'PLACEHOLDER_TMPFILES_SHA256') + +prepare() { + cat > stirling-pdf-server.service << 'EOF' +[Unit] +Description=Stirling-PDF Server +After=network.target + +[Service] +Type=simple +User=stirling-pdf +Group=stirling-pdf +WorkingDirectory=/var/lib/stirling-pdf-server +ExecStart=/usr/bin/java -jar /usr/share/stirling-pdf-server/stirling-pdf-server.jar +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=stirling-pdf-server +Environment=JAVA_OPTS=-Xmx512m + +[Install] +WantedBy=multi-user.target +EOF + + cat > stirling-pdf-server.sysusers << 'EOF' +u stirling-pdf - "Stirling-PDF Server" /var/lib/stirling-pdf-server - +EOF + + cat > stirling-pdf-server.tmpfiles << 'EOF' +d /var/lib/stirling-pdf-server 0750 stirling-pdf stirling-pdf - +d /var/log/stirling-pdf-server 0750 stirling-pdf stirling-pdf - +EOF +} + +package() { + # JAR + install -Dm644 "Stirling-PDF-with-login-${pkgver}.jar" \ + "${pkgdir}/usr/share/stirling-pdf-server/stirling-pdf-server.jar" + + # Wrapper script + install -Dm755 /dev/stdin "${pkgdir}/usr/bin/stirling-pdf-server" << 'EOF' +#!/bin/sh +exec java $JAVA_OPTS -jar /usr/share/stirling-pdf-server/stirling-pdf-server.jar "$@" +EOF + + # systemd unit + install -Dm644 stirling-pdf-server.service \ + "${pkgdir}/usr/lib/systemd/system/stirling-pdf-server.service" + + # sysusers / tmpfiles + install -Dm644 stirling-pdf-server.sysusers \ + "${pkgdir}/usr/lib/sysusers.d/stirling-pdf-server.conf" + install -Dm644 stirling-pdf-server.tmpfiles \ + "${pkgdir}/usr/lib/tmpfiles.d/stirling-pdf-server.conf" + + # Default config stub + install -dm755 "${pkgdir}/etc/stirling-pdf-server" + install -Dm644 /dev/stdin "${pkgdir}/etc/stirling-pdf-server/settings.yml" << 'EOF' +# Stirling-PDF Server configuration +# See https://github.com/Stirling-Tools/Stirling-PDF for all options +server: + port: 8080 +EOF + + # License + install -Dm644 /dev/stdin "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" << 'EOF' +MIT License — see https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LICENSE +EOF +} diff --git a/.github/config/.files.yaml b/.github/config/.files.yaml index 7d73e47137..48d03bfc73 100644 --- a/.github/config/.files.yaml +++ b/.github/config/.files.yaml @@ -46,8 +46,7 @@ frontend: &frontend - testing/** - docker/** - scripts/translations/*.py - - scripts/build-tauri-jlink.bat - - scripts/build-tauri-jlink.sh + - .taskfiles/desktop.yml - scripts/convert_cff_to_ttf.py - scripts/harvest_type3_fonts.py - scripts/ignore_translation.toml diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d1287c0117..d9eb6dbe10 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -17,7 +17,7 @@ Closes #(issue_number) ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) -- [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) +- [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings @@ -37,4 +37,5 @@ Closes #(issue_number) ### Testing (if applicable) -- [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. +- [ ] I have run `task check` to verify linters, typechecks, and tests pass +- [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index 2fc00080af..9762af9822 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.yml @@ -3,6 +3,31 @@ name: PR Deployment via Comment on: issue_comment: types: [created] + workflow_dispatch: + inputs: + pr_number: + description: "PR number to deploy" + required: true + enable_prototypes: + description: "Build with prototypes frontend" + required: false + type: boolean + default: false + enable_pro: + description: "Enable pro features" + required: false + type: boolean + default: false + enable_enterprise: + description: "Enable enterprise features" + required: false + type: boolean + default: false + disable_security: + description: "Disable security/login" + required: false + type: boolean + default: true permissions: contents: read @@ -14,23 +39,27 @@ jobs: permissions: issues: write if: | - vars.CI_PROFILE != 'lite' && - github.event.issue.pull_request && - ( - contains(github.event.comment.body, 'prdeploy') || - contains(github.event.comment.body, 'deploypr') - ) - && - ( - github.event.comment.user.login == 'frooodle' || - github.event.comment.user.login == 'sf298' || - 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' + vars.CI_PROFILE != 'lite' && ( + github.event_name == 'workflow_dispatch' || + ( + github.event.issue.pull_request && + ( + contains(github.event.comment.body, 'prdeploy') || + contains(github.event.comment.body, 'deploypr') + ) + && + ( + github.event.comment.user.login == 'frooodle' || + github.event.comment.user.login == 'sf298' || + 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' + ) + ) ) outputs: pr_number: ${{ steps.get-pr.outputs.pr_number }} @@ -38,6 +67,7 @@ jobs: disable_security: ${{ steps.check-security-flag.outputs.disable_security }} enable_pro: ${{ steps.check-pro-flag.outputs.enable_pro }} enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }} + enable_prototypes: ${{ steps.check-prototypes-flag.outputs.enable_prototypes }} steps: - name: Harden Runner uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 @@ -61,7 +91,9 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | - const prNumber = context.payload.issue.number; + const prNumber = context.eventName === 'workflow_dispatch' + ? context.payload.inputs.pr_number + : context.payload.issue.number; console.log(`PR Number: ${prNumber}`); core.setOutput('pr_number', prNumber); @@ -69,12 +101,14 @@ jobs: id: check-security-flag env: COMMENT_BODY: ${{ github.event.comment.body }} + IS_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }} + DISPATCH_DISABLE_SECURITY: ${{ inputs.disable_security }} run: | - if [[ "$COMMENT_BODY" == *"security"* ]] || [[ "$COMMENT_BODY" == *"login"* ]]; then - echo "Security flags detected in comment" + if [[ "$IS_DISPATCH" == "true" ]]; then + echo "disable_security=$DISPATCH_DISABLE_SECURITY" >> $GITHUB_OUTPUT + elif [[ "$COMMENT_BODY" == *"security"* ]] || [[ "$COMMENT_BODY" == *"login"* ]]; then echo "disable_security=false" >> $GITHUB_OUTPUT else - echo "No security flags detected in comment" echo "disable_security=true" >> $GITHUB_OUTPUT fi @@ -82,22 +116,43 @@ jobs: id: check-pro-flag env: COMMENT_BODY: ${{ github.event.comment.body }} + IS_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }} + DISPATCH_PRO: ${{ inputs.enable_pro }} + DISPATCH_ENTERPRISE: ${{ inputs.enable_enterprise }} run: | - if [[ "$COMMENT_BODY" == *"pro"* ]] || [[ "$COMMENT_BODY" == *"premium"* ]]; then - echo "pro flags detected in comment" + if [[ "$IS_DISPATCH" == "true" ]]; then + echo "enable_pro=$DISPATCH_PRO" >> $GITHUB_OUTPUT + echo "enable_enterprise=$DISPATCH_ENTERPRISE" >> $GITHUB_OUTPUT + elif [[ "$COMMENT_BODY" == *"pro"* ]] || [[ "$COMMENT_BODY" == *"premium"* ]]; then echo "enable_pro=true" >> $GITHUB_OUTPUT echo "enable_enterprise=false" >> $GITHUB_OUTPUT elif [[ "$COMMENT_BODY" == *"enterprise"* ]]; then - echo "enterprise flags detected in comment" echo "enable_enterprise=true" >> $GITHUB_OUTPUT echo "enable_pro=true" >> $GITHUB_OUTPUT else - echo "No pro or enterprise flags detected in comment" echo "enable_pro=false" >> $GITHUB_OUTPUT echo "enable_enterprise=false" >> $GITHUB_OUTPUT fi + - name: Check for prototypes flag + id: check-prototypes-flag + env: + COMMENT_BODY: ${{ github.event.comment.body }} + IS_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }} + DISPATCH_PROTOTYPES: ${{ inputs.enable_prototypes }} + run: | + if [[ "$IS_DISPATCH" == "true" ]]; then + echo "enable_prototypes=$DISPATCH_PROTOTYPES" >> $GITHUB_OUTPUT + elif [[ "$COMMENT_BODY" == *"prototypes"* ]]; then + echo "Prototypes flag detected in comment" + echo "enable_prototypes=true" >> $GITHUB_OUTPUT + else + echo "No prototypes flag detected in comment" + echo "enable_prototypes=false" >> $GITHUB_OUTPUT + fi + - name: Add 'in_progress' reaction to comment + if: github.event_name == 'issue_comment' id: add-eyes-reaction uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: @@ -161,6 +216,8 @@ jobs: with: gradle-version: 9.3.1 + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 - name: Run Gradle Command run: | if [ "${{ needs.check-comment.outputs.disable_security }}" == "true" ]; then @@ -168,7 +225,7 @@ jobs: else export DISABLE_ADDITIONAL_FEATURES=false fi - ./gradlew build + task backend:build env: MAVEN_USER: ${{ secrets.MAVEN_USER }} MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} @@ -193,7 +250,21 @@ jobs: 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 }} - build-args: VERSION_TAG=alpha + build-args: | + VERSION_TAG=alpha + PROTOTYPES_BUILD=${{ needs.check-comment.outputs.enable_prototypes }} + platforms: linux/amd64 + + - name: Build and push engine image + if: needs.check-comment.outputs.enable_prototypes == 'true' + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + context: ./engine + 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 }} platforms: linux/amd64 - name: Set up SSH @@ -231,33 +302,64 @@ jobs: PREMIUM_PROFEATURES_AUDIT_ENABLED="false" fi + 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 + AI_ENGINE_VARS=" + SYSTEM_AIENGINE_ENABLED: \"true\" + SYSTEM_AIENGINE_URL: \"http://stirling-pdf-engine-pr-${PR_NUMBER}:5001\"" + ENGINE_SERVICE=" + stirling-pdf-engine: + container_name: stirling-pdf-engine-pr-${PR_NUMBER} + image: ${DOCKER_USER}/test:engine-pr-${PR_NUMBER} + environment: + ANTHROPIC_API_KEY: \"${{ secrets.ANTHROPIC_API_KEY }}\" + networks: + - pr-network + restart: on-failure:5" + NETWORK_SECTION=" + networks: + pr-network:" + BACKEND_NETWORK=" + networks: + - pr-network" + else + AI_ENGINE_VARS="" + ENGINE_SERVICE="" + NETWORK_SECTION="" + BACKEND_NETWORK="" + fi + # First create the docker-compose content locally cat > docker-compose.yml << EOF version: '3.3' services: stirling-pdf: - container_name: stirling-pdf-pr-${{ needs.check-comment.outputs.pr_number }} - image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }} + container_name: stirling-pdf-pr-${PR_NUMBER} + image: ${DOCKER_USER}/test:pr-${PR_NUMBER} ports: - - "${{ needs.check-comment.outputs.pr_number }}:8080" + - "${PR_NUMBER}:8080" volumes: - - /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/data:/usr/share/tessdata:rw - - /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/config:/configs:rw - - /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/logs:/logs:rw + - /stirling/PR-${PR_NUMBER}/data:/usr/share/tessdata:rw + - /stirling/PR-${PR_NUMBER}/config:/configs:rw + - /stirling/PR-${PR_NUMBER}/logs:/logs:rw environment: DISABLE_ADDITIONAL_FEATURES: "${DISABLE_ADDITIONAL_FEATURES}" SECURITY_ENABLELOGIN: "${LOGIN_SECURITY}" SYSTEM_DEFAULTLOCALE: en-GB - UI_APPNAME: "Stirling-PDF PR#${{ needs.check-comment.outputs.pr_number }}" - UI_HOMEDESCRIPTION: "PR#${{ needs.check-comment.outputs.pr_number }} for Stirling-PDF Latest" - UI_APPNAMENAVBAR: "PR#${{ needs.check-comment.outputs.pr_number }}" + UI_APPNAME: "Stirling-PDF PR#${PR_NUMBER}" + UI_HOMEDESCRIPTION: "PR#${PR_NUMBER} for Stirling-PDF Latest" + UI_APPNAMENAVBAR: "PR#${PR_NUMBER}" SYSTEM_MAXFILESIZE: "100" METRICS_ENABLED: "true" SYSTEM_GOOGLEVISIBILITY: "false" PREMIUM_KEY: "${PREMIUM_KEY}" PREMIUM_ENABLED: "${PREMIUM_ENABLED}" - PREMIUM_PROFEATURES_AUDIT_ENABLED: "${PREMIUM_PROFEATURES_AUDIT_ENABLED}" - restart: on-failure:5 + PREMIUM_PROFEATURES_AUDIT_ENABLED: "${PREMIUM_PROFEATURES_AUDIT_ENABLED}"${AI_ENGINE_VARS} + restart: on-failure:5${BACKEND_NETWORK}${ENGINE_SERVICE}${NETWORK_SECTION} EOF # Then copy the file and execute commands @@ -265,13 +367,13 @@ jobs: ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.NEW_VPS_USERNAME }}@${{ secrets.NEW_VPS_HOST }} << ENDSSH # Create PR-specific directories - mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs} + mkdir -p /stirling/PR-${PR_NUMBER}/{data,config,logs} # Move docker-compose file to correct location - mv /tmp/docker-compose.yml /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/docker-compose.yml + mv /tmp/docker-compose.yml /stirling/PR-${PR_NUMBER}/docker-compose.yml # Start or restart the container - cd /stirling/PR-${{ needs.check-comment.outputs.pr_number }} + cd /stirling/PR-${PR_NUMBER} docker-compose pull docker-compose up -d ENDSSH @@ -280,7 +382,7 @@ jobs: echo "security_status=${SECURITY_STATUS}" >> $GITHUB_ENV - name: Add success reaction to comment - if: success() + if: success() && github.event_name == 'issue_comment' uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.setup-bot.outputs.token }} @@ -315,7 +417,7 @@ jobs: } - name: Add failure reaction to comment - if: failure() + if: failure() && github.event_name == 'issue_comment' uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ steps.setup-bot.outputs.token }} diff --git a/.github/workflows/PR-Demo-cleanup.yml b/.github/workflows/PR-Demo-cleanup.yml index ced43f37f8..cc72cc024c 100644 --- a/.github/workflows/PR-Demo-cleanup.yml +++ b/.github/workflows/PR-Demo-cleanup.yml @@ -121,8 +121,9 @@ jobs: # Remove PR-specific directories rm -rf /stirling/PR-${{ github.event.pull_request.number }} - # Remove the Docker image + # 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 echo "PERFORMED_CLEANUP" else diff --git a/.github/workflows/ai-engine.yml b/.github/workflows/ai-engine.yml index ed0569c312..a90246f887 100644 --- a/.github/workflows/ai-engine.yml +++ b/.github/workflows/ai-engine.yml @@ -11,10 +11,6 @@ jobs: permissions: contents: read pull-requests: write - defaults: - run: - working-directory: engine - steps: - name: Checkout code uses: actions/checkout@v4 @@ -24,60 +20,85 @@ jobs: with: enable-cache: true - - name: Install dependencies - run: make install - - - name: Run fixers - # Ignore errors here because we're going to add comments for them in the following steps before actually failing - run: make fix || true - - - name: Check for fixer changes - id: fixer_changes - run: | - if git diff --quiet; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Post fixer suggestions - if: steps.fixer_changes.outputs.changed == 'true' && github.event_name == 'pull_request' - uses: reviewdog/action-suggester@v1 - continue-on-error: true + - name: Set up JDK 25 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: - tool_name: engine-make-fix - github_token: ${{ secrets.GITHUB_TOKEN }} - filter_mode: file - fail_level: any - level: info + java-version: "25" + distribution: "temurin" - - name: Comment on fixer suggestions - if: steps.fixer_changes.outputs.changed == 'true' && github.event_name == 'pull_request' - uses: actions/github-script@v7 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1 with: - script: | - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: "The Python code in your PR has formatting/linting issues. Consider running `make fix` locally or setting up your editor's Ruff integration to auto-format and lint your files as you go, or commit the suggested changes on this PR.", - }); + gradle-version: 9.3.1 - - name: Verify fixer changes are committed - if: steps.fixer_changes.outputs.changed == 'true' + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 + + - name: Regenerate tool models + run: task engine:tool-models + + - name: Verify tool models are up to date run: | - if ! git diff --exit-code; then - echo "Fixes are out of date." - echo "Apply the reviewdog suggestions or run 'make fix' from engine/ and commit the updated files." - git --no-pager diff --stat + if ! git diff --exit-code engine/src/stirling/models/tool_models.py; then + echo "tool_models.py is out of date." + echo "Run 'task engine:tool-models' locally and commit the updated file." exit 1 fi + - name: Run fixers + run: task engine:fix + + - name: Verify fixes are committed + id: fixer_changes + run: | + if ! git diff --quiet; then + git --no-pager diff --stat + echo "::error::There are issues with your Python code that will need to be fixed before they can be merged in. Run 'task engine:fix' to auto-fix what can be fixed automatically, then run 'task engine:check' to see what still needs fixing manually." + exit 1 + fi + + - name: Comment on fixer failures + if: steps.fixer_changes.outcome == 'failure' && github.event_name == 'pull_request' + continue-on-error: true + uses: actions/github-script@v7 + with: + script: | + const marker = ''; + const body = [ + marker, + '### Engine Check Failed', + '', + 'There are issues with your Python code that will need to be fixed before they can be merged in.', + '', + 'Run `task engine:fix` to auto-fix what can be fixed automatically, then run `task engine:check` to see what still needs fixing manually.', + ].join('\n'); + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + - name: Run linting - run: make lint + run: task engine:lint - name: Run type checking - run: make typecheck + run: task engine:typecheck - name: Run tests - run: make test + run: task engine:test diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml new file mode 100644 index 0000000000..935e58ffe0 --- /dev/null +++ b/.github/workflows/aur-publish.yml @@ -0,0 +1,128 @@ +name: Publish to AUR + +on: + release: + types: [released] + workflow_dispatch: + inputs: + version: + description: "Version to publish (e.g. 2.9.2 — no v prefix)" + required: true + type: string + dry_run: + description: "Skip the AUR push (safe test)" + type: boolean + default: true + +permissions: + contents: read + +jobs: + get-release-info: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.info.outputs.version }} + deb_sha256: ${{ steps.hashes.outputs.deb_sha256 }} + jar_sha256: ${{ steps.hashes.outputs.jar_sha256 }} + steps: + - name: Harden Runner + uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0 + with: + egress-policy: audit + + - name: Extract version from tag or manual input + id: info + env: + DISPATCH_VERSION: ${{ inputs.version }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + VERSION="$DISPATCH_VERSION" + else + VERSION="$RELEASE_TAG" + fi + VERSION="${VERSION#v}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Download release assets and compute SHA256 + id: hashes + env: + VERSION: ${{ steps.info.outputs.version }} + run: | + BASE="https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v${VERSION}" + + download_sha256() { + local url="$1" + local file + file=$(basename "$url") + curl -fsSL --retry 3 -o "$file" "$url" + sha256sum "$file" | awk '{print $1}' + } + + DEB_SHA=$(download_sha256 "${BASE}/Stirling-PDF-linux-x86_64.deb") + JAR_SHA=$(download_sha256 "${BASE}/Stirling-PDF-with-login.jar") + + echo "deb_sha256=$DEB_SHA" >> "$GITHUB_OUTPUT" + echo "jar_sha256=$JAR_SHA" >> "$GITHUB_OUTPUT" + + publish-aur: + needs: get-release-info + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Checkout repository (for PKGBUILD templates) + uses: actions/checkout@v4 + + - name: Update stirling-pdf-bin PKGBUILD + env: + VERSION: ${{ needs.get-release-info.outputs.version }} + DEB_SHA: ${{ needs.get-release-info.outputs.deb_sha256 }} + run: | + PKGBUILD=".github/aur/stirling-pdf-bin/PKGBUILD" + sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "$PKGBUILD" + sed -i "s/^pkgrel=.*/pkgrel=1/" "$PKGBUILD" + sed -i "s/'PLACEHOLDER_DEB_SHA256'/'${DEB_SHA}'/" "$PKGBUILD" + + - name: Update stirling-pdf-server-bin PKGBUILD + env: + VERSION: ${{ needs.get-release-info.outputs.version }} + JAR_SHA: ${{ needs.get-release-info.outputs.jar_sha256 }} + run: | + PKGBUILD=".github/aur/stirling-pdf-server-bin/PKGBUILD" + sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "$PKGBUILD" + sed -i "s/^pkgrel=.*/pkgrel=1/" "$PKGBUILD" + sed -i "s/'PLACEHOLDER_JAR_SHA256'/'${JAR_SHA}'/" "$PKGBUILD" + + - name: Show updated PKGBUILDs (for dry-run visibility) + run: | + echo "--- stirling-pdf-bin PKGBUILD ---" + cat .github/aur/stirling-pdf-bin/PKGBUILD + echo "" + echo "--- stirling-pdf-server-bin PKGBUILD ---" + cat .github/aur/stirling-pdf-server-bin/PKGBUILD + + - name: Publish stirling-pdf-bin to AUR + if: ${{ github.event_name == 'release' || inputs.dry_run == false }} + uses: KSXGitHub/github-actions-deploy-aur@2ac5a4c1d7035885d46b10e3193393be8460b6f1 # v4.1.1 + with: + pkgname: stirling-pdf-bin + pkgbuild: .github/aur/stirling-pdf-bin/PKGBUILD + commit_username: Stirling PDF Inc + commit_email: contact@stirlingpdf.com + ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + commit_message: "Update to v${{ needs.get-release-info.outputs.version }}" + + - name: Publish stirling-pdf-server-bin to AUR + if: ${{ github.event_name == 'release' || inputs.dry_run == false }} + uses: KSXGitHub/github-actions-deploy-aur@v4.1.1 + with: + pkgname: stirling-pdf-server-bin + pkgbuild: .github/aur/stirling-pdf-server-bin/PKGBUILD + commit_username: Stirling PDF Inc + commit_email: contact@stirlingpdf.com + ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + commit_message: "Update to v${{ needs.get-release-info.outputs.version }}" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d5c8285d45..78efa46cc6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,6 +50,7 @@ jobs: permissions: actions: read security-events: write + pull-requests: write strategy: fail-fast: false matrix: @@ -84,8 +85,77 @@ jobs: gradle-version: 9.3.1 cache-disabled: true + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 + - name: Check Java formatting (Spotless) + if: matrix.jdk-version == 25 && matrix.spring-security == false + id: spotless-check + run: task backend:format:check + continue-on-error: true + env: + MAVEN_USER: ${{ secrets.MAVEN_USER }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} + + - name: Comment on Java formatting failure + if: steps.spotless-check.outcome == 'failure' + continue-on-error: true + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const marker = ''; + const body = [ + marker, + '### Java Formatting Check Failed', + '', + 'Your code has formatting issues. Run the following command to fix them:', + '', + '```bash', + 'task backend:format', + '```', + '', + 'Then commit and push the changes.', + ].join('\n'); + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + + - name: Fail if Java formatting issues found + if: steps.spotless-check.outcome == 'failure' + run: | + echo "============================================" + echo " Java Formatting Check Failed" + echo "============================================" + echo "" + echo "Your code has formatting issues." + echo "Run the following command to fix them:" + echo "" + echo " task backend:format" + echo "" + echo "Then commit and push the changes." + echo "============================================" + exit 1 + - name: Build with Gradle and spring security ${{ matrix.spring-security }} - run: ./gradlew build -PnoSpotless + run: task backend:build:ci env: MAVEN_USER: ${{ secrets.MAVEN_USER }} MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} @@ -169,8 +239,10 @@ jobs: gradle-version: 9.3.1 cache-disabled: true + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 - name: Generate OpenAPI documentation - run: ./gradlew :stirling-pdf:generateOpenApiDocs + run: task backend:swagger env: MAVEN_USER: ${{ secrets.MAVEN_USER }} MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} @@ -187,6 +259,9 @@ jobs: if: needs.files-changed.outputs.frontend == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - name: Harden Runner uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 @@ -200,16 +275,63 @@ jobs: node-version: "22" cache: "npm" cache-dependency-path: frontend/package-lock.json - - name: Install frontend dependencies - run: cd frontend && npm ci - - name: Type-check frontend - run: cd frontend && npm run prep && npm run typecheck:all - - name: Lint frontend - run: cd frontend && npm run lint - - name: Build frontend - run: cd frontend && npm run build - - name: Run frontend tests - run: cd frontend && npm run test -- --run + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 + - name: Quality-check frontend + id: frontend-check + run: task frontend:check:all + continue-on-error: true + - name: Comment on frontend check failure + if: steps.frontend-check.outcome == 'failure' + continue-on-error: true + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const marker = ''; + const body = [ + marker, + '### Frontend Check Failed', + '', + 'There are issues with your frontend code that will need to be fixed before they can be merged in.', + '', + 'Run `task frontend:fix` to auto-fix what can be fixed automatically, then run `task frontend:check:all` to see what still needs fixing manually.', + ].join('\n'); + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + - name: Fail if frontend check failed + if: steps.frontend-check.outcome == 'failure' + run: | + echo "============================================" + echo " Frontend Check Failed" + echo "============================================" + echo "" + echo "There are issues with your frontend code that" + echo "will need to be fixed before they can be merged in." + echo "" + echo "Run 'task frontend:fix' to auto-fix what can be" + echo "fixed automatically, then run 'task frontend:check:all'" + echo "to see what still needs fixing manually." + echo "============================================" + exit 1 - name: Upload frontend build artifacts uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: @@ -234,14 +356,12 @@ jobs: node-version: "22" cache: "npm" cache-dependency-path: frontend/package-lock.json - - name: Install frontend dependencies - run: cd frontend && npm ci - - name: Generate icons - run: cd frontend && node scripts/generate-icons.js + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 - name: Install Playwright (chromium only) - run: cd frontend && npx playwright install chromium --with-deps + run: task frontend:test:e2e:install -- chromium - name: Run E2E tests (chromium) - run: cd frontend && npx playwright test --project=chromium + run: task frontend:test:e2e -- --project=chromium - name: Upload Playwright report if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 @@ -284,13 +404,10 @@ jobs: gradle-version: 9.3.1 cache-disabled: true - - name: check the licenses for compatibility - # NOTE: --no-parallel is intentional here. Running the checkLicense task in parallel with other - # Gradle tasks has been observed to cause intermittent failures with the dependency license - # checking plugin on this Gradle version. Disabling parallel execution trades some build speed - # for more reliable, deterministic license checks. If upgrading Gradle or the plugin, consider - # re-evaluating whether this flag is still required before removing it. - run: ./gradlew checkLicense --no-parallel + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 + - name: Check licenses for compatibility + run: task backend:licenses:check env: MAVEN_USER: ${{ secrets.MAVEN_USER }} MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} @@ -485,8 +602,10 @@ jobs: gradle-version: 9.3.1 cache-disabled: true + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 - name: Build application - run: ./gradlew build + run: task backend:build env: MAVEN_USER: ${{ secrets.MAVEN_USER }} MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} @@ -513,7 +632,7 @@ jobs: echo "base_image=stirling-pdf-base:pr-test" >> $GITHUB_OUTPUT echo "platforms=linux/amd64" >> $GITHUB_OUTPUT else - echo "base_image=ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf-base:latest" >> $GITHUB_OUTPUT + echo "base_image=stirlingtools/stirling-pdf-base:latest" >> $GITHUB_OUTPUT echo "platforms=linux/amd64,linux/arm64/v8" >> $GITHUB_OUTPUT fi diff --git a/.github/workflows/frontend-backend-licenses-update.yml b/.github/workflows/frontend-backend-licenses-update.yml index a7e10dd4fe..be7c0f1148 100644 --- a/.github/workflows/frontend-backend-licenses-update.yml +++ b/.github/workflows/frontend-backend-licenses-update.yml @@ -89,12 +89,13 @@ jobs: NPM_CONFIG_IGNORE_SCRIPTS: "true" run: npm ci --ignore-scripts --audit=false --fund=false + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 - name: Generate frontend license report (internal PR) if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false - working-directory: frontend env: PR_IS_FORK: "false" - run: npm run generate-licenses + run: task frontend:licenses:generate - name: Generate frontend license report (fork PRs, pinned) if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true @@ -341,15 +342,11 @@ jobs: with: gradle-version: 9.3.1 + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 - name: Check licenses and generate report id: license-check - run: | - # NOTE: --no-parallel is intentional here. Running the license-checking tasks in parallel has - # previously caused intermittent concurrency issues in CI (e.g. flaky failures in the license - # plugin/Gradle when multiple projects are evaluated concurrently). Disabling parallelism trades - # some build speed for more reliable license reports. If the underlying issues are resolved in - # future Gradle or plugin versions, this flag can be reconsidered. - ./gradlew checkLicense generateLicenseReport --no-parallel || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV + run: task backend:licenses:generate || echo "LICENSE_CHECK_FAILED=true" >> $GITHUB_ENV env: MAVEN_USER: ${{ secrets.MAVEN_USER }} MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index b1ee6c8c88..024cccd441 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -21,6 +21,14 @@ on: - windows - macos - linux + sign: + description: "Code sign the binaries (requires signing secrets)" + required: false + default: "true" + type: choice + options: + - "true" + - "false" release: types: [created] @@ -63,11 +71,11 @@ jobs: with: gradle-version: 9.3.1 + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 - name: Get version number id: versionNumber run: | - echo "Running gradlew printVersion..." - ./gradlew printVersion --quiet VERSION=$(./gradlew printVersion --quiet | tail -1) echo "Extracted version: $VERSION" echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT @@ -144,6 +152,9 @@ jobs: cache: "npm" cache-dependency-path: frontend/package-lock.json + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 + - name: Build JAR run: ./gradlew build ${{ matrix.variant.build_frontend && '-PbuildWithFrontend=true' || '' }} -x spotlessApply -x spotlessCheck -x test -x sonarqube env: @@ -219,89 +230,21 @@ jobs: with: gradle-version: 9.3.1 - - name: Build Java backend with JLink - working-directory: ./ - shell: bash - run: | - chmod +x ./gradlew - echo "🔧 Building Stirling-PDF JAR..." - ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 - # Find the built JAR - STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1) - echo "✅ Built JAR: $STIRLING_JAR" - - # Create Tauri directories - mkdir -p ./frontend/src-tauri/libs - mkdir -p ./frontend/src-tauri/runtime - - # Copy JAR to Tauri libs - cp "$STIRLING_JAR" ./frontend/src-tauri/libs/ - echo "✅ JAR copied to Tauri libs" - - # Analyze JAR dependencies for jlink modules - echo "🔠Analyzing JAR dependencies..." - if command -v jdeps &> /dev/null; then - DETECTED_MODULES=$(jdeps --print-module-deps --ignore-missing-deps "$STIRLING_JAR" 2>/dev/null || echo "") - if [ -n "$DETECTED_MODULES" ]; then - echo "📋 jdeps detected modules: $DETECTED_MODULES" - MODULES="$DETECTED_MODULES,java.compiler,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported" - else - echo "âš ï¸ jdeps analysis failed, using predefined modules" - MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported" - fi - else - echo "âš ï¸ jdeps not available, using predefined modules" - MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported" - fi - - # Create custom JRE with jlink - echo "🔧 Creating custom JRE with jlink..." - echo "📋 Using modules: $MODULES" - - # Remove any existing JRE - rm -rf ./frontend/src-tauri/runtime/jre - - # Create the custom JRE - jlink \ - --add-modules "$MODULES" \ - --strip-debug \ - --compress=2 \ - --no-header-files \ - --no-man-pages \ - --output ./frontend/src-tauri/runtime/jre - - if [ ! -d "./frontend/src-tauri/runtime/jre" ]; then - echo "⌠Failed to create JLink runtime" - exit 1 - fi - - # Test the bundled runtime - if [ -f "./frontend/src-tauri/runtime/jre/bin/java" ]; then - RUNTIME_VERSION=$(./frontend/src-tauri/runtime/jre/bin/java --version 2>&1 | head -n 1) - echo "✅ Custom JRE created successfully: $RUNTIME_VERSION" - else - echo "⌠Custom JRE executable not found" - exit 1 - fi - - # Calculate runtime size - RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1) - echo "📊 Custom JRE size: $RUNTIME_SIZE" + - name: Prepare desktop build + run: task desktop:prepare env: MAVEN_USER: ${{ secrets.MAVEN_USER }} MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} DISABLE_ADDITIONAL_FEATURES: true - - name: Install frontend dependencies - working-directory: ./frontend - run: npm ci - # 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.ref == 'refs/heads/V2-master') }} + 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') }} uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1 env: SM_API_KEY: ${{ secrets.SM_API_KEY }} @@ -311,7 +254,7 @@ 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.ref == 'refs/heads/V2-master') }} + 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') }} shell: pwsh run: | Write-Host "Setting up DigiCert KeyLocker environment..." @@ -346,7 +289,7 @@ 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.ref == 'refs/heads/V2-master') }} + 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 }} @@ -377,7 +320,7 @@ jobs: } - name: Import Apple Developer Certificate - if: (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') + if: (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} @@ -398,7 +341,7 @@ jobs: rm certificate.p12 - name: Verify Certificate - if: (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') + if: (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master') run: | echo "Verifying Apple Developer Certificate..." KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db @@ -409,6 +352,82 @@ jobs: echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV echo "Certificate imported successfully." + # Pre-flight: verify smctl can talk to DigiCert and sync cert before we sign. + # Mirrors the setup from working public Tauri+KeyLocker repos (Labric, Meetily). + # 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') }} + shell: pwsh + env: + KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} + run: | + & smctl healthcheck + if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl healthcheck failed"; exit 1 } + & smctl keypair ls + if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl keypair ls failed"; exit 1 } + & smctl windows certsync --keypair-alias "$env:KEYPAIR_ALIAS" + if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] smctl windows certsync returned non-zero - continuing" } + Write-Host "[SUCCESS] smctl preflight passed" + + # Write platform-specific Tauri config that adds signCommand for Windows. + # Tauri auto-merges tauri.windows.conf.json with tauri.conf.json (RFC 7396). + # Tauri calls this command on every binary BEFORE bundling into the MSI, + # substituting %1 with the file path. + # + # Why OBJECT form (cmd + args) instead of string: + # Tauri's string-form parser does a naive split(' ') with no shell/quote handling. + # Args with spaces or quote characters get mangled. The object form passes each + # arg directly to Rust's Command::arg which handles Windows CreateProcess quoting. + # + # Why --keypair-alias instead of --fingerprint: + # --fingerprint requires smctl windows certsync to have synced the cert to the + # Windows cert store first. --keypair-alias goes direct through PKCS11 and works + # without certsync. All real-world working Tauri+smctl examples use this flag. + # + # 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') }} + shell: bash + env: + KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} + run: | + cat > ./frontend/src-tauri/tauri.windows.conf.json < /usr/lib/libjvm.so" + else + echo "libjvm not found at $JAVA_LIBJVM" + exit 1 + fi + - name: Build Tauri app uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25 env: @@ -419,114 +438,115 @@ jobs: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }} + # AppImage signing — three env vars work together: + # SIGN=1 tells linuxdeploy-plugin-appimage to forward --sign to appimagetool + # 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. + SIGN: "1" + APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }} + SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }} VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }} VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }} - # Only enable Windows signing in Tauri when on release or V2-master - SIGN: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }} + # DigiCert KeyLocker env vars consumed by smctl during signCommand + SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }} CI: true with: projectPath: ./frontend tauriScript: npx tauri args: ${{ matrix.args }} - # Sign with DigiCert KeyLocker (post-build) - - name: Sign Windows binaries with DigiCert KeyLocker - if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && (github.event_name == 'release' || github.ref == 'refs/heads/V2-master') }} + - name: Clear release GPG key from runner keyring (Linux) + if: always() && matrix.platform == 'ubuntu-22.04' + env: + RELEASE_GPG_FINGERPRINT: ${{ vars.RELEASE_GPG_FINGERPRINT }} + run: | + if [ -n "$RELEASE_GPG_FINGERPRINT" ]; then + gpg --batch --yes --delete-secret-keys "$RELEASE_GPG_FINGERPRINT" || true + gpg --batch --yes --delete-keys "$RELEASE_GPG_FINGERPRINT" || true + fi + + # Verify the MSI (outer wrapper users download) AND the inner exe extracted + # from it (what actually gets installed and what AV scans). We don't check + # target/.../release/stirling-pdf.exe - that's Tauri's intermediate build + # 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') }} shell: pwsh run: | - Write-Host "=== DigiCert KeyLocker Signing ===" + $allSigned = $true - # Test smctl connectivity first - Write-Host "Testing smctl connection..." - $healthCheck = & smctl healthcheck 2>&1 - if ($LASTEXITCODE -eq 0) { - Write-Host "[SUCCESS] Connected to DigiCert KeyLocker" - } else { - Write-Host "[ERROR] Failed to connect to DigiCert KeyLocker" - Write-Host $healthCheck - exit 1 - } - Write-Host "" - - # Sync certificates to Windows certificate store - Write-Host "Syncing certificates to Windows certificate store..." - $syncOutput = & smctl windows certsync 2>&1 - Write-Host "Cert sync result: $syncOutput" - Write-Host "" - - # Find only the files we need to sign - $filesToSign = @() - - # Main application executable - $mainExe = Get-ChildItem -Path "./frontend/src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "stirling-pdf.exe" -File -ErrorAction SilentlyContinue - if ($mainExe) { $filesToSign += $mainExe } - - # MSI installer + # Check MSI installer (outer wrapper - what users download) $msiFiles = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*.msi" -Recurse -File - $filesToSign += $msiFiles - - if ($filesToSign.Count -eq 0) { - Write-Host "[ERROR] No files found to sign" + if ($msiFiles.Count -eq 0) { + Write-Host "[ERROR] No MSI found under target/" exit 1 } - - Write-Host "Found $($filesToSign.Count) files to sign:" - foreach ($f in $filesToSign) { Write-Host " - $($f.Name)" } - Write-Host "" - - $signedCount = 0 - foreach ($file in $filesToSign) { - Write-Host "Signing: $($file.Name)" - - # Get PKCS11 config file path - $pkcs11Config = $env:PKCS11_CONFIG - if (-not $pkcs11Config) { - Write-Host "[ERROR] PKCS11_CONFIG environment variable not set" - exit 1 + foreach ($msi in $msiFiles) { + $sig = Get-AuthenticodeSignature -FilePath $msi.FullName + Write-Host "MSI: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)" + if ($sig.Status -ne "Valid") { + Write-Host "[ERROR] MSI is not signed" + $allSigned = $false } - - Write-Host "Using PKCS11 config: $pkcs11Config" - - # Try signing with certificate fingerprint first (if available) - $fingerprint = "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}" - if ($fingerprint -and $fingerprint -ne "") { - Write-Host "Attempting to sign with certificate fingerprint..." - $output = & smctl sign --fingerprint "$fingerprint" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1 - $exitCode = $LASTEXITCODE - } else { - Write-Host "No fingerprint provided, using keypair alias..." - $output = & smctl sign --keypair-alias "${{ secrets.SM_KEYPAIR_ALIAS }}" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1 - $exitCode = $LASTEXITCODE - } - - Write-Host "Exit code: $exitCode" - Write-Host "Output: $output" - - if ($output -match "FAILED" -or $output -match "error" -or $output -match "Error") { - Write-Host "[ERROR] Signing failed for $($file.Name)" - exit 1 - } - - if ($exitCode -ne 0) { - Write-Host "[ERROR] Failed to sign $($file.Name)" - Write-Host "Full error output:" - Write-Host $output - exit 1 - } - - $signedCount++ - Write-Host "[SUCCESS] Signed: $($file.Name)" - Write-Host "" } - Write-Host "=== Summary ===" - Write-Host "[SUCCESS] Signed $signedCount/$($filesToSign.Count) files successfully" + # Extract MSI and verify the inner exe (the file that actually gets installed). + # This is the critical check - AV flags the installed exe at runtime. + $msi = $msiFiles[0].FullName + $extractDir = Join-Path $env:RUNNER_TEMP "msi-verify" + if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force } + $proc = Start-Process msiexec.exe -ArgumentList '/a', $msi, '/qn', "TARGETDIR=$extractDir" -Wait -PassThru -NoNewWindow + if ($proc.ExitCode -eq 0) { + $innerExe = Get-ChildItem -Path $extractDir -Filter "stirling-pdf.exe" -Recurse -File | Select-Object -First 1 + if ($innerExe) { + $sig = Get-AuthenticodeSignature -FilePath $innerExe.FullName + Write-Host "Inner EXE (from MSI): Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)" + if ($sig.Status -ne "Valid") { + Write-Host "[ERROR] Inner exe extracted from MSI is NOT signed - AV will flag this at runtime" + $allSigned = $false + } + } else { + Write-Host "[ERROR] Could not find stirling-pdf.exe inside MSI" + $allSigned = $false + } + } else { + Write-Host "[ERROR] Failed to extract MSI for verification (exit code: $($proc.ExitCode))" + $allSigned = $false + } + if (-not $allSigned) { + Write-Host "[ERROR] Signature verification failed" + exit 1 + } + Write-Host "[SUCCESS] MSI and installed exe are properly signed" + + # Dump smctl log files on failure. Tauri's signCommand captures smctl output + # 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 != '' }} + shell: pwsh + run: | + $logDir = "$env:USERPROFILE\.signingmanager\logs" + if (Test-Path $logDir) { + Get-ChildItem $logDir | ForEach-Object { + Write-Host "=== $($_.FullName) ===" + Get-Content $_.FullName -Tail 200 + Write-Host "" + } + } else { + Write-Host "smctl log directory not found at $logDir" + } + + # Rename + Upload: use always() so artifacts are still collected when verify + # fails - we need them to manually inspect what actually came out of the build. - name: Rename artifacts + if: always() && steps.digicert-setup.conclusion != 'failure' shell: bash run: | mkdir -p ./dist @@ -534,17 +554,20 @@ jobs: # Find and rename artifacts based on platform if [ "${{ matrix.platform }}" = "windows-latest" ]; then - find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \; + # Only ship the MSI installer on Windows. 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 }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \; find . -name "*.app" -exec cp -r {} "../../../dist/Stirling-PDF-${{ matrix.name }}.app" \; else find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \; + find . -name "*.rpm" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.rpm" \; find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \; fi - name: Upload build artifacts + if: always() && steps.digicert-setup.conclusion != 'failure' uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: Stirling-PDF-${{ matrix.name }} @@ -600,6 +623,7 @@ jobs: ./artifacts/**/*.msi ./artifacts/**/*.dmg ./artifacts/**/*.deb + ./artifacts/**/*.rpm ./artifacts/**/*.AppImage draft: false prerelease: false diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index b3a13e233d..ce7b000475 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -32,17 +32,13 @@ jobs: cache: "npm" cache-dependency-path: frontend/package-lock.json - - name: Install frontend dependencies - run: cd frontend && npm ci - - - name: Generate icons - run: cd frontend && node scripts/generate-icons.js - + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 - name: Install all Playwright browsers - run: cd frontend && npx playwright install --with-deps + run: task frontend:test:e2e:install - name: Run E2E tests (all browsers) - run: cd frontend && npx playwright test + run: task frontend:test:e2e - name: Upload Playwright report if: always() diff --git a/.github/workflows/package-managers.yml b/.github/workflows/package-managers.yml new file mode 100644 index 0000000000..a1c3de0227 --- /dev/null +++ b/.github/workflows/package-managers.yml @@ -0,0 +1,197 @@ +name: Update Package Manager Manifests + +on: + # release: + # types: [released] + workflow_dispatch: + inputs: + version: + description: "Version to test (e.g. 2.9.2 — no v prefix)" + required: true + type: string + dry_run: + description: "Skip the git push at the end (safe test)" + type: boolean + default: true + +permissions: + contents: read + +jobs: + get-release-info: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.info.outputs.version }} + dmg_arm64_sha256: ${{ steps.hashes.outputs.dmg_arm64_sha256 }} + dmg_x86_64_sha256: ${{ steps.hashes.outputs.dmg_x86_64_sha256 }} + msi_sha256: ${{ steps.hashes.outputs.msi_sha256 }} + deb_sha256: ${{ steps.hashes.outputs.deb_sha256 }} + jar_sha256: ${{ steps.hashes.outputs.jar_sha256 }} + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Extract version from tag or manual input + id: info + env: + DISPATCH_VERSION: ${{ inputs.version }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + VERSION="$DISPATCH_VERSION" + else + VERSION="$RELEASE_TAG" + fi + VERSION="${VERSION#v}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Download release assets and compute SHA256 + id: hashes + env: + VERSION: ${{ steps.info.outputs.version }} + GH_TOKEN: ${{ github.token }} + run: | + BASE="https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v${VERSION}" + + download_sha256() { + local url="$1" + local file + file=$(basename "$url") + curl -fsSL --retry 3 -o "$file" "$url" + sha256sum "$file" | awk '{print $1}' + } + + DMG_ARM64_SHA=$(download_sha256 "${BASE}/Stirling-PDF-macos-aarch64.dmg") + DMG_X64_SHA=$(download_sha256 "${BASE}/Stirling-PDF-macos-x86_64.dmg") + MSI_SHA=$(download_sha256 "${BASE}/Stirling-PDF-windows-x86_64.msi") + DEB_SHA=$(download_sha256 "${BASE}/Stirling-PDF-linux-x86_64.deb") + JAR_SHA=$(download_sha256 "${BASE}/Stirling-PDF-with-login.jar") + + echo "dmg_arm64_sha256=$DMG_ARM64_SHA" >> "$GITHUB_OUTPUT" + echo "dmg_x86_64_sha256=$DMG_X64_SHA" >> "$GITHUB_OUTPUT" + echo "msi_sha256=$MSI_SHA" >> "$GITHUB_OUTPUT" + echo "deb_sha256=$DEB_SHA" >> "$GITHUB_OUTPUT" + echo "jar_sha256=$JAR_SHA" >> "$GITHUB_OUTPUT" + + update-homebrew: + needs: get-release-info + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Checkout homebrew tap + uses: actions/checkout@v4 + with: + repository: Stirling-Tools/homebrew-stirling-pdf + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + path: homebrew-tap + + - name: Update cask (stirling-pdf.rb) + env: + VERSION: ${{ needs.get-release-info.outputs.version }} + ARM64_SHA: ${{ needs.get-release-info.outputs.dmg_arm64_sha256 }} + X64_SHA: ${{ needs.get-release-info.outputs.dmg_x86_64_sha256 }} + run: | + CASK="homebrew-tap/Casks/stirling-pdf.rb" + sed -i "s/version \".*\"/version \"${VERSION}\"/" "$CASK" + # Update ARM64 sha256 (line following on_arm block) + awk -v arm="$ARM64_SHA" -v x64="$X64_SHA" ' + /on_arm/ { in_arm=1 } + /on_intel/ { in_arm=0; in_intel=1 } + /end/ { in_arm=0; in_intel=0 } + in_arm && /sha256/ { sub(/sha256 ".*"/, "sha256 \"" arm "\"") } + in_intel && /sha256/ { sub(/sha256 ".*"/, "sha256 \"" x64 "\"") } + { print } + ' "$CASK" > tmp && mv tmp "$CASK" + + - name: Update formula (stirling-pdf-server.rb) + env: + VERSION: ${{ needs.get-release-info.outputs.version }} + JAR_SHA: ${{ needs.get-release-info.outputs.jar_sha256 }} + run: | + FORMULA="homebrew-tap/Formula/stirling-pdf-server.rb" + sed -i "s/version \".*\"/version \"${VERSION}\"/" "$FORMULA" + sed -i "s/sha256 \".*\"/sha256 \"${JAR_SHA}\"/" "$FORMULA" + + - name: Show homebrew tap diff (for dry-run visibility) + working-directory: homebrew-tap + run: | + echo "--- diff --stat ---" + git diff --stat + echo "--- full diff ---" + git diff + + - name: Commit and push homebrew tap updates + if: ${{ github.event_name == 'release' || inputs.dry_run == false }} + working-directory: homebrew-tap + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add Casks/stirling-pdf.rb Formula/stirling-pdf-server.rb + git diff --cached --quiet && echo "No changes" && exit 0 + git commit -m "chore: bump Stirling-PDF to v${{ needs.get-release-info.outputs.version }}" + git push + + update-scoop: + needs: get-release-info + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Checkout Scoop bucket (shared with Homebrew tap) + uses: actions/checkout@v4 + with: + repository: Stirling-Tools/homebrew-stirling-pdf + token: ${{ secrets.SCOOP_BUCKET_TOKEN }} + path: scoop-bucket + + - name: Update stirling-pdf.json + env: + VERSION: ${{ needs.get-release-info.outputs.version }} + MSI_SHA: ${{ needs.get-release-info.outputs.msi_sha256 }} + run: | + MANIFEST="scoop-bucket/scoop/stirling-pdf.json" + jq --arg v "$VERSION" --arg h "$MSI_SHA" \ + '.version = $v | .architecture["64bit"].url = "https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v\($v)/Stirling-PDF-windows-x86_64.msi" | .architecture["64bit"].hash = $h' \ + "$MANIFEST" > tmp.json && mv tmp.json "$MANIFEST" + + - name: Update stirling-pdf-server.json + env: + VERSION: ${{ needs.get-release-info.outputs.version }} + JAR_SHA: ${{ needs.get-release-info.outputs.jar_sha256 }} + run: | + MANIFEST="scoop-bucket/scoop/stirling-pdf-server.json" + jq --arg v "$VERSION" --arg h "$JAR_SHA" \ + '.version = $v | .url = "https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v\($v)/Stirling-PDF-with-login.jar" | .hash = $h' \ + "$MANIFEST" > tmp.json && mv tmp.json "$MANIFEST" + + - name: Show Scoop bucket diff (for dry-run visibility) + working-directory: scoop-bucket + run: | + echo "--- diff --stat ---" + git diff --stat + echo "--- full diff ---" + git diff + + - name: Commit and push Scoop bucket updates + if: ${{ github.event_name == 'release' || inputs.dry_run == false }} + working-directory: scoop-bucket + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add scoop/stirling-pdf.json scoop/stirling-pdf-server.json + git diff --cached --quiet && echo "No changes" && exit 0 + git commit -m "chore: bump Stirling-PDF to v${{ needs.get-release-info.outputs.version }}" + git push diff --git a/.github/workflows/pre_commit.yml b/.github/workflows/pre_commit.yml index 764f748d44..9e4d5d574f 100644 --- a/.github/workflows/pre_commit.yml +++ b/.github/workflows/pre_commit.yml @@ -2,7 +2,7 @@ name: Pre-commit on: workflow_dispatch: - push: + pull_request: branches: - main @@ -16,9 +16,6 @@ jobs: # Prevents sdist builds → no tar extraction PIP_ONLY_BINARY: ":all:" PIP_DISABLE_PIP_VERSION_CHECK: "1" - permissions: - contents: write - pull-requests: write steps: - name: Harden Runner uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 @@ -31,13 +28,6 @@ jobs: fetch-depth: 0 persist-credentials: false - - 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: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: @@ -57,47 +47,4 @@ jobs: pre-commit run gitleaks --all-files -c .pre-commit-config.yaml pre-commit run end-of-file-fixer --all-files -c .pre-commit-config.yaml pre-commit run trailing-whitespace --all-files -c .pre-commit-config.yaml - continue-on-error: true - - - 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@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1 - with: - gradle-version: 9.3.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 }} - - - name: git add - run: | - git add . - git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV - - - name: Create Pull Request - if: env.CHANGES_DETECTED == 'true' - uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 - with: - token: ${{ steps.setup-bot.outputs.token }} - commit-message: ":file_folder: pre-commit" - committer: ${{ steps.setup-bot.outputs.committer }} - author: ${{ steps.setup-bot.outputs.committer }} - signoff: true - branch: pre-commit - title: "🤖 format everything with pre-commit by ${{ steps.setup-bot.outputs.app-slug }}" - body: | - Auto-generated by [create-pull-request][1] with **${{ steps.setup-bot.outputs.app-slug }}** - - [1]: https://github.com/peter-evans/create-pull-request - draft: false - delete-branch: true - labels: github-actions - sign-commits: true + git diff --exit-code diff --git a/.github/workflows/push-docker-base.yml b/.github/workflows/push-docker-base.yml index 59561ed0df..699eb4708c 100644 --- a/.github/workflows/push-docker-base.yml +++ b/.github/workflows/push-docker-base.yml @@ -4,6 +4,7 @@ on: push: branches: - baseDockerImage + - accessIssueFix workflow_dispatch: inputs: version: @@ -34,6 +35,8 @@ jobs: run: | if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then VERSION="${{ github.event.inputs.version }}" + elif [ "${{ github.ref_name }}" == "accessIssueFix" ]; then + VERSION="1.0.3" else VERSION="1.0.0" fi diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index 11151557ea..244c044bd8 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -64,6 +64,8 @@ jobs: id: buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 - name: Get version number id: versionNumber run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT diff --git a/.github/workflows/rollback-latest.yml b/.github/workflows/rollback-latest.yml new file mode 100644 index 0000000000..21027cc762 --- /dev/null +++ b/.github/workflows/rollback-latest.yml @@ -0,0 +1,93 @@ +name: Rollback Latest Tags to Version + +on: + workflow_dispatch: + inputs: + version: + description: "Version to rollback to (e.g. 2.8.0)" + required: true + type: string + +permissions: + contents: read + +jobs: + rollback: + runs-on: ubuntu-latest + permissions: + packages: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + with: + egress-policy: audit + + - name: Install crane + uses: imjasonh/setup-crane@31b88afe9de28ae0ffa220711af4b60be9435f6e # v0.4 + + - name: Login to Docker Hub + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_API }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + 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: Rollback all latest tags to v${{ inputs.version }} + env: + VERSION: ${{ inputs.version }} + DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} + DOCKER_HUB_ORG_USERNAME: ${{ secrets.DOCKER_HUB_ORG_USERNAME }} + REPO_OWNER: ${{ steps.repoowner.outputs.lowercase }} + run: | + set -euo pipefail + + IMAGES=( + "${DOCKER_HUB_USERNAME}/s-pdf" + "ghcr.io/${REPO_OWNER}/s-pdf" + "ghcr.io/${REPO_OWNER}/stirling-pdf" + "${DOCKER_HUB_ORG_USERNAME}/stirling-pdf" + ) + + VARIANTS=( + "${VERSION}:latest" + "${VERSION}-fat:latest-fat" + "${VERSION}-ultra-lite:latest-ultra-lite" + ) + + FAILED=0 + + for image in "${IMAGES[@]}"; do + for variant in "${VARIANTS[@]}"; do + SOURCE_TAG="${variant%%:*}" + TARGET_TAG="${variant##*:}" + + echo "::group::${image} — ${SOURCE_TAG} → ${TARGET_TAG}" + + if crane manifest "${image}:${SOURCE_TAG}" > /dev/null 2>&1; then + crane cp "${image}:${SOURCE_TAG}" "${image}:${TARGET_TAG}" + echo "✅ ${image}:${TARGET_TAG} now points to ${SOURCE_TAG}" + else + echo "::warning::âš ï¸ ${image}:${SOURCE_TAG} not found, skipping" + FAILED=1 + fi + + echo "::endgroup::" + done + done + + if [ "$FAILED" -ne 0 ]; then + echo "::warning::Some source tags were not found. This is expected if not all variants exist for this version." + fi + + echo "" + echo "🎉 Rollback to ${VERSION} complete!" diff --git a/.github/workflows/swagger.yml b/.github/workflows/swagger.yml index 78195183ad..2b34ab9ca6 100644 --- a/.github/workflows/swagger.yml +++ b/.github/workflows/swagger.yml @@ -56,6 +56,8 @@ jobs: SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }} SWAGGERHUB_USER: "Frooodle" + - name: Install Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 - name: Get version number id: versionNumber run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml index d7ca66b2e0..b03f5bfee4 100644 --- a/.github/workflows/tauri-build.yml +++ b/.github/workflows/tauri-build.yml @@ -128,86 +128,16 @@ jobs: with: gradle-version: 9.3.1 - - name: Build Java backend with JLink - working-directory: ./ - shell: bash - run: | - chmod +x ./gradlew - echo "🔧 Building Stirling-PDF JAR..." - # STIRLING_PDF_DESKTOP_UI=false ./gradlew bootJar --no-daemon - ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube - - # Find the built JAR - STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1) - echo "✅ Built JAR: $STIRLING_JAR" - - # Create Tauri directories - mkdir -p ./frontend/src-tauri/libs - mkdir -p ./frontend/src-tauri/runtime - - # Copy JAR to Tauri libs - cp "$STIRLING_JAR" ./frontend/src-tauri/libs/ - echo "✅ JAR copied to Tauri libs" - - # Analyze JAR dependencies for jlink modules - echo "🔠Analyzing JAR dependencies..." - if command -v jdeps &> /dev/null; then - DETECTED_MODULES=$(jdeps --print-module-deps --ignore-missing-deps "$STIRLING_JAR" 2>/dev/null || echo "") - if [ -n "$DETECTED_MODULES" ]; then - echo "📋 jdeps detected modules: $DETECTED_MODULES" - MODULES="$DETECTED_MODULES,java.compiler,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported" - else - echo "âš ï¸ jdeps analysis failed, using predefined modules" - MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported" - fi - else - echo "âš ï¸ jdeps not available, using predefined modules" - MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported" - fi - - # Create custom JRE with jlink (always rebuild) - echo "🔧 Creating custom JRE with jlink..." - echo "📋 Using modules: $MODULES" - - # Remove any existing JRE - rm -rf ./frontend/src-tauri/runtime/jre - - # Create the custom JRE - jlink \ - --add-modules "$MODULES" \ - --strip-debug \ - --compress=2 \ - --no-header-files \ - --no-man-pages \ - --output ./frontend/src-tauri/runtime/jre - - if [ ! -d "./frontend/src-tauri/runtime/jre" ]; then - echo "⌠Failed to create JLink runtime" - exit 1 - fi - - # Test the bundled runtime - if [ -f "./frontend/src-tauri/runtime/jre/bin/java" ]; then - RUNTIME_VERSION=$(./frontend/src-tauri/runtime/jre/bin/java --version 2>&1 | head -n 1) - echo "✅ Custom JRE created successfully: $RUNTIME_VERSION" - else - echo "⌠Custom JRE executable not found" - exit 1 - fi - - # Calculate runtime size - RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1) - echo "📊 Custom JRE size: $RUNTIME_SIZE" + - name: Setup Task + uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0 + - name: Prepare desktop build + run: task desktop:prepare env: MAVEN_USER: ${{ secrets.MAVEN_USER }} MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }} DISABLE_ADDITIONAL_FEATURES: true - - name: Install frontend dependencies - working-directory: ./frontend - run: npm ci - # DigiCert KeyLocker Setup (Cloud HSM) - name: Setup DigiCert KeyLocker id: digicert-setup @@ -330,6 +260,58 @@ jobs: echo "Available tools:" ls -la /usr/bin/hd* || echo "No hd* tools found" + - name: Preflight smctl + if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} + shell: pwsh + env: + KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} + run: | + & smctl healthcheck + if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl healthcheck failed"; exit 1 } + & smctl keypair ls + if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] smctl keypair ls failed"; exit 1 } + & smctl windows certsync --keypair-alias "$env:KEYPAIR_ALIAS" + if ($LASTEXITCODE -ne 0) { Write-Host "[WARN] smctl windows certsync returned non-zero - continuing" } + + - name: Configure Windows code signing + if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} + shell: bash + env: + KEYPAIR_ALIAS: ${{ secrets.SM_KEYPAIR_ALIAS }} + run: | + cat > ./frontend/src-tauri/tauri.windows.conf.json < /usr/lib/libjvm.so" + else + echo "libjvm not found at $JAVA_LIBJVM" + exit 1 + fi + - name: Build Tauri app uses: tauri-apps/tauri-action@51a9f1156b33df106d827c3a78f8f894946c5faa # v0.5.25 env: @@ -340,178 +322,35 @@ jobs: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }} + # AppImage signing — three env vars work together: + # SIGN=1 tells linuxdeploy-plugin-appimage to forward --sign to appimagetool + # 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. + SIGN: "1" + APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.RELEASE_GPG_PASSPHRASE }} + SIGN_KEY: ${{ vars.RELEASE_GPG_FINGERPRINT }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb' }} VITE_SAAS_SERVER_URL: ${{ secrets.VITE_SAAS_SERVER_URL || 'https://app.stirlingpdf.com' }} VITE_SAAS_BACKEND_API_URL: ${{ secrets.VITE_SAAS_BACKEND_API_URL || 'https://api.stirlingpdf.com' }} - # Only enable Windows signing in Tauri when on main - SIGN: ${{ github.ref == 'refs/heads/main' && (env.SM_API_KEY == '' && env.WINDOWS_CERTIFICATE != '') && '1' || '0' }} + SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }} CI: true with: projectPath: ./frontend tauriScript: npx tauri args: ${{ matrix.args }} - # Sign with DigiCert KeyLocker (post-build) - - name: Sign Windows binaries with DigiCert KeyLocker - if: ${{ matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' }} - shell: pwsh + - name: Clear release GPG key from runner keyring (Linux) + if: always() && matrix.platform == 'ubuntu-22.04' + env: + RELEASE_GPG_FINGERPRINT: ${{ vars.RELEASE_GPG_FINGERPRINT }} run: | - Write-Host "=== DigiCert KeyLocker Signing ===" - - # Test smctl connectivity first - Write-Host "Testing smctl connection..." - $healthCheck = & smctl healthcheck 2>&1 - if ($LASTEXITCODE -eq 0) { - Write-Host "[SUCCESS] Connected to DigiCert KeyLocker" - } else { - Write-Host "[ERROR] Failed to connect to DigiCert KeyLocker" - Write-Host $healthCheck - exit 1 - } - Write-Host "" - - # Sync certificates to Windows certificate store - Write-Host "Syncing certificates to Windows certificate store..." - $syncOutput = & smctl windows certsync 2>&1 - Write-Host "Cert sync result: $syncOutput" - Write-Host "" - - # List available certificates and check if they have certificates attached - Write-Host "Checking for available certificates..." - $certList = & smctl keypair ls 2>&1 - Write-Host "Keypair list output:" - Write-Host $certList - Write-Host "" - - # Parse the output to check certificate status - $lines = $certList -split "`n" - $foundKeypair = $false - $hasCertificate = $false - - foreach ($line in $lines) { - if ($line -match "${{ secrets.SM_KEYPAIR_ALIAS }}") { - $foundKeypair = $true - Write-Host "[SUCCESS] Found keypair in list" - - # Check if this line has certificate info (not just empty spaces after alias) - $parts = $line -split "\s+" - if ($parts.Count -gt 2 -and $parts[1] -ne "" -and $parts[1] -ne "CERTIFICATE") { - $hasCertificate = $true - Write-Host "[SUCCESS] Certificate is associated with keypair" - } - } - } - - if (-not $foundKeypair) { - Write-Host "[ERROR] Keypair not found: ${{ secrets.SM_KEYPAIR_ALIAS }}" - Write-Host "Available keypairs are listed above" - Write-Host "" - Write-Host "Please verify:" - Write-Host " 1. Keypair alias is correct in GitHub secret" - Write-Host " 2. API key has access to this keypair" - exit 1 - } - - if (-not $hasCertificate) { - Write-Host "[ERROR] No certificate associated with keypair" - Write-Host "This usually means:" - Write-Host " 1. Certificate not yet synced to KeyLocker (run sync manually)" - Write-Host " 2. Certificate is pending approval" - Write-Host " 3. Certificate needs to be attached to the keypair" - Write-Host "" - Write-Host "Try running in DigiCert ONE portal:" - Write-Host " smctl keypair sync" - exit 1 - } - - Write-Host "[SUCCESS] Certificate check passed" - Write-Host "" - - # Find only the files we need to sign (not build scripts) - $filesToSign = @() - - # Main application executable - $mainExe = Get-ChildItem -Path "./frontend/src-tauri/target/x86_64-pc-windows-msvc/release" -Filter "stirling-pdf.exe" -File -ErrorAction SilentlyContinue - if ($mainExe) { $filesToSign += $mainExe } - - # MSI installer - $msiFiles = Get-ChildItem -Path "./frontend/src-tauri/target" -Filter "*.msi" -Recurse -File - $filesToSign += $msiFiles - - if ($filesToSign.Count -eq 0) { - Write-Host "[ERROR] No files found to sign" - exit 1 - } - - Write-Host "Found $($filesToSign.Count) files to sign:" - foreach ($f in $filesToSign) { Write-Host " - $($f.Name)" } - Write-Host "" - - $signedCount = 0 - foreach ($file in $filesToSign) { - Write-Host "Signing: $($file.Name)" - - # Get PKCS11 config file path (set by DigiCert action) - $pkcs11Config = $env:PKCS11_CONFIG - if (-not $pkcs11Config) { - Write-Host "[ERROR] PKCS11_CONFIG environment variable not set" - Write-Host "DigiCert KeyLocker action may not have run correctly" - exit 1 - } - - Write-Host "Using PKCS11 config: $pkcs11Config" - - # Try signing with certificate fingerprint first (if available) - $fingerprint = "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}" - if ($fingerprint -and $fingerprint -ne "") { - Write-Host "Attempting to sign with certificate fingerprint..." - $output = & smctl sign --fingerprint "$fingerprint" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1 - $exitCode = $LASTEXITCODE - } else { - Write-Host "No fingerprint provided, using keypair alias..." - # Use smctl to sign with keypair alias - $output = & smctl sign --keypair-alias "${{ secrets.SM_KEYPAIR_ALIAS }}" --input "$($file.FullName)" --config-file "$pkcs11Config" --verbose 2>&1 - $exitCode = $LASTEXITCODE - } - - Write-Host "Exit code: $exitCode" - Write-Host "Output: $output" - - # Check if output contains "FAILED" even with exit code 0 - if ($output -match "FAILED" -or $output -match "error" -or $output -match "Error") { - Write-Host "" - Write-Host "[ERROR] Signing failed for $($file.Name)" - Write-Host "[ERROR] smctl returned success but output indicates failure" - Write-Host "" - Write-Host "Possible issues:" - Write-Host " 1. Certificate not fully synced to KeyLocker (wait a few minutes)" - Write-Host " 2. Incorrect keypair alias" - Write-Host " 3. API key lacks signing permissions" - Write-Host "" - Write-Host "Please verify in DigiCert ONE portal:" - Write-Host " - Certificate status is 'Issued' (not Pending)" - Write-Host " - Keypair status is 'Online'" - Write-Host " - 'Can sign' is set to 'Yes'" - exit 1 - } - - if ($exitCode -ne 0) { - Write-Host "[ERROR] Failed to sign $($file.Name)" - Write-Host "Full error output:" - Write-Host $output - exit 1 - } - - $signedCount++ - Write-Host "[SUCCESS] Signed: $($file.Name)" - Write-Host "" - } - - Write-Host "=== Summary ===" - Write-Host "[SUCCESS] Signed $signedCount/$($filesToSign.Count) files successfully" + if [ -n "$RELEASE_GPG_FINGERPRINT" ]; then + gpg --batch --yes --delete-secret-keys "$RELEASE_GPG_FINGERPRINT" || true + gpg --batch --yes --delete-keys "$RELEASE_GPG_FINGERPRINT" || true + fi - name: Verify notarization (macOS only) if: matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel' @@ -536,73 +375,80 @@ jobs: # Find and rename artifacts based on platform if [ "${{ matrix.platform }}" = "windows-latest" ]; then - find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \; + # 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 }}" = "macos-15" ] || [ "${{ matrix.platform }}" = "macos-15-intel" ]; then find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \; else find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \; + find . -name "*.rpm" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.rpm" \; find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \; fi + # 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: matrix.platform == 'windows-latest' && github.ref == 'refs/heads/main' + if: matrix.platform == 'windows-latest' && env.SM_API_KEY != '' && github.ref == 'refs/heads/main' shell: pwsh run: | - Write-Host "Verifying Windows code signatures..." - - $exePath = "./dist/Stirling-PDF-${{ matrix.name }}.exe" + $allSigned = $true $msiPath = "./dist/Stirling-PDF-${{ matrix.name }}.msi" - $allSigned = $true - $usingKeyLocker = "${{ env.SM_API_KEY }}" -ne "" - $usingPfx = "${{ env.WINDOWS_CERTIFICATE }}" -ne "" - - # Check EXE signature - if (Test-Path $exePath) { - $exeSig = Get-AuthenticodeSignature -FilePath $exePath - Write-Host "EXE Signature Status: $($exeSig.Status)" - Write-Host "EXE Signer: $($exeSig.SignerCertificate.Subject)" - Write-Host "EXE Timestamp: $($exeSig.TimeStamperCertificate.NotAfter)" - - if ($exeSig.Status -ne "Valid") { - Write-Host "[WARNING] EXE is not properly signed (Status: $($exeSig.Status))" - if ($usingKeyLocker -or $usingPfx) { - Write-Host "[ERROR] Certificate was provided but signing failed" - $allSigned = $false - } else { - Write-Host "[INFO] Building unsigned binary (no certificate provided)" - } - } else { - Write-Host "[SUCCESS] EXE is properly signed" - } - } - - # Check MSI signature + # Check MSI (outer wrapper) if (Test-Path $msiPath) { - $msiSig = Get-AuthenticodeSignature -FilePath $msiPath - Write-Host "MSI Signature Status: $($msiSig.Status)" - Write-Host "MSI Signer: $($msiSig.SignerCertificate.Subject)" - Write-Host "MSI Timestamp: $($msiSig.TimeStamperCertificate.NotAfter)" + $sig = Get-AuthenticodeSignature -FilePath $msiPath + Write-Host "MSI: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)" + if ($sig.Status -ne "Valid") { + Write-Host "[ERROR] MSI is not signed" + $allSigned = $false + } - if ($msiSig.Status -ne "Valid") { - Write-Host "[WARNING] MSI is not properly signed (Status: $($msiSig.Status))" - if ($usingKeyLocker -or $usingPfx) { - Write-Host "[ERROR] Certificate was provided but signing failed" - $allSigned = $false + # Extract MSI and verify inner exe + $extractDir = Join-Path $env:RUNNER_TEMP "msi-verify" + if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force } + $proc = Start-Process msiexec.exe -ArgumentList '/a', $msiPath, '/qn', "TARGETDIR=$extractDir" -Wait -PassThru -NoNewWindow + if ($proc.ExitCode -eq 0) { + $innerExe = Get-ChildItem -Path $extractDir -Filter "stirling-pdf.exe" -Recurse -File | Select-Object -First 1 + if ($innerExe) { + $sig = Get-AuthenticodeSignature -FilePath $innerExe.FullName + Write-Host "Inner EXE: Status=$($sig.Status), Signer=$($sig.SignerCertificate.Subject)" + if ($sig.Status -ne "Valid") { + Write-Host "[ERROR] Inner exe is NOT signed - AV will flag this at runtime" + $allSigned = $false + } } else { - Write-Host "[INFO] Building unsigned binary (no certificate provided)" + Write-Host "[ERROR] Could not find stirling-pdf.exe inside MSI" + $allSigned = $false } } else { - Write-Host "[SUCCESS] MSI is properly signed" + Write-Host "[ERROR] MSI extraction failed (exit code: $($proc.ExitCode))" + $allSigned = $false } + } else { + Write-Host "[ERROR] MSI not found at $msiPath" + $allSigned = $false } - if (($usingKeyLocker -or $usingPfx) -and -not $allSigned) { - Write-Host "[ERROR] Code signing verification failed" + if (-not $allSigned) { + Write-Host "[ERROR] Signature verification failed" exit 1 + } + 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 != '' }} + shell: pwsh + run: | + $logDir = "$env:USERPROFILE\.signingmanager\logs" + if (Test-Path $logDir) { + Get-ChildItem $logDir | ForEach-Object { + Write-Host "=== $($_.FullName) ===" + Get-Content $_.FullName -Tail 200 + Write-Host "" + } } else { - Write-Host "[SUCCESS] Code signature verification completed" + Write-Host "smctl log directory not found at $logDir" } - name: Upload artifacts @@ -634,8 +480,8 @@ jobs: fi else echo "Checking for Linux artifacts..." - find . -name "*.deb" -o -name "*.AppImage" | head -5 - if [ $(find . -name "*.deb" -o -name "*.AppImage" | wc -l) -eq 0 ]; then + find . -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" | head -5 + if [ $(find . -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" | wc -l) -eq 0 ]; then echo "⌠No Linux artifacts found" exit 1 fi @@ -648,7 +494,7 @@ jobs: run: | cd ./frontend/src-tauri/target echo "Artifact sizes for ${{ matrix.name }}:" - find . -name "*.exe" -o -name "*.dmg" -o -name "*.deb" -o -name "*.AppImage" -o -name "*.msi" | while read file; do + find . -name "*.exe" -o -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" -o -name "*.msi" | while read file; do if [ -f "$file" ]; then size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo "unknown") echo "$file: $size bytes" @@ -692,7 +538,7 @@ jobs: 'Stirling-PDF-windows-x86_64': { icon: '🪟', platform: 'Windows x64', files: '.exe, .msi' }, 'Stirling-PDF-macos-aarch64': { icon: 'ðŸŽ', platform: 'macOS ARM64', files: '.dmg' }, 'Stirling-PDF-macos-x86_64': { icon: 'ðŸŽ', platform: 'macOS Intel', files: '.dmg' }, - 'Stirling-PDF-linux-x86_64': { icon: 'ðŸ§', platform: 'Linux x64', files: '.deb, .AppImage' } + 'Stirling-PDF-linux-x86_64': { icon: 'ðŸ§', platform: 'Linux x64', files: '.deb, .rpm, .AppImage' } }; let commentBody = `## 📦 Tauri Desktop Builds Ready!\n\n`; diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml index 5d2466013b..4d61ce5df0 100644 --- a/.github/workflows/testdriver.yml +++ b/.github/workflows/testdriver.yml @@ -167,9 +167,9 @@ jobs: with: key: ${{secrets.TESTDRIVER_API_KEY}} prerun: | + choco install go-task -y + task frontend:build cd frontend - npm install - npm run build 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 diff --git a/.gitignore b/.gitignore index b5025f1b4f..d6dc4826ca 100644 --- a/.gitignore +++ b/.gitignore @@ -165,6 +165,7 @@ __pycache__/ # Virtual environments .env* !.env*.example +!engine/.env .venv* env*/ venv*/ @@ -181,6 +182,7 @@ venv.bak/ .idea/ *.iml out/ +.junie/ # Ignore Mac DS_Store files .DS_Store @@ -216,8 +218,14 @@ id_ecdsa.pub id_ed25519 id_ed25519.pub .ssh/ + +# Allow the published GPG release signing public key (safe to share) +!docs/security/signing-key.pub *ssh +# Taskfile checksum cache +.task/ + # cache .cache .ruff_cache @@ -254,3 +262,6 @@ docs/type3/signatures/ # Type3 sample PDFs (development only) **/type3/samples/ + +# Claude +.claude/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 928602fdda..5279547522 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: hooks: - id: codespell args: - - --ignore-words-list=thirdParty,tabEl,tabEls,Sie,ist + - --ignore-words-list=thirdParty,tabEl,tabEls,Sie,ist,fulfilment - --skip="./.*,*.csv,*.json,*.ambr" - --quiet-level=2 files: \.(html|css|js|py|md)$ diff --git a/.taskfiles/backend.yml b/.taskfiles/backend.yml new file mode 100644 index 0000000000..7e487d092c --- /dev/null +++ b/.taskfiles/backend.yml @@ -0,0 +1,117 @@ +version: '3' + +tasks: + dev: + desc: "Start backend dev server" + ignore_error: true + cmds: + - cmd: cmd /c gradlew.bat :stirling-pdf:bootRun + platforms: [windows] + - cmd: ./gradlew :stirling-pdf:bootRun + platforms: [linux, darwin] + + build: + desc: "Full backend build" + cmds: + - cmd: cmd /c gradlew.bat clean build + platforms: [windows] + - cmd: ./gradlew clean build + platforms: [linux, darwin] + + build:fast: + desc: "Build without tests" + cmds: + - cmd: cmd /c gradlew.bat clean build -x test + platforms: [windows] + - cmd: ./gradlew clean build -x test + platforms: [linux, darwin] + + build:ci: + desc: "Build for CI (formatting checked separately)" + cmds: + - cmd: cmd /c gradlew.bat build -PnoSpotless + platforms: [windows] + - cmd: ./gradlew build -PnoSpotless + platforms: [linux, darwin] + + test: + desc: "Run backend tests" + cmds: + - cmd: cmd /c gradlew.bat test + platforms: [windows] + - cmd: ./gradlew test + platforms: [linux, darwin] + + format: + desc: "Auto-fix code formatting" + cmds: + - cmd: cmd /c gradlew.bat spotlessApply + platforms: [windows] + - cmd: ./gradlew spotlessApply + platforms: [linux, darwin] + + format:check: + desc: "Check code formatting" + cmds: + - cmd: cmd /c gradlew.bat spotlessCheck + platforms: [windows] + - cmd: ./gradlew spotlessCheck + platforms: [linux, darwin] + + fix: + desc: "Auto-fix backend" + cmds: + - task: format + + swagger: + desc: "Generate OpenAPI docs" + cmds: + - cmd: cmd /c gradlew.bat :stirling-pdf:copySwaggerDoc + platforms: [windows] + - cmd: ./gradlew :stirling-pdf:copySwaggerDoc + platforms: [linux, darwin] + sources: + - app/core/src/main/java/**/*.java + - app/proprietary/src/main/java/**/*.java + - app/common/src/main/java/**/*.java + generates: + - SwaggerDoc.json + + check: + desc: "Backend quality gate" + cmds: + - task: format:check + - task: test + + version: + desc: "Print project version" + silent: true + cmds: + - cmd: cmd /c gradlew.bat printVersion --quiet | tail -1 + platforms: [windows] + - cmd: ./gradlew printVersion --quiet | tail -1 + platforms: [linux, darwin] + + licenses:check: + desc: "Check dependency licenses" + cmds: + - cmd: cmd /c gradlew.bat checkLicense --no-parallel + platforms: [windows] + - cmd: ./gradlew checkLicense --no-parallel + platforms: [linux, darwin] + + licenses:generate: + desc: "Check and generate dependency license report" + cmds: + - cmd: cmd /c gradlew.bat checkLicense generateLicenseReport --no-parallel + platforms: [windows] + - cmd: ./gradlew checkLicense generateLicenseReport --no-parallel + platforms: [linux, darwin] + + clean: + desc: "Clean build artifacts" + cmds: + - cmd: cmd /c gradlew.bat clean + platforms: [windows] + - cmd: ./gradlew clean + platforms: [linux, darwin] diff --git a/.taskfiles/desktop.yml b/.taskfiles/desktop.yml new file mode 100644 index 0000000000..0bf938e806 --- /dev/null +++ b/.taskfiles/desktop.yml @@ -0,0 +1,105 @@ +version: '3' + +vars: + JLINK_MODULES: "java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported" + +tasks: + prepare: + desc: "Prepare desktop build dependencies" + deps: [jlink, ":frontend:prepare:desktop", provisioner] + + provisioner: + desc: "Build installer provisioner" + platforms: [windows] + cmds: + - node scripts/build-provisioner.mjs + + dev: + desc: "Start Tauri desktop dev mode" + deps: [prepare] + ignore_error: true + cmds: + - npx tauri dev --no-watch + + build: + desc: "Build Tauri desktop app (production)" + deps: [prepare] + cmds: + - npx tauri build + + build:dev: + desc: "Build Tauri desktop app (dev, no bundling)" + deps: [prepare] + cmds: + - npx tauri build --no-bundle + + build:dev:mac: + desc: "Build Tauri desktop .app bundle (macOS)" + deps: [prepare] + cmds: + - npx tauri build --bundles app + + build:dev:windows: + desc: "Build Tauri desktop NSIS installer (Windows)" + deps: [prepare] + cmds: + - npx tauri build --bundles nsis + + build:dev:linux: + desc: "Build Tauri desktop AppImage (Linux)" + deps: [prepare] + cmds: + - npx tauri build --bundles appimage + + clean: + desc: "Clean Tauri/Cargo build artifacts" + cmds: + - task: jlink:clean + - cd src-tauri && cargo clean + - rm -rf dist build + + # ============================================================ + # JLink — Build bundled Java runtime for Tauri + # ============================================================ + + jlink: + desc: "Build backend JAR and create JLink runtime for Tauri" + deps: [jlink:jar, jlink:runtime] + + jlink:jar: + desc: "Build backend JAR for Tauri bundling" + run: once + dir: .. + env: + DISABLE_ADDITIONAL_FEATURES: "true" + cmds: + - cmd: cmd /c gradlew.bat bootJar --no-daemon + platforms: [windows] + - cmd: ./gradlew bootJar --no-daemon + platforms: [linux, darwin] + - mkdir -p frontend/src-tauri/libs + - cp app/core/build/libs/stirling-pdf-*.jar frontend/src-tauri/libs/ + status: + - test -f frontend/src-tauri/libs/stirling-pdf-*.jar + + jlink:runtime: + desc: "Create custom JRE with jlink" + deps: [jlink:jar] + cmds: + - rm -rf src-tauri/runtime/jre + - mkdir -p src-tauri/runtime + - >- + jlink + --add-modules {{.JLINK_MODULES}} + --strip-debug + --compress=2 + --no-header-files + --no-man-pages + --output src-tauri/runtime/jre + status: + - test -d src-tauri/runtime/jre + + jlink:clean: + desc: "Remove JLink runtime and bundled JARs" + cmds: + - rm -rf src-tauri/libs src-tauri/runtime diff --git a/.taskfiles/docker.yml b/.taskfiles/docker.yml new file mode 100644 index 0000000000..eca54f4a85 --- /dev/null +++ b/.taskfiles/docker.yml @@ -0,0 +1,57 @@ +version: '3' + +vars: + COMPOSE_DIR: docker/compose + EMBEDDED_DIR: docker/embedded + +tasks: + build: + desc: "Build standard Docker image" + cmds: + - docker build -t stirling-pdf -f {{.EMBEDDED_DIR}}/Dockerfile . + + build:fat: + desc: "Build fat Docker image (all features)" + cmds: + - docker build -t stirling-pdf-fat -f {{.EMBEDDED_DIR}}/Dockerfile.fat . + + build:ultra-lite: + desc: "Build ultra-lite Docker image" + cmds: + - docker build -t stirling-pdf-ultra-lite -f {{.EMBEDDED_DIR}}/Dockerfile.ultra-lite . + + build:frontend: + desc: "Build frontend-only Docker image" + cmds: + - docker build -t stirling-pdf-frontend -f docker/frontend/Dockerfile . + + build:engine: + desc: "Build engine Docker image" + dir: engine + cmds: + - docker build -t stirling-pdf-engine . + + up: + desc: "Start standard docker compose stack" + cmds: + - docker compose -f {{.COMPOSE_DIR}}/docker-compose.yml up -d + + up:fat: + desc: "Start fat docker compose stack" + cmds: + - docker compose -f {{.COMPOSE_DIR}}/docker-compose.fat.yml up -d + + up:ultra-lite: + desc: "Start ultra-lite docker compose stack" + cmds: + - docker compose -f {{.COMPOSE_DIR}}/docker-compose.ultra-lite.yml up -d + + down: + desc: "Stop all running docker compose stacks" + cmds: + - docker compose -f {{.COMPOSE_DIR}}/docker-compose.yml down + + logs: + desc: "Tail docker compose logs" + cmds: + - docker compose -f {{.COMPOSE_DIR}}/docker-compose.yml logs -f diff --git a/.taskfiles/engine.yml b/.taskfiles/engine.yml new file mode 100644 index 0000000000..9d830e0808 --- /dev/null +++ b/.taskfiles/engine.yml @@ -0,0 +1,127 @@ +version: '3' + +tasks: + install: + desc: "Install engine dependencies" + run: once + cmds: + - uv python install 3.13.8 + - uv sync + sources: + - uv.lock + - pyproject.toml + status: + - test -d .venv + + prepare: + desc: "Set up engine .env from template" + deps: [install] + cmds: + - uv run scripts/setup_env.py + sources: + - scripts/setup_env.py + generates: + - .env.local + + run: + desc: "Run engine server" + deps: [prepare] + ignore_error: true + dir: src + env: + PYTHONUNBUFFERED: "1" + cmds: + - uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port 5001 + + dev: + desc: "Start engine dev server with hot reload" + deps: [prepare] + ignore_error: true + dir: src + env: + PYTHONUNBUFFERED: "1" + cmds: + - uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port 5001 --reload + + lint: + desc: "Run linting" + deps: [install] + cmds: + - uv run ruff check . + + lint:fix: + desc: "Auto-fix lint issues" + deps: [install] + cmds: + - uv run ruff check . --fix + + format: + desc: "Auto-fix code formatting" + deps: [install] + cmds: + - uv run ruff format . + + format:check: + desc: "Check code formatting" + deps: [install] + cmds: + - uv run ruff format . --diff + + typecheck: + desc: "Run type checking" + deps: [install] + cmds: + - uv run pyright . --warnings + + test: + desc: "Run tests" + deps: [prepare] + cmds: + - uv run pytest tests + + fix: + desc: "Auto-fix lint + format" + cmds: + - task: lint:fix + - task: format + + check: + desc: "Full engine quality gate" + cmds: + - task: typecheck + - task: lint + - task: format:check + - task: test + + tool-models: + 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 + sources: + - ../SwaggerDoc.json + - scripts/generate_tool_models.py + generates: + - src/stirling/models/tool_models.py + + clean: + desc: "Clean build artifacts" + cmds: + - task: '{{if eq .OS "Windows_NT"}}clean-windows{{else}}clean-unix{{end}}' + + clean-unix: + internal: true + desc: "Clean build artifacts" + cmds: + - rm -rf .venv data logs output + + # On Windows, use PowerShell as bash failed to delete some dependencies + clean-windows: + internal: true + desc: "Clean build artifacts" + ignore_error: true + cmds: + - powershell rm -Recurse -Force -ErrorAction SilentlyContinue .venv + - powershell rm -Recurse -Force -ErrorAction SilentlyContinue data + - powershell rm -Recurse -Force -ErrorAction SilentlyContinue logs + - powershell rm -Recurse -Force -ErrorAction SilentlyContinue output diff --git a/.taskfiles/frontend.yml b/.taskfiles/frontend.yml new file mode 100644 index 0000000000..4156a34f1c --- /dev/null +++ b/.taskfiles/frontend.yml @@ -0,0 +1,313 @@ +version: '3' + +tasks: + install: + desc: "Install dependencies" + run: once + cmds: + - '{{ if eq .CI "true" }}npm ci{{ else }}npm install{{ end }}' + sources: + - package-lock.json + - package.json + status: + - test -d node_modules + env: + CI: '{{ .CI | default "false" }}' + + prepare:env: + desc: "Generate .env from example if missing" + run: once + deps: [install] + cmds: + - npx tsx scripts/setup-env.ts + sources: + - scripts/setup-env.ts + - config/.env.example + generates: + - .env + + prepare:env:saas: + desc: "Generate .env and .env.saas from examples if missing" + run: once + deps: [install] + cmds: + - npx tsx scripts/setup-env.ts --saas + sources: + - scripts/setup-env.ts + - config/.env.example + - config/.env.saas.example + generates: + - .env + - .env.saas + + prepare:env:desktop: + desc: "Generate .env and .env.desktop from examples if missing" + run: once + deps: [install] + cmds: + - npx tsx scripts/setup-env.ts --desktop + sources: + - scripts/setup-env.ts + - config/.env.example + - config/.env.desktop.example + generates: + - .env + - .env.desktop + + prepare:icons: + desc: "Generate icon bundle from source references" + run: once + deps: [install] + cmds: + - node scripts/generate-icons.js + + prepare: + desc: "Set up dev environment" + run: once + deps: [prepare:env, prepare:icons] + + prepare:saas: + desc: "Prepare for SaaS mode" + run: once + deps: [prepare:env:saas, prepare:icons] + + prepare:desktop: + desc: "Prepare for desktop mode" + run: once + deps: [prepare:env:desktop, prepare:icons] + + # ============================================================ + # Development + # ============================================================ + + dev: + desc: "Start frontend dev server" + deps: [prepare] + ignore_error: true + cmds: + - npx vite + + dev:core: + desc: "Start frontend dev server in core mode" + deps: [prepare] + ignore_error: true + cmds: + - npx vite --mode core + + dev:proprietary: + desc: "Start frontend dev server in proprietary mode" + deps: [prepare] + ignore_error: true + cmds: + - npx vite --mode proprietary + + dev:saas: + desc: "Start frontend dev server in SaaS mode" + deps: [prepare:saas] + ignore_error: true + cmds: + - npx vite --mode saas + + dev:desktop: + desc: "Start frontend dev server in desktop mode" + deps: [prepare:desktop] + ignore_error: true + cmds: + - npx vite --mode desktop + + dev:prototypes: + desc: "Start frontend dev server in prototypes mode" + deps: [prepare] + ignore_error: true + cmds: + - npx vite --mode prototypes + + # ============================================================ + # Build + # ============================================================ + + build: + desc: "Production build (default mode)" + deps: [prepare] + cmds: + - npx vite build + + build:core: + desc: "Build for core mode" + deps: [prepare] + cmds: + - npx vite build --mode core + + build:proprietary: + desc: "Build for proprietary mode" + deps: [prepare] + cmds: + - npx vite build --mode proprietary + + build:saas: + desc: "Build for SaaS mode" + deps: [prepare:saas] + cmds: + - npx vite build --mode saas + + build:desktop: + desc: "Build for desktop mode" + deps: [prepare:desktop] + cmds: + - npx vite build --mode desktop + + build:prototypes: + desc: "Build for prototypes mode" + deps: [prepare] + cmds: + - npx vite build --mode prototypes + + # ============================================================ + # Code quality + # ============================================================ + + lint: + desc: "Run linting" + deps: [install] + cmds: + - npx eslint --max-warnings=0 + - npx dpdm src --circular --no-warning --no-tree --exit-code circular:1 + + lint:fix: + desc: "Auto-fix lint issues" + deps: [install] + cmds: + - npx eslint --fix + + format: + desc: "Auto-fix code formatting" + deps: [install] + cmds: + - npx prettier --write . + + format:check: + desc: "Check code formatting" + deps: [install] + cmds: + - npx prettier --check . + + fix: + desc: "Auto-fix lint and format" + cmds: + - task: format + - task: lint:fix + + typecheck: + desc: "Typecheck default build of the app" + cmds: + - task: typecheck:proprietary + + typecheck:core: + desc: "Typecheck core build variant" + deps: [prepare] + cmds: + - npx tsc --noEmit --project src/core/tsconfig.json + + typecheck:proprietary: + desc: "Typecheck proprietary build variant" + deps: [prepare] + cmds: + - npx tsc --noEmit --project src/proprietary/tsconfig.json + + typecheck:saas: + desc: "Typecheck SaaS build variant" + deps: [prepare:saas] + cmds: + - npx tsc --noEmit --project src/saas/tsconfig.json + + typecheck:desktop: + desc: "Typecheck desktop build variant" + deps: [prepare:desktop] + cmds: + - npx tsc --noEmit --project src/desktop/tsconfig.json + + typecheck:scripts: + desc: "Typecheck scripts" + deps: [prepare] + cmds: + - npx tsc --noEmit --project scripts/tsconfig.json + + typecheck:prototypes: + desc: "Typecheck prototypes build variant" + deps: [prepare] + cmds: + - npx tsc --noEmit --project src/prototypes/tsconfig.json + + typecheck:all: + desc: "Typecheck all build variants" + cmds: + - task: typecheck:core + - task: typecheck:proprietary + - task: typecheck:saas + - task: typecheck:desktop + - task: typecheck:scripts + + # ============================================================ + # Quality Gate + # ============================================================ + + check: + desc: "Quick quality gate for local development" + cmds: + - task: typecheck + - task: lint + - task: format:check + - task: test + + check:all: + desc: "Full CI quality gate" + cmds: + - task: typecheck:all + - task: lint + - task: format:check + - task: build + - task: test + + # ============================================================ + # Test + # ============================================================ + + test: + desc: "Run tests" + deps: [install] + cmds: + - npx vitest run + + test:watch: + desc: "Run tests in watch mode" + deps: [install] + cmds: + - npx vitest --watch + + test:coverage: + desc: "Run tests with coverage" + deps: [install] + cmds: + - npx vitest --coverage + + test:e2e: + desc: "Run E2E tests" + deps: [prepare] + cmds: + - npx playwright test {{.CLI_ARGS}} + + test:e2e:install: + desc: "Install E2E test browsers" + deps: [install] + cmds: + - npx playwright install {{.CLI_ARGS}} --with-deps + + # ============================================================ + # Code Generation + # ============================================================ + + licenses:generate: + desc: "Generate frontend license report" + deps: [install] + cmds: + - node scripts/generate-licenses.js diff --git a/AGENTS.md b/AGENTS.md index 0e46552a21..6fe3cacc99 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,18 +2,42 @@ This file provides guidance to AI Agents when working with code in this repository. +## Taskfile (Recommended) + +This project uses [Task](https://taskfile.dev/) as a unified command runner. All build, dev, test, lint, and docker commands can be run from the repo root via `task `. Run `task --list` to see all available commands. + +### Quick Reference +- `task install` — install all dependencies +- `task dev` — start backend + frontend concurrently +- `task dev:all` — start backend + frontend + engine concurrently +- `task build` — build all components +- `task test` — run all tests (backend + frontend + engine) +- `task lint` — run all linters +- `task format` — auto-fix formatting across all components +- `task check` — full quality gate (lint + typecheck + test) +- `task clean` — clean all build artifacts +- `task docker:build` — build standard Docker image +- `task docker:up` — start Docker compose stack + ## Common Development Commands ### Build and Test -- **Build project**: `./gradlew clean build` -- **Run locally**: `./gradlew bootRun` -- **Full test suite**: `./test.sh` (builds all Docker variants and runs comprehensive tests) -- **Code formatting**: `./gradlew spotlessApply` (runs automatically before compilation) +- **Build project**: `task build` +- **Run backend locally**: `task backend:dev` +- **Run all tests**: `task test` (or individually: `task backend:test`, `task frontend:test`, `task engine:test`) +- **Docker integration tests**: `./test.sh` (builds all Docker variants and runs comprehensive tests) +- **Code formatting**: `task format` (or `task backend:format` for Java only) +- **Full quality gate**: `task check` (runs lint + typecheck + test across all components) + +After modifying any files in the project, you must run the relevant `task check` command that covers that area of the code. For example, when editing frontend files run `task frontend:check`; for Python engine files run `task engine:check`; for Java backend files run `task backend:check`. ### Docker Development -- **Build ultra-lite**: `docker build -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .` -- **Build standard**: `docker build -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .` -- **Build fat version**: `docker build -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .` +- **Build standard**: `task docker:build` (or `docker build -t stirling-pdf -f docker/embedded/Dockerfile .`) +- **Build fat version**: `task docker:build:fat` +- **Build ultra-lite**: `task docker:build:ultra-lite` +- **Start compose stack**: `task docker:up` (or `task docker:up:fat`, `task docker:up:ultra-lite`) +- **Stop compose stack**: `task docker:down` +- **View logs**: `task docker:logs` - **Example compose files**: Located in `exampleYmlFiles/` directory ### Security Mode Development @@ -23,20 +47,22 @@ Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security featur Development for the AI engine happens in the `engine/` folder. The frontend calls the Python via Java as a proxy. - Follow the engine-specific guidance in [engine/AGENTS.md](engine/AGENTS.md) for Python architecture, code style, and AI usage. -- Use Makefile commands for Python work: - - From `engine/`: `make check` to lint, type-check, test, etc. and `make fix` to fix easily fixable linting and formatting issues. -- The project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed there, followed by running `make install`. +- Use Task commands from the repo root: + - `task engine:check` — lint, type-check, test + - `task engine:fix` — auto-fix linting and formatting + - `task engine:install` — install dependencies +- The project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed there, followed by running `task engine:install`. ### Frontend Development -- **Frontend dev server**: `cd frontend && npm run dev` (requires backend on localhost:8080) +- **Frontend dev server**: `task frontend:dev` — requires backend on localhost:8080 - **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS - **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080) - **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines -- **Package Installation**: DO NOT run npm install commands - package management handled separately +- **Package Installation**: `task frontend:install` - **Deployment Options**: - - **Desktop App**: `npm run tauri-build` (native desktop application) - - **Web Server**: `npm run build` then serve dist/ folder - - **Development**: `npm run tauri-dev` for desktop dev mode + - **Desktop App**: `task desktop:build` + - **Web Server**: `task frontend:build` then serve dist/ folder + - **Development**: `task desktop:dev` for desktop dev mode #### Environment Variables - All `VITE_*` variables must be declared in the appropriate example file: @@ -44,8 +70,8 @@ Development for the AI engine happens in the `engine/` folder. The frontend call - `frontend/config/.env.saas.example` — SaaS-only vars - `frontend/config/.env.desktop.example` — desktop (Tauri)-only vars - Never use `|| 'hardcoded-fallback'` inline — put defaults in the example files -- `npm run prep` / `prep:saas` / `prep:desktop` auto-create the env files from examples on first run, and error if any required keys are missing -- These prep scripts run automatically at the start of all `dev*`, `build*`, and `tauri*` commands +- `task frontend:prepare` / `prepare:saas` / `prepare:desktop` auto-create the env files from examples on first run, and error if any required keys are missing +- Prepare runs automatically as a dependency of all `dev*`, `build*`, and `desktop*` tasks - See `frontend/README.md#environment-variables` for full documentation #### Import Paths - CRITICAL @@ -299,15 +325,17 @@ The frontend is organized with a clear separation of concerns: ## Development Workflow -1. **Local Development**: - - Backend: `./gradlew bootRun` (runs on localhost:8080) - - Frontend: `cd frontend && npm run dev` (runs on localhost:5173, proxies to backend) -2. **Docker Testing**: Use `./test.sh` before submitting PRs -3. **Code Style**: Spotless enforces Google Java Format automatically -4. **Translations**: +1. **Local Development** (using Taskfile): + - Backend + frontend: `task dev` + - All services (including AI engine): `task dev:all` + - Or individually: `task backend:dev` (localhost:8080), `task frontend:dev` (localhost:5173), `task engine:dev` (localhost:5001) +2. **Quality Gate**: Run `task check` before submitting PRs +3. **Docker Testing**: Use `./test.sh` for full Docker integration tests +4. **Code Style**: Spotless enforces Google Java Format automatically (`task backend:format`) +5. **Translations**: - Backend: Use helper scripts in `/scripts` for multi-language updates - Frontend: Update JSON files in `frontend/public/locales/` or use conversion script -5. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html` +6. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html` ## Frontend Architecture Status diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d9828c4096..79eb43f645 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,19 @@ Before you start working on an issue, please comment on (or create) the issue an Once you have been assigned an issue, you can start working on it. When you are ready to submit your changes, open a pull request. For a detailed pull request tutorial, see [this guide](https://www.digitalocean.com/community/tutorials/how-to-create-a-pull-request-on-github). +## Development Quick Start + +This project uses [Task](https://taskfile.dev/) as a unified command runner. After cloning: + +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 +4. Run `task check` before submitting a PR + +Run `task --list` to see all available commands. + +## Pull Request Guidelines + Please make sure your Pull Request adheres to the following guidelines: - Use the PR template provided. @@ -39,9 +52,10 @@ If, at any point in time, you have a question, please feel free to ask in the sa ## Developer Documentation -For technical guides, setup instructions, and development resources, please see our [Developer Documentation](devGuide/) which includes: +For technical guides, setup instructions, and development resources: -- [Developer Guide](devGuide/DeveloperGuide.md) - Main setup and architecture guide +- [Developer Guide](DeveloperGuide.md) - Main setup and architecture guide +- [Taskfile.yml](Taskfile.yml) - Unified task runner for all build/dev/test/lint commands - [Exception Handling Guide](devGuide/EXCEPTION_HANDLING_GUIDE.md) - Error handling patterns and i18n - [Translation Guide](devGuide/HowToAddNewLanguage.md) - Adding new languages - And more in the [devGuide folder](devGuide/) diff --git a/DeveloperGuide.md b/DeveloperGuide.md index d67429437a..fdfab20c5e 100644 --- a/DeveloperGuide.md +++ b/DeveloperGuide.md @@ -42,11 +42,13 @@ This guide focuses on developing for Stirling 2.0, including both the React fron ### Prerequisites +- [Task](https://taskfile.dev/installation/) — unified command runner (recommended) - Docker - Git - Java JDK 21 or later (JDK 25 recommended) - Node.js 18+ and npm (required for frontend development) - Gradle 7.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`) @@ -82,13 +84,29 @@ For local testing, you should generally be testing the full 'Security' version o 5. **Frontend Setup (Required for Stirling 2.0)** Navigate to the frontend directory and install dependencies using npm. +### Verify Setup + +Run `task install` to install all project dependencies (frontend npm packages, engine Python packages). Gradle manages its own dependencies automatically. Then run `task check` to verify everything builds and passes. + ## 4. Stirling 2.0 Development Workflow +### Using Taskfile (Recommended) + +The fastest way to start developing: + +1. **Start developing**: `task dev` (runs backend + frontend concurrently — Ctrl+C to stop) +2. **Or start services individually** in separate terminals: + - `task backend:dev` — Spring Boot on localhost:8080 + - `task frontend:dev` — Vite on localhost:5173 + - `task engine:dev` — FastAPI on localhost:5001 + +Run `task --list` to see all available commands. + ### Frontend Development (React) The frontend is a React SPA that runs independently during development: -1. **Start the backend**: Run the Spring Boot application (serves API endpoints on localhost:8080) -2. **Start the frontend dev server**: Navigate to the frontend directory and run the development server (serves UI on localhost:5173) +1. **Start the backend**: `task backend:dev` (serves API endpoints on localhost:8080) +2. **Start the frontend dev server**: `task frontend:dev` (serves UI on localhost:5173) 3. **Development flow**: The Vite dev server automatically proxies API calls to the backend ### File Storage Architecture @@ -99,7 +117,10 @@ Stirling 2.0 uses client-side file storage: ### Tauri Desktop App Development Stirling-PDF can be packaged as a cross-platform desktop application using Tauri with PDF file association support and bundled JRE. -See [the frontend README](frontend/README.md#tauri) for build instructions. + +Using Taskfile: `task desktop:dev` (development) or `task desktop:build` (production build). + +See [the frontend README](frontend/README.md#tauri) for detailed build instructions. ## 5. Project Structure @@ -187,7 +208,7 @@ services: limits: memory: 4G healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"] + test: ["CMD-SHELL", "curl -f http://localhost:8080$${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"] interval: 5s timeout: 10s retries: 16 @@ -222,6 +243,20 @@ docker-compose -f exampleYmlFiles/docker-compose-latest-security.yml up ### Building Docker Images +#### Using Taskfile (Recommended) + +```bash +task docker:build # standard image +task docker:build:fat # fat image (all features) +task docker:build:ultra-lite # ultra-lite image +task docker:up # start standard compose stack +task docker:up:fat # start fat compose stack +task docker:down # stop all stacks +task docker:logs # tail logs +``` + +#### Manual Docker Builds + Stirling-PDF uses different Docker images for various configurations. The build process is controlled by environment variables and uses specific Dockerfile variants. Here's how to build the Docker images: 1. Set the security environment variable: @@ -230,10 +265,10 @@ Stirling-PDF uses different Docker images for various configurations. The build export DISABLE_ADDITIONAL_FEATURES=true # or false for to enable login and security features for builds ``` -2. Build the project with Gradle: +2. Build the project: ```bash - ./gradlew clean build + task backend:build ``` 3. Build the Docker images: @@ -261,9 +296,18 @@ Note: The `--no-cache` and `--pull` flags ensure that the build process uses the ## 7. Testing +### Quick Testing with Taskfile + +Run all unit/integration tests across all components: + +```bash +task test # run all tests (backend + frontend + engine) +task check # full quality gate: lint + typecheck + test +``` + ### Comprehensive Testing Script -Stirling-PDF provides a `test.sh` script in the root directory. This script builds all versions of Stirling-PDF, checks that each version works, and runs Cucumber tests. It's recommended to run this script before submitting a final pull request. +Stirling-PDF also provides a `test.sh` script in the root directory for Docker integration tests. This script builds all versions of Stirling-PDF, checks that each version works, and runs Cucumber tests. It's recommended to run this script before submitting a final pull request. To run the test script: @@ -289,10 +333,11 @@ Note: The `test.sh` script will run automatically when you raise a PR. However, For React frontend development: -1. Start the backend: Run the Spring Boot application to serve API endpoints on localhost:8080 -2. Start the frontend dev server: Navigate to the frontend directory and run the development server on localhost:5173 +1. Start the backend: `task backend:dev` (serves API endpoints on localhost:8080) +2. Start the frontend dev server: `task frontend:dev` (serves UI on localhost:5173) 3. The Vite dev server automatically proxies API calls to the backend -4. Test React components, UI interactions, and IndexedDB file operations using browser developer tools +4. Run frontend tests: `task frontend:test` (or `task frontend:test:watch` for watch mode) +5. Test React components, UI interactions, and IndexedDB file operations using browser developer tools ### Local Testing (Java and UI Components) @@ -308,7 +353,7 @@ To run Stirling-PDF locally: 1. Compile and run the project using built-in IDE methods or by running: ```bash - ./gradlew bootRun + task backend:dev ``` 2. Access the application at `http://localhost:8080` in your web browser. @@ -329,10 +374,11 @@ Important notes: 2. Create a new branch for your feature or bug fix. 3. Make your changes and commit them with clear, descriptive messages and ensure any documentation is updated related to your changes. 4. Test your changes thoroughly in the Docker environment. -5. Run the `test.sh` script to ensure all versions build correctly and pass the Cucumber tests: +5. Run the quality gate and integration tests: ```bash - ./test.sh + task check # lint + typecheck + test across all components + ./test.sh # Docker integration tests (builds all variants + Cucumber) ``` 6. Push your changes to your fork. diff --git a/LICENSE b/LICENSE index e7a8034e41..ea74278a4e 100644 --- a/LICENSE +++ b/LICENSE @@ -14,6 +14,8 @@ if that directory exists, is licensed under the license defined in "frontend/src if that directory exists, is licensed under the license defined in "frontend/src/desktop/LICENSE". * All content that resides under the "frontend/src/saas/" directory of this repository, if that directory exists, is licensed under the license defined in "frontend/src/saas/LICENSE". +* All content that resides under the "frontend/src/prototypes/" directory of this repository, +if that directory exists, is licensed under the license defined in "frontend/src/prototypes/LICENSE". * Content outside of the above mentioned directories or restrictions above is available under the MIT License as defined below. diff --git a/README.md b/README.md index d9e0d5a418..9329b20eed 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ For full installation options (including desktop and Kubernetes), see our [Docum We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. -For development setup, see the [Developer Guide](DeveloperGuide.md). +This project uses [Task](https://taskfile.dev/) as a unified command runner for all build, dev, and test commands. Run `task install` to get started, or see the [Developer Guide](DeveloperGuide.md) for full details. For adding translations, see the [Translation Guide](devGuide/HowToAddNewLanguage.md). diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000000..4840a82697 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,128 @@ +version: '3' + +output: prefixed + +includes: + backend: + taskfile: .taskfiles/backend.yml + dir: . + frontend: + taskfile: .taskfiles/frontend.yml + dir: frontend + engine: + taskfile: .taskfiles/engine.yml + dir: engine + docker: + taskfile: .taskfiles/docker.yml + dir: . + desktop: + taskfile: .taskfiles/desktop.yml + dir: frontend + +tasks: + # ============================================================ + # Setup & Prerequisites + # ============================================================ + + install: + desc: "Install all project dependencies" + cmds: + - task: frontend:install + - task: engine:install + + # ============================================================ + # Development + # ============================================================ + + dev: + desc: "Start backend + frontend concurrently" + deps: + - backend:dev + - frontend:dev + + dev:all: + desc: "Start backend + frontend + engine concurrently" + deps: + - backend:dev + - frontend:dev:prototypes + - engine:dev + + # ============================================================ + # Build + # ============================================================ + + build: + desc: "Build all components" + cmds: + - task: backend:build + - task: frontend:build + + # ============================================================ + # Test + # ============================================================ + + test: + desc: "Run ALL tests (backend + frontend + engine)" + cmds: + - task: backend:test + - task: frontend:test + - task: engine:test + + # ============================================================ + # Lint & Format + # ============================================================ + + lint: + desc: "Run all linters" + cmds: + - task: frontend:lint + - task: engine:lint + + fix: + desc: "Auto-fix all components" + cmds: + - task: backend:fix + - task: frontend:fix + - task: engine:fix + + format: + desc: "Auto-fix formatting across all components" + cmds: + - task: backend:format + - task: frontend:format + - task: engine:format + + format:check: + desc: "Check formatting across all components" + cmds: + - task: backend:format:check + - task: frontend:format:check + - task: engine:format:check + + # ============================================================ + # Quality Gate + # ============================================================ + + check: + desc: "Quick quality gate for local development" + cmds: + - task: backend:check + - task: frontend:check + - task: engine:check + + check:all: + desc: "Full CI quality gate" + cmds: + - task: backend:check + - task: frontend:check:all + - task: engine:check + + # ============================================================ + # Clean + # ============================================================ + + clean: + desc: "Clean all build artifacts" + cmds: + - task: backend:clean + - task: engine:clean diff --git a/app/common/build.gradle b/app/common/build.gradle index e19b36d466..41d17273f5 100644 --- a/app/common/build.gradle +++ b/app/common/build.gradle @@ -7,6 +7,8 @@ spotless { target 'src/**/java/**/*.java' targetExclude 'src/main/java/org/apache/**' googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false) + // google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25 + suppressLintsFor { setStep('google-java-format') } importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling") trimTrailingWhitespace() @@ -27,10 +29,10 @@ spotless { } } dependencies { - api 'com.google.guava:guava:33.4.8-jre' + api 'com.google.guava:guava:33.5.0-jre' api 'org.springframework.boot:spring-boot-starter-webmvc' api 'org.springframework.boot:spring-boot-starter-aspectj' - api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20260102.1' + api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20260313.1' api 'com.fathzer:javaluator:3.0.6' api 'com.posthog.java:posthog:1.2.0' api 'org.apache.commons:commons-lang3:3.20.0' @@ -43,7 +45,7 @@ dependencies { api 'com.github.junrar:junrar:7.5.8' // 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.1" + api "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.2" // 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 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 fa36998e7f..dba7deca22 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 @@ -75,6 +75,7 @@ public class ApplicationProperties { private AutoPipeline autoPipeline = new AutoPipeline(); private ProcessExecutor processExecutor = new ProcessExecutor(); private PdfEditor pdfEditor = new PdfEditor(); + private AiEngine aiEngine = new AiEngine(); @Bean public PropertySource dynamicYamlPropertySource(ConfigurableEnvironment environment) @@ -231,6 +232,13 @@ public class ApplicationProperties { } } + @Data + public static class AiEngine { + private boolean enabled = false; + private String url = "http://localhost:5001"; + private int timeoutSeconds = 120; + } + @Data public static class Legal { private String termsAndConditions; 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 46b4a57082..8b0f3a53a2 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 @@ -1,8 +1,10 @@ package stirling.software.common.service; import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.UUID; @@ -10,6 +12,7 @@ import java.util.UUID; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -143,6 +146,24 @@ public class FileStorage { return new StoredFile(fileId, size); } + public String storeFromStreamingBody(StreamingResponseBody body, String originalName) + throws IOException { + String fileId = generateFileId(); + Path filePath = getFilePath(fileId); + Files.createDirectories(filePath.getParent()); + boolean success = false; + try (OutputStream os = new BufferedOutputStream(Files.newOutputStream(filePath))) { + body.writeTo(os); + success = true; + } finally { + if (!success) { + Files.deleteIfExists(filePath); + } + } + log.debug("Stored StreamingResponseBody with ID: {}", fileId); + return fileId; + } + /** * Delete a file by its ID * 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 new file mode 100644 index 0000000000..ba53e7fdef --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/service/InternalApiClient.java @@ -0,0 +1,184 @@ +package stirling.software.common.service; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.regex.Pattern; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RequestCallback; +import org.springframework.web.client.RestTemplate; + +import jakarta.servlet.ServletContext; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.model.enumeration.Role; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; + +/** + * Dispatches HTTP POST requests to internal Stirling API endpoints via loopback. Used by + * PipelineProcessor and AiWorkflowService to execute tool operations programmatically without + * leaving the JVM network stack. + */ +@Service +@Slf4j +public class InternalApiClient { + + // Allowlist for internal dispatch. Matches a fixed namespace prefix, + // but rejects traversal (..), URL-encoding (%), query/fragment, backslashes, and any other + // character that could alter the resolved endpoint on the local Spring server. + private static final Pattern ALLOWED_ENDPOINT_PATH = + Pattern.compile("^/api/v1/(general|misc|security|convert|filter)(/[A-Za-z0-9_-]+)+$"); + + private final ServletContext servletContext; + private final UserServiceInterface userService; + private final TempFileManager tempFileManager; + private final Environment environment; + + public InternalApiClient( + ServletContext servletContext, + @Autowired(required = false) UserServiceInterface userService, + TempFileManager tempFileManager, + Environment environment) { + this.servletContext = servletContext; + this.userService = userService; + this.tempFileManager = tempFileManager; + this.environment = environment; + } + + /** + * POST to an internal API endpoint. The endpointPath must start with one of the allowed + * prefixes (e.g. {@code /api/v1/misc/compress-pdf}). + * + * @param endpointPath API path (e.g. {@code /api/v1/general/rotate-pdf}) + * @param body multipart form body (fileInput + parameters) + * @return response with the result file as a {@link TempFileResource} body + */ + public ResponseEntity post(String endpointPath, MultiValueMap body) { + validateUrl(endpointPath); + String url = getBaseUrl() + endpointPath; + + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + String apiKey = getApiKeyForUser(); + if (apiKey != null && !apiKey.isEmpty()) { + headers.add("X-API-KEY", apiKey); + } + + HttpEntity> entity = new HttpEntity<>(body, headers); + RequestCallback requestCallback = restTemplate.httpEntityCallback(entity, Resource.class); + + return restTemplate.execute( + url, + HttpMethod.POST, + requestCallback, + response -> { + try { + TempFile tempFile = tempFileManager.createManagedTempFile("internal-api"); + Files.copy( + response.getBody(), + tempFile.getPath(), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + String filename = extractFilename(response.getHeaders()); + TempFileResource resource = new TempFileResource(tempFile, filename); + return ResponseEntity.status(response.getStatusCode()) + .headers(response.getHeaders()) + .body(resource); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + + /** + * Extract the filename from a response's {@code Content-Disposition} header. Returns {@code + * null} if the header is missing or has no filename. + */ + private static String extractFilename(HttpHeaders headers) { + String contentDisposition = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION); + if (contentDisposition == null || contentDisposition.isBlank()) { + return null; + } + for (String part : contentDisposition.split(";")) { + String trimmed = part.trim(); + if (trimmed.startsWith("filename")) { + String[] kv = trimmed.split("=", 2); + if (kv.length != 2) { + continue; + } + String value = kv[1].trim().replace("\"", ""); + return URLDecoder.decode(value, StandardCharsets.UTF_8); + } + } + return null; + } + + private String getBaseUrl() { + // Resolve the port lazily so desktop mode (server.port=0, OS-assigned) dispatches to the + // actual bound port. Spring publishes local.server.port once the web server is up; fall + // back to the configured server.port for early calls (tests, non-web contexts). + String port = environment.getProperty("local.server.port"); + if (port == null) { + port = environment.getProperty("server.port", "8080"); + } + return "http://localhost:" + port + servletContext.getContextPath(); + } + + private String getApiKeyForUser() { + if (userService == null) return ""; + String username = userService.getCurrentUsername(); + if (username != null && !username.equals("anonymousUser")) { + return userService.getApiKeyForUser(username); + } + return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); + } + + private void validateUrl(String endpointPath) { + if (endpointPath == null || !ALLOWED_ENDPOINT_PATH.matcher(endpointPath).matches()) { + log.warn("Blocked internal API request to disallowed path: {}", endpointPath); + throw new SecurityException( + "Internal API dispatch not permitted for endpoint: " + endpointPath); + } + } + + /** + * A {@link FileSystemResource} that holds a reference to its backing {@link TempFile}. + * + *

If a display filename is supplied (typically parsed from the upstream response's {@code + * Content-Disposition} header), it is returned from {@link #getFilename()} instead of the + * underlying temp file's path-based name. + */ + public static class TempFileResource extends FileSystemResource { + private final TempFile tempFile; + private final String displayFilename; + + public TempFileResource(TempFile tempFile) { + this(tempFile, null); + } + + public TempFileResource(TempFile tempFile, String displayFilename) { + super(tempFile.getFile()); + this.tempFile = tempFile; + this.displayFilename = displayFilename; + } + + public TempFile getTempFile() { + return tempFile; + } + + @Override + public String getFilename() { + return displayFilename != null ? displayFilename : super.getFilename(); + } + } +} 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 459e77c0fa..dd53eef3c1 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 @@ -16,6 +16,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import jakarta.servlet.http.HttpServletRequest; @@ -305,33 +306,21 @@ public class JobExecutorService { Object body = response.getBody(); if (body instanceof byte[]) { - // Extract filename from content-disposition header if available - String filename = "result.pdf"; - String contentType = MediaType.APPLICATION_PDF_VALUE; + String filename = extractResponseFilename(response); + String contentType = extractResponseContentType(response); - if (response.getHeaders().getContentDisposition() != null) { - String disposition = - response.getHeaders().getContentDisposition().toString(); - if (disposition.contains("filename=")) { - filename = - disposition.substring( - disposition.indexOf("filename=") + 9, - disposition.lastIndexOf('"')); - } - } - - MediaType mediaType = response.getHeaders().getContentType(); - - if (mediaType != null) { - contentType = mediaType.toString(); - } - - // Store byte array directly to disk String fileId = fileStorage.storeBytes((byte[]) body, filename); taskManager.setFileResult(jobId, fileId, filename, contentType); log.debug("Stored ResponseEntity result with fileId: {}", fileId); + } else if (body instanceof StreamingResponseBody streamingBody) { + String filename = extractResponseFilename(response); + String contentType = extractResponseContentType(response); - // Let the GC handle the memory naturally + String fileId = fileStorage.storeFromStreamingBody(streamingBody, filename); + taskManager.setFileResult(jobId, fileId, filename, contentType); + log.debug( + "Stored ResponseEntity result with fileId: {}", + fileId); } else { // Check if the response body contains a fileId if (body != null && body.toString().contains("fileId")) { @@ -481,6 +470,21 @@ public class JobExecutorService { } } + private static String extractResponseFilename(ResponseEntity response) { + if (response.getHeaders().getContentDisposition() != null) { + String filename = response.getHeaders().getContentDisposition().getFilename(); + if (filename != null && !filename.isEmpty()) { + return filename; + } + } + return "result.pdf"; + } + + private static String extractResponseContentType(ResponseEntity response) { + MediaType mediaType = response.getHeaders().getContentType(); + return mediaType != null ? mediaType.toString() : MediaType.APPLICATION_PDF_VALUE; + } + /** * Parse session timeout string (e.g., "30m", "1h") to milliseconds * diff --git a/app/common/src/main/java/stirling/software/common/service/JobQueue.java b/app/common/src/main/java/stirling/software/common/service/JobQueue.java index 595cc6e395..28d94baced 100644 --- a/app/common/src/main/java/stirling/software/common/service/JobQueue.java +++ b/app/common/src/main/java/stirling/software/common/service/JobQueue.java @@ -401,7 +401,7 @@ public class JobQueue implements SmartLifecycle { * @throws Exception If there is an execution error */ private T executeWithTimeout(Supplier supplier, long timeoutMs) throws Exception { - CompletableFuture future = CompletableFuture.supplyAsync(supplier); + CompletableFuture future = CompletableFuture.supplyAsync(supplier, jobExecutor); try { if (timeoutMs <= 0) { diff --git a/app/common/src/main/java/stirling/software/common/service/PostHogService.java b/app/common/src/main/java/stirling/software/common/service/PostHogService.java index 1ec3e87946..92093762fc 100644 --- a/app/common/src/main/java/stirling/software/common/service/PostHogService.java +++ b/app/common/src/main/java/stirling/software/common/service/PostHogService.java @@ -7,11 +7,8 @@ import java.lang.management.MemoryMXBean; import java.lang.management.OperatingSystemMXBean; import java.lang.management.RuntimeMXBean; import java.lang.management.ThreadMXBean; -import java.net.InetAddress; -import java.net.NetworkInterface; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.Enumeration; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -94,21 +91,12 @@ public class PostHogService { metrics.put("os_name", System.getProperty("os.name")); metrics.put("os_version", System.getProperty("os.version")); metrics.put("java_version", System.getProperty("java.version")); - metrics.put("user_name", System.getProperty("user.name")); - metrics.put("user_home", System.getProperty("user.home")); - metrics.put("user_dir", System.getProperty("user.dir")); // CPU and Memory metrics.put("cpu_cores", Runtime.getRuntime().availableProcessors()); metrics.put("total_memory", Runtime.getRuntime().totalMemory()); metrics.put("free_memory", Runtime.getRuntime().freeMemory()); - // Network and Server Identity - InetAddress localHost = InetAddress.getLocalHost(); - metrics.put("ip_address", localHost.getHostAddress()); - metrics.put("hostname", localHost.getHostName()); - metrics.put("mac_address", getMacAddress()); - // JVM info metrics.put("jvm_vendor", System.getProperty("java.vendor")); metrics.put("jvm_version", System.getProperty("java.vm.version")); @@ -153,9 +141,6 @@ public class PostHogService { metrics.put("gc_" + gcBean.getName() + "_time", gcBean.getCollectionTime()); } - // Network interfaces - metrics.put("network_interfaces", getNetworkInterfacesInfo()); - // Docker detection and stats boolean isDocker = isRunningInDocker(); if (isDocker) { @@ -353,30 +338,6 @@ public class PostHogService { .getProFeatures() .getCustomMetadata() .isAutoUpdateMetadata()); - addIfNotEmpty( - properties, - "enterpriseEdition_customMetadata_author", - applicationProperties - .getPremium() - .getProFeatures() - .getCustomMetadata() - .getAuthor()); - addIfNotEmpty( - properties, - "enterpriseEdition_customMetadata_creator", - applicationProperties - .getPremium() - .getProFeatures() - .getCustomMetadata() - .getCreator()); - addIfNotEmpty( - properties, - "enterpriseEdition_customMetadata_producer", - applicationProperties - .getPremium() - .getProFeatures() - .getCustomMetadata() - .getProducer()); } // Capture AutoPipeline properties addIfNotEmpty( @@ -386,39 +347,4 @@ public class PostHogService { return properties; } - - private String getMacAddress() { - try { - Enumeration networkInterfaces = - NetworkInterface.getNetworkInterfaces(); - while (networkInterfaces.hasMoreElements()) { - NetworkInterface ni = networkInterfaces.nextElement(); - byte[] hardwareAddress = ni.getHardwareAddress(); - if (hardwareAddress != null) { - String[] hexadecimal = new String[hardwareAddress.length]; - for (int i = 0; i < hardwareAddress.length; i++) { - hexadecimal[i] = String.format("%02X", hardwareAddress[i]); - } - return String.join("-", hexadecimal); - } - } - } catch (Exception e) { - // Handle exception - } - return "Unknown"; - } - - private Map getNetworkInterfacesInfo() { - Map interfacesInfo = new HashMap<>(); - try { - Enumeration nets = NetworkInterface.getNetworkInterfaces(); - while (nets.hasMoreElements()) { - NetworkInterface netint = nets.nextElement(); - interfacesInfo.put(netint.getName(), netint.getDisplayName()); - } - } catch (Exception e) { - interfacesInfo.put("error", e.getMessage()); - } - return interfacesInfo; - } } 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 new file mode 100644 index 0000000000..662878b741 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/service/ToolMetadataService.java @@ -0,0 +1,18 @@ +package stirling.software.common.service; + +/** Provides metadata about tool endpoints for internal dispatch. */ +public interface ToolMetadataService { + + /** Returns true if the given operation path accepts multiple input files. */ + boolean isMultiInput(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 false for a bare {@code Output:ZIP} (e.g. {@code get-attachments}), where the + * archive itself is the deliverable and should be kept packed. + */ + boolean shouldUnpackZipResponse(String operationPath); +} diff --git a/app/common/src/main/java/stirling/software/common/service/UserServiceInterface.java b/app/common/src/main/java/stirling/software/common/service/UserServiceInterface.java index 074f422000..9649696567 100644 --- a/app/common/src/main/java/stirling/software/common/service/UserServiceInterface.java +++ b/app/common/src/main/java/stirling/software/common/service/UserServiceInterface.java @@ -5,6 +5,8 @@ public interface UserServiceInterface { String getCurrentUsername(); + String getCurrentUserApiKey(); + long getTotalUsersCount(); boolean isCurrentUserAdmin(); diff --git a/app/common/src/main/java/stirling/software/common/util/FileMonitor.java b/app/common/src/main/java/stirling/software/common/util/FileMonitor.java index 09caccdb50..dc0362b356 100644 --- a/app/common/src/main/java/stirling/software/common/util/FileMonitor.java +++ b/app/common/src/main/java/stirling/software/common/util/FileMonitor.java @@ -112,8 +112,6 @@ public class FileMonitor { All files observed changes in the last iteration will be considered as staging files. If those files are not modified in current iteration, they will be considered as ready for processing. */ - stagingFiles = new HashSet<>(newlyDiscoveredFiles); - readyForProcessingFiles.clear(); if (path2KeyMapping.isEmpty()) { log.warn("Not monitoring any directories; attempting to re-register root paths."); @@ -129,8 +127,19 @@ public class FileMonitor { } } - WatchKey key; - while ((key = watchService.poll()) != null) { + // Skip expensive collection work when there is nothing to track + WatchKey firstKey = watchService.poll(); + if (firstKey == null + && newlyDiscoveredFiles.isEmpty() + && readyForProcessingFiles.isEmpty()) { + return; + } + + stagingFiles = new HashSet<>(newlyDiscoveredFiles); + readyForProcessingFiles.clear(); + + WatchKey key = firstKey; + while (key != null) { final Path watchingDir = (Path) key.watchable(); key.pollEvents() .forEach( @@ -167,6 +176,7 @@ public class FileMonitor { if (!isKeyValid) { // key is invalid when the directory itself is no longer exists path2KeyMapping.remove((Path) key.watchable()); } + key = watchService.poll(); } readyForProcessingFiles.addAll(stagingFiles); } diff --git a/app/common/src/main/java/stirling/software/common/util/FileToPdf.java b/app/common/src/main/java/stirling/software/common/util/FileToPdf.java index 3f8c3aa5c3..83bc0e35bc 100644 --- a/app/common/src/main/java/stirling/software/common/util/FileToPdf.java +++ b/app/common/src/main/java/stirling/software/common/util/FileToPdf.java @@ -1,6 +1,5 @@ package stirling.software.common.util; -import java.io.ByteArrayInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.UncheckedIOException; @@ -66,16 +65,7 @@ public class FileToPdf { ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) .runCommandWithOutputHandling(command); - byte[] pdfBytes = Files.readAllBytes(tempOutputFile.getPath()); - try { - return pdfBytes; - } catch (Exception e) { - pdfBytes = Files.readAllBytes(tempOutputFile.getPath()); - if (pdfBytes.length < 1) { - throw e; - } - return pdfBytes; - } + return Files.readAllBytes(tempOutputFile.getPath()); } // tempInputFile auto-closed } // tempOutputFile auto-closed } @@ -92,8 +82,7 @@ public class FileToPdf { throws IOException { try (TempDirectory tempUnzippedDir = new TempDirectory(tempFileManager)) { try (ZipInputStream zipIn = - ZipSecurity.createHardenedInputStream( - new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) { + ZipSecurity.createHardenedInputStream(Files.newInputStream(zipFilePath))) { ZipEntry entry = zipIn.getNextEntry(); while (entry != null) { Path filePath = 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 c3b009efa7..2ae5ace9e9 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 @@ -629,11 +629,12 @@ public class FormUtils { } log.debug("Skipping form fill because document has no AcroForm"); if (flatten) { - flattenEntireDocument(document, null); + flattenEntireDocument(document, null, false); } return; } + boolean valuesApplied = false; if (values != null && !values.isEmpty()) { acroForm.setCacheFields(true); @@ -667,18 +668,26 @@ public class FormUtils { Object rawValue = entry.getValue(); String value = rawValue == null ? null : Objects.toString(rawValue, null); applyValueToField(field, value, strict); + valuesApplied = true; } - ensureAppearances(acroForm); + if (valuesApplied) { + ensureAppearances(acroForm); + } } repairWidgetGeometry(document, acroForm); if (flatten) { - flattenEntireDocument(document, acroForm); + flattenEntireDocument(document, acroForm, valuesApplied); } } + // Cap the fallback rendering DPI. This path only runs when acroForm.flatten() + // throws, and the goal is a readable flattened document — not print quality — + // so clamping avoids runaway memory/CPU on pathological inputs. + private static final int FLATTEN_FALLBACK_MAX_DPI = 200; + private void flattenViaRendering(PDDocument document, PDAcroForm acroForm) throws IOException { if (document == null) { return; @@ -704,28 +713,34 @@ public class FormUtils { properties != null && properties.getSystem() != null ? properties.getSystem().getMaxDPI() : 300; + int effectiveDpi = Math.min(requestedDpi, FLATTEN_FALLBACK_MAX_DPI); - rebuildDocumentFromImages(document, renderer, requestedDpi); + rebuildDocumentFromImages(document, renderer, effectiveDpi); } - // note: this implementation suffers from: - // https://issues.apache.org/jira/browse/PDFBOX-5962 - private void flattenEntireDocument(PDDocument document, PDAcroForm acroForm) - throws IOException { - if (document == null) { + // Use PDFBox's built-in field flattening which bakes form field values + // into the page content stream as static text/graphics, removing the + // interactive form structure but preserving all other document content + // (images, text, annotations, etc.) at full quality. + // + // Forcing appearance regeneration via setNeedAppearances(true) drives + // PDFBox into refreshAppearances inside flatten(), where it can hang on + // certain documents (PDFBOX-5962). We therefore only regenerate when we + // actually wrote new values, or when some widgets are missing appearance + // streams and would otherwise flatten blank. + private void flattenEntireDocument( + PDDocument document, PDAcroForm acroForm, boolean valuesWritten) throws IOException { + if (document == null || acroForm == null) { return; } - if (acroForm == null) { - return; - } - - // Use PDFBox's built-in field flattening which bakes form field values - // into the page content stream as static text/graphics, removing the - // interactive form structure but preserving all other document content - // (images, text, annotations, etc.) at full quality. - try { + if (valuesWritten || hasWidgetWithoutAppearance(acroForm)) { ensureAppearances(acroForm); + } else { + acroForm.setNeedAppearances(false); + } + + try { acroForm.flatten(); } catch (Exception e) { log.warn( @@ -736,6 +751,28 @@ public class FormUtils { } } + private boolean hasWidgetWithoutAppearance(PDAcroForm acroForm) { + for (PDField field : acroForm.getFieldTree()) { + if (!(field instanceof PDTerminalField terminalField)) { + continue; + } + List widgets = terminalField.getWidgets(); + if (widgets == null) { + continue; + } + for (PDAnnotationWidget widget : widgets) { + if (widget == null) { + continue; + } + PDAppearanceDictionary appearance = widget.getAppearance(); + if (appearance == null || appearance.getNormalAppearance() == null) { + return true; + } + } + } + return false; + } + private void rebuildDocumentFromImages(PDDocument document, PDFRenderer renderer, int dpi) throws IOException { int pageCount = document.getNumberOfPages(); 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 a0086ea346..61ba8670fd 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 @@ -1183,4 +1183,25 @@ public class GeneralUtils { } } } + + public String getLocalNetworkIp() { + try { + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + if (interfaces == null) return null; + while (interfaces.hasMoreElements()) { + NetworkInterface iface = interfaces.nextElement(); + if (!iface.isUp() || iface.isLoopback() || iface.isVirtual()) continue; + Enumeration addresses = iface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = addresses.nextElement(); + if (addr instanceof Inet4Address && addr.isSiteLocalAddress()) { + return addr.getHostAddress(); + } + } + } + } catch (Exception e) { + log.warn("Failed to detect local network IP", e); + } + return null; + } } 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 680eb50d62..1300e165ff 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 @@ -1,9 +1,9 @@ package stirling.software.common.util; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -20,6 +20,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter; import com.vladsch.flexmark.util.data.MutableDataSet; @@ -48,7 +49,7 @@ public class PDFToFile { this.runtimePathConfig = runtimePathConfig; } - public ResponseEntity processPdfToMarkdown(MultipartFile inputFile) + public ResponseEntity processPdfToMarkdown(MultipartFile inputFile) throws IOException, InterruptedException { if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); @@ -85,78 +86,77 @@ public class PDFToFile { pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.')); } - byte[] fileBytes; - String fileName; + String fileName = pdfBaseName + "ToMarkdown.zip"; + TempFile finalOut = tempFileManager.createManagedTempFile(".zip"); + try { + try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf"); + TempDirectory tempOutputDir = new TempDirectory(tempFileManager)) { + inputFile.transferTo(tempInputFile.getFile()); - try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf"); - TempDirectory tempOutputDir = new TempDirectory(tempFileManager)) { - inputFile.transferTo(tempInputFile.getFile()); + List command = + new ArrayList<>( + Arrays.asList( + "pdftohtml", + "-s", + "-noframes", + "-c", + tempInputFile.getAbsolutePath(), + pdfBaseName)); - List command = - new ArrayList<>( - Arrays.asList( - "pdftohtml", - "-s", - "-noframes", - "-c", - tempInputFile.getAbsolutePath(), - pdfBaseName)); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML) + .runCommandWithOutputHandling( + command, tempOutputDir.getPath().toFile()); + // Process HTML files to Markdown + File[] outputFiles = + Objects.requireNonNull(tempOutputDir.getPath().toFile().listFiles()); + List markdownFiles = new ArrayList<>(); + List imageFiles = new ArrayList<>(); - ProcessExecutorResult returnCode = - ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML) - .runCommandWithOutputHandling( - command, tempOutputDir.getPath().toFile()); - // Process HTML files to Markdown - File[] outputFiles = - Objects.requireNonNull(tempOutputDir.getPath().toFile().listFiles()); - List markdownFiles = new ArrayList<>(); - List imageFiles = new ArrayList<>(); + // Convert HTML files to Markdown and collect image files + for (File outputFile : outputFiles) { + if (outputFile.getName().endsWith(".html")) { + String html = Files.readString(outputFile.toPath()); + String markdown = htmlToMarkdownConverter.convert(html); - // Convert HTML files to Markdown and collect image files - for (File outputFile : outputFiles) { - if (outputFile.getName().endsWith(".html")) { - String html = Files.readString(outputFile.toPath()); - String markdown = htmlToMarkdownConverter.convert(html); + // Update image references to point to images/ folder + markdown = updateImageReferences(markdown); - // Update image references to point to images/ folder - markdown = updateImageReferences(markdown); + String mdFileName = outputFile.getName().replace(".html", ".md"); + File mdFile = new File(tempOutputDir.getPath().toFile(), mdFileName); + Files.writeString(mdFile.toPath(), markdown); + markdownFiles.add(mdFile); + } else if (!outputFile.getName().endsWith(".md")) { + // Collect non-HTML, non-MD files as images/assets + imageFiles.add(outputFile); + } + } - String mdFileName = outputFile.getName().replace(".html", ".md"); - File mdFile = new File(tempOutputDir.getPath().toFile(), mdFileName); - Files.writeString(mdFile.toPath(), markdown); - markdownFiles.add(mdFile); - } else if (!outputFile.getName().endsWith(".md")) { - // Collect non-HTML, non-MD files as images/assets - imageFiles.add(outputFile); + try (OutputStream fos = Files.newOutputStream(finalOut.getPath()); + ZipOutputStream zipOutputStream = new ZipOutputStream(fos)) { + // Add markdown files to root of ZIP + for (File mdFile : markdownFiles) { + ZipEntry mdEntry = new ZipEntry(mdFile.getName()); + zipOutputStream.putNextEntry(mdEntry); + Files.copy(mdFile.toPath(), zipOutputStream); + zipOutputStream.closeEntry(); + } + + // Add images and other assets to images/ folder + for (File imageFile : imageFiles) { + ZipEntry assetEntry = new ZipEntry("images/" + imageFile.getName()); + zipOutputStream.putNextEntry(assetEntry); + Files.copy(imageFile.toPath(), zipOutputStream); + zipOutputStream.closeEntry(); + } } } - - // Always create a ZIP file - fileName = pdfBaseName + "ToMarkdown.zip"; - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - - try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) { - // Add markdown files to root of ZIP - for (File mdFile : markdownFiles) { - ZipEntry mdEntry = new ZipEntry(mdFile.getName()); - zipOutputStream.putNextEntry(mdEntry); - Files.copy(mdFile.toPath(), zipOutputStream); - zipOutputStream.closeEntry(); - } - - // Add images and other assets to images/ folder - for (File imageFile : imageFiles) { - ZipEntry assetEntry = new ZipEntry("images/" + imageFile.getName()); - zipOutputStream.putNextEntry(assetEntry); - Files.copy(imageFile.toPath(), zipOutputStream); - zipOutputStream.closeEntry(); - } - } - - fileBytes = byteArrayOutputStream.toByteArray(); + } catch (Exception e) { + finalOut.close(); + throw e; } - return WebResponseUtils.bytesToWebResponse( - fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.fileToWebResponse( + finalOut, fileName, MediaType.APPLICATION_OCTET_STREAM); } /** @@ -169,7 +169,7 @@ public class PDFToFile { return PATTERN.matcher(markdown).replaceAll("$1(images/$2)"); } - public ResponseEntity processPdfToHtml(MultipartFile inputFile) + public ResponseEntity processPdfToHtml(MultipartFile inputFile) throws IOException, InterruptedException { if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); @@ -182,56 +182,57 @@ public class PDFToFile { pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.')); } - byte[] fileBytes; - String fileName; + String fileName = pdfBaseName + "ToHtml.zip"; + TempFile finalOut = tempFileManager.createManagedTempFile(".zip"); + try { + try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf"); + TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) { - try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf"); - TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) { + Path tempInputFile = inputFileTemp.getPath(); + Path tempOutputDir = outputDirTemp.getPath(); - Path tempInputFile = inputFileTemp.getPath(); - Path tempOutputDir = outputDirTemp.getPath(); + // Save the uploaded file to a temporary location + inputFile.transferTo(tempInputFile); - // Save the uploaded file to a temporary location - inputFile.transferTo(tempInputFile); + // Run the pdftohtml command with complex output + List command = + new ArrayList<>( + Arrays.asList( + "pdftohtml", "-c", tempInputFile.toString(), pdfBaseName)); - // Run the pdftohtml command with complex output - List command = - new ArrayList<>( - Arrays.asList( - "pdftohtml", "-c", tempInputFile.toString(), pdfBaseName)); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML) + .runCommandWithOutputHandling(command, tempOutputDir.toFile()); - ProcessExecutorResult returnCode = - ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML) - .runCommandWithOutputHandling(command, tempOutputDir.toFile()); + // Get output files + File[] outputFiles = Objects.requireNonNull(tempOutputDir.toFile().listFiles()); - // Get output files - File[] outputFiles = Objects.requireNonNull(tempOutputDir.toFile().listFiles()); - - // Return output files in a ZIP archive - fileName = pdfBaseName + "ToHtml.zip"; - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) { - for (File outputFile : outputFiles) { - ZipEntry entry = new ZipEntry(outputFile.getName()); - zipOutputStream.putNextEntry(entry); - try (FileInputStream fis = new FileInputStream(outputFile)) { - IOUtils.copy(fis, zipOutputStream); - } catch (IOException e) { - log.error("Exception writing zip entry", e); + try (OutputStream fos = Files.newOutputStream(finalOut.getPath()); + ZipOutputStream zipOutputStream = new ZipOutputStream(fos)) { + for (File outputFile : outputFiles) { + ZipEntry entry = new ZipEntry(outputFile.getName()); + zipOutputStream.putNextEntry(entry); + try (FileInputStream fis = new FileInputStream(outputFile)) { + IOUtils.copy(fis, zipOutputStream); + } catch (IOException e) { + log.error("Exception writing zip entry", e); + } + zipOutputStream.closeEntry(); } - zipOutputStream.closeEntry(); + } catch (IOException e) { + log.error("Exception writing zip", e); } - } catch (IOException e) { - log.error("Exception writing zip", e); } - fileBytes = byteArrayOutputStream.toByteArray(); + } catch (Exception e) { + finalOut.close(); + throw e; } - return WebResponseUtils.bytesToWebResponse( - fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.fileToWebResponse( + finalOut, fileName, MediaType.APPLICATION_OCTET_STREAM); } - public ResponseEntity processPdfToOfficeFormat( + public ResponseEntity processPdfToOfficeFormat( MultipartFile inputFile, String outputFormat, String libreOfficeFilter) throws IOException, InterruptedException { @@ -257,109 +258,115 @@ public class PDFToFile { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } - byte[] fileBytes; String fileName; - + TempFile finalOut = + tempFileManager.createManagedTempFile("." + resolvePrimaryExtension(outputFormat)); Path libreOfficeProfile = null; - try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf"); - TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) { + try { + try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf"); + TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) { - Path tempInputFile = inputFileTemp.getPath(); - Path tempOutputDir = outputDirTemp.getPath(); - Path unoOutputFile = - tempOutputDir.resolve( - pdfBaseName + "." + resolvePrimaryExtension(outputFormat)); + Path tempInputFile = inputFileTemp.getPath(); + Path tempOutputDir = outputDirTemp.getPath(); + Path unoOutputFile = + tempOutputDir.resolve( + pdfBaseName + "." + resolvePrimaryExtension(outputFormat)); - // Save the uploaded file to a temporary location - inputFile.transferTo(tempInputFile); + // Save the uploaded file to a temporary location + inputFile.transferTo(tempInputFile); - // Run the LibreOffice command - ProcessExecutorResult returnCode = null; - IOException unoconvertException = null; + // Run the LibreOffice command + ProcessExecutorResult returnCode = null; + IOException unoconvertException = null; - if (isUnoConvertEnabled()) { - try { - List unoCommand = - buildUnoConvertCommand( - tempInputFile, unoOutputFile, outputFormat, libreOfficeFilter); - returnCode = - ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE) - .runCommandWithOutputHandling(unoCommand); - } catch (IOException e) { - unoconvertException = e; - log.warn( - "Unoconvert command failed ({}). Falling back to soffice command.", - e.getMessage()); - } - } - - if (returnCode == null) { - // Run the LibreOffice command as a fallback - libreOfficeProfile = Files.createTempDirectory("libreoffice_profile_"); - List command = new ArrayList<>(); - command.add(runtimePathConfig.getSOfficePath()); - command.add("-env:UserInstallation=" + libreOfficeProfile.toUri().toString()); - command.add("--headless"); - command.add("--nologo"); - command.add("--infilter=" + libreOfficeFilter); - command.add("--convert-to"); - command.add(outputFormat); - command.add("--outdir"); - command.add(tempOutputDir.toString()); - command.add(tempInputFile.toString()); - - try { - returnCode = - ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE) - .runCommandWithOutputHandling(command); - } catch (IOException e) { - if (unoconvertException != null) { - e.addSuppressed(unoconvertException); + if (isUnoConvertEnabled()) { + try { + List unoCommand = + buildUnoConvertCommand( + tempInputFile, + unoOutputFile, + outputFormat, + libreOfficeFilter); + returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE) + .runCommandWithOutputHandling(unoCommand); + } catch (IOException e) { + unoconvertException = e; + log.warn( + "Unoconvert command failed ({}). Falling back to soffice command.", + e.getMessage()); } - throw e; } - } - // Get output files - List outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles()); + if (returnCode == null) { + // Run the LibreOffice command as a fallback + libreOfficeProfile = Files.createTempDirectory("libreoffice_profile_"); + List command = new ArrayList<>(); + command.add(runtimePathConfig.getSOfficePath()); + command.add("-env:UserInstallation=" + libreOfficeProfile.toUri().toString()); + command.add("--headless"); + command.add("--nologo"); + command.add("--infilter=" + libreOfficeFilter); + command.add("--convert-to"); + command.add(outputFormat); + command.add("--outdir"); + command.add(tempOutputDir.toString()); + command.add(tempInputFile.toString()); - if (outputFiles.size() == 1) { - // Return single output file - File outputFile = outputFiles.get(0); - if ("txt:Text".equals(outputFormat)) { - outputFormat = "txt"; - } - fileName = pdfBaseName + "." + outputFormat; - fileBytes = FileUtils.readFileToByteArray(outputFile); - } else { - // Return output files in a ZIP archive - fileName = pdfBaseName + "To" + outputFormat + ".zip"; - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) { - for (File outputFile : outputFiles) { - ZipEntry entry = new ZipEntry(outputFile.getName()); - zipOutputStream.putNextEntry(entry); - try (FileInputStream fis = new FileInputStream(outputFile)) { - IOUtils.copy(fis, zipOutputStream); - } catch (IOException e) { - log.error("Exception writing zip entry", e); + try { + returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE) + .runCommandWithOutputHandling(command); + } catch (IOException e) { + if (unoconvertException != null) { + e.addSuppressed(unoconvertException); } - - zipOutputStream.closeEntry(); + throw e; } - } catch (IOException e) { - log.error("Exception writing zip", e); } - fileBytes = byteArrayOutputStream.toByteArray(); + // Get output files + List outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles()); + + if (outputFiles.size() == 1) { + // Return single output file + File outputFile = outputFiles.get(0); + if ("txt:Text".equals(outputFormat)) { + outputFormat = "txt"; + } + fileName = pdfBaseName + "." + outputFormat; + FileUtils.copyFile(outputFile, finalOut.getFile()); + } else { + // Return output files in a ZIP archive + fileName = pdfBaseName + "To" + outputFormat + ".zip"; + try (OutputStream fos = Files.newOutputStream(finalOut.getPath()); + ZipOutputStream zipOutputStream = new ZipOutputStream(fos)) { + for (File outputFile : outputFiles) { + ZipEntry entry = new ZipEntry(outputFile.getName()); + zipOutputStream.putNextEntry(entry); + try (FileInputStream fis = new FileInputStream(outputFile)) { + IOUtils.copy(fis, zipOutputStream); + } catch (IOException e) { + log.error("Exception writing zip entry", e); + } + + zipOutputStream.closeEntry(); + } + } catch (IOException e) { + log.error("Exception writing zip", e); + } + } } + } catch (Exception e) { + finalOut.close(); + throw e; } finally { if (libreOfficeProfile != null) { FileUtils.deleteQuietly(libreOfficeProfile.toFile()); } } - return WebResponseUtils.bytesToWebResponse( - fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.fileToWebResponse( + finalOut, fileName, MediaType.APPLICATION_OCTET_STREAM); } private boolean isUnoConvertEnabled() { 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 0e48628d8a..99d25838be 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 @@ -282,8 +282,9 @@ public class ProcessExecutor { boolean finished = process.waitFor(timeoutDuration, TimeUnit.MINUTES); if (!finished) { - // Terminate the process - process.destroy(); + // Kill the entire process tree (descendants first, then the process itself) + process.descendants().forEach(ProcessHandle::destroyForcibly); + process.destroyForcibly(); // Interrupt the reader threads errorReaderThread.interrupt(); outputReaderThread.interrupt(); 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 de59f09c64..48e419a0ae 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 @@ -538,9 +538,9 @@ public final class RegexPatternUtils { getPattern("[^a-zA-Z0-9 ]"); // Input sanitization getPattern("[^a-zA-Z0-9]"); // Filename sanitization // API doc patterns - getPattern("Output:(\\w+)"); // precompiled single-escaped for runtime regex \w - getPattern("Input:(\\w+)"); - getPattern("Type:(\\w+)"); + getPattern("Output:\\s*(\\w+)"); + getPattern("Input:\\s*(\\w+)"); + getPattern("Type:\\s*(\\w+)"); log.debug("Pre-compiled {} common regex patterns", patternCache.size()); } @@ -552,19 +552,19 @@ public final class RegexPatternUtils { /* Pattern for matching Output: in API descriptions */ public Pattern getApiDocOutputTypePattern() { - return getPattern("Output:(\\w+)"); + return getPattern("Output:\\s*(\\w+)"); } /* Pattern for matching Input: in API descriptions */ public Pattern getApiDocInputTypePattern() { - return getPattern("Input:(\\w+)"); + return getPattern("Input:\\s*(\\w+)"); } /** * Pattern for matching Type: in API descriptions */ public Pattern getApiDocTypePattern() { - return getPattern("Type:(\\w+)"); + return getPattern("Type:\\s*(\\w+)"); } /* Pattern for validating file extensions (2-4 alphanumeric, case-insensitive) */ diff --git a/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java b/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java index a5132e2a19..ab8311dd4e 100644 --- a/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java @@ -73,6 +73,19 @@ public class WebResponseUtils { return baosToWebResponse(baos, docName); } + public static ResponseEntity pdfDocToWebResponse( + PDDocument document, String docName, TempFileManager tempFileManager) + throws IOException { + TempFile tempFile = tempFileManager.createManagedTempFile(".pdf"); + try { + document.save(tempFile.getFile()); + } catch (IOException e) { + tempFile.close(); + throw e; + } + return pdfFileToWebResponse(tempFile, docName); + } + /** * Convert a File to a web response (PDF default). * @@ -108,23 +121,37 @@ public class WebResponseUtils { public static ResponseEntity fileToWebResponse( TempFile outputTempFile, String docName, MediaType mediaType) throws IOException { - Path path = outputTempFile.getFile().toPath().normalize(); - long len = Files.size(path); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(mediaType); - headers.setContentLength(len); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + docName + "\""); + try { + Path path = outputTempFile.getFile().toPath().normalize(); + long len = Files.size(path); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(mediaType); + headers.setContentLength(len); + String encodedDocName = + RegexPatternUtils.getInstance() + .getPlusSignPattern() + .matcher(URLEncoder.encode(docName, StandardCharsets.UTF_8)) + .replaceAll("%20"); + headers.setContentDispositionFormData("attachment", encodedDocName); - StreamingResponseBody body = - os -> { - try (os) { - Files.copy(path, os); - os.flush(); - } finally { - outputTempFile.close(); - } - }; + StreamingResponseBody body = + os -> { + try (os) { + Files.copy(path, os); + os.flush(); + } finally { + outputTempFile.close(); + } + }; - return new ResponseEntity<>(body, headers, HttpStatus.OK); + return new ResponseEntity<>(body, headers, HttpStatus.OK); + } catch (IOException | RuntimeException e) { + try { + outputTempFile.close(); + } catch (Exception closeEx) { + e.addSuppressed(closeEx); + } + throw e; + } } } diff --git a/app/common/src/main/java/stirling/software/common/util/ZipExtractionUtils.java b/app/common/src/main/java/stirling/software/common/util/ZipExtractionUtils.java new file mode 100644 index 0000000000..b49d1d2106 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/ZipExtractionUtils.java @@ -0,0 +1,142 @@ +package stirling.software.common.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; + +import io.github.pixee.security.ZipSecurity; + +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; + +/** + * Helpers for detecting and extracting ZIP-formatted responses returned from Stirling API + * endpoints. Shared between {@code PipelineProcessor} and {@code AiWorkflowService} so both callers + * unpack ZIPs consistently (hardened against zip-slip, depth-limited, backed by managed temp + * files). + */ +@Slf4j +@UtilityClass +public class ZipExtractionUtils { + + private static final int MAX_UNZIP_DEPTH = 10; + private static final byte[] ZIP_MAGIC = {0x50, 0x4B, 0x03, 0x04}; + + /** + * Returns true if the resource starts with the standard ZIP magic bytes. CBZ files are + * explicitly treated as non-ZIP. + */ + public static boolean isZip(Resource data) throws IOException { + return isZip(data, null); + } + + /** + * Returns true if the resource starts with the standard ZIP magic bytes. Files named with the + * {@code .cbz} extension are excluded (handled separately by the comic viewer). + */ + public static boolean isZip(Resource data, String filename) throws IOException { + if (data == null || data.contentLength() < ZIP_MAGIC.length) { + return false; + } + if (filename != null && filename.toLowerCase().endsWith(".cbz")) { + return false; + } + try (InputStream is = data.getInputStream()) { + byte[] header = new byte[ZIP_MAGIC.length]; + if (is.read(header) < ZIP_MAGIC.length) { + return false; + } + for (int i = 0; i < ZIP_MAGIC.length; i++) { + if (header[i] != ZIP_MAGIC[i]) { + return false; + } + } + return true; + } + } + + /** + * Extract a ZIP resource into a flat list of resources, one per file entry. Nested ZIPs are + * recursively extracted up to {@link #MAX_UNZIP_DEPTH}. Each entry is materialized as a + * hardened-extracted managed temp file so downstream consumers can stream the bytes. + */ + public static List extractZip(Resource zip, TempFileManager tempFileManager) + throws IOException { + return extractZip(zip, tempFileManager, null); + } + + /** + * Extract a ZIP resource into a flat list of resources. Each created {@link TempFile} is also + * passed to {@code tempFileConsumer} when non-null, giving callers the option to register the + * temp files with an auxiliary lifecycle (e.g. {@code PipelineResult}). + */ + public static List extractZip( + Resource zip, TempFileManager tempFileManager, Consumer tempFileConsumer) + throws IOException { + return extractZipInternal(zip, tempFileManager, tempFileConsumer, 0); + } + + private static List extractZipInternal( + Resource zip, + TempFileManager tempFileManager, + Consumer tempFileConsumer, + int depth) + throws IOException { + if (depth > MAX_UNZIP_DEPTH) { + log.warn( + "ZIP nesting depth {} exceeds limit {}, treating as file", + depth, + MAX_UNZIP_DEPTH); + return List.of(zip); + } + log.debug("Unzipping data of length: {}", zip.contentLength()); + List extracted = new ArrayList<>(); + try (InputStream bais = zip.getInputStream(); + ZipInputStream zis = ZipSecurity.createHardenedInputStream(bais)) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.isDirectory()) { + continue; + } + TempFile tempFile = tempFileManager.createManagedTempFile("unzip"); + if (tempFileConsumer != null) { + tempFileConsumer.accept(tempFile); + } + try (OutputStream os = Files.newOutputStream(tempFile.getPath())) { + byte[] buffer = new byte[4096]; + int count; + while ((count = zis.read(buffer)) != -1) { + os.write(buffer, 0, count); + } + } + final String filename = entry.getName(); + Resource fileResource = + new FileSystemResource(tempFile.getFile()) { + @Override + public String getFilename() { + return filename; + } + }; + if (isZip(fileResource, filename)) { + log.debug("Nested ZIP entry {} — recursing", filename); + extracted.addAll( + extractZipInternal( + fileResource, tempFileManager, tempFileConsumer, depth + 1)); + } else { + extracted.add(fileResource); + } + } + } + log.debug("Unzipping completed. {} files extracted.", extracted.size()); + return extracted; + } +} diff --git a/app/common/src/test/java/stirling/software/common/service/InternalApiClientTest.java b/app/common/src/test/java/stirling/software/common/service/InternalApiClientTest.java new file mode 100644 index 0000000000..f815e92e71 --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/service/InternalApiClientTest.java @@ -0,0 +1,159 @@ +package stirling.software.common.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.BeforeEach; +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.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.*; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RequestCallback; +import org.springframework.web.client.ResponseExtractor; +import org.springframework.web.client.RestTemplate; + +import jakarta.servlet.ServletContext; + +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; + +@ExtendWith(MockitoExtension.class) +class InternalApiClientTest { + + @Mock ServletContext servletContext; + @Mock UserServiceInterface userService; + @Mock TempFileManager tempFileManager; + + InternalApiClient client; + + @BeforeEach + void setUp() { + lenient().when(servletContext.getContextPath()).thenReturn(""); + MockEnvironment environment = new MockEnvironment().withProperty("server.port", "8080"); + client = new InternalApiClient(servletContext, userService, tempFileManager, environment); + } + + @Test + void postDoesNotForceContentType() throws Exception { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("fileInput", namedResource("input.pdf", "data")); + + Path tempPath = Files.createTempFile("internal-api-test", ".tmp"); + TempFile tempFile = mock(TempFile.class); + when(tempFile.getPath()).thenReturn(tempPath); + when(tempFile.getFile()).thenReturn(tempPath.toFile()); + when(tempFileManager.createManagedTempFile("internal-api")).thenReturn(tempFile); + + HttpHeaders[] captured = {null}; + + try (var ignored = + mockConstruction( + RestTemplate.class, + (rt, ctx) -> { + when(rt.httpEntityCallback(any(), eq(Resource.class))) + .thenAnswer( + inv -> { + HttpEntity entity = inv.getArgument(0); + captured[0] = entity.getHeaders(); + return (RequestCallback) req -> {}; + }); + + when(rt.execute(anyString(), eq(HttpMethod.POST), any(), any())) + .thenAnswer(inv -> fakeOkResponse(inv.getArgument(3))); + })) { + + ResponseEntity response = client.post("/api/v1/general/merge-pdfs", body); + + assertNotNull(response); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertNull(captured[0].getContentType(), "Content-Type should not be forced"); + } finally { + Files.deleteIfExists(tempPath); + } + } + + @Test + void postRejectsDisallowedPath() { + MultiValueMap body = new LinkedMultiValueMap<>(); + assertThrows(SecurityException.class, () -> client.post("/api/v1/admin/settings", body)); + } + + @Test + void postRejectsPathTraversal() { + MultiValueMap body = new LinkedMultiValueMap<>(); + assertThrows( + SecurityException.class, + () -> client.post("/api/v1/misc/../../actuator/env", body)); + } + + @Test + void postRejectsUrlEncodedCharacters() { + MultiValueMap body = new LinkedMultiValueMap<>(); + assertThrows( + SecurityException.class, () -> client.post("/api/v1/misc/%2e%2e/actuator", body)); + } + + @Test + void postRejectsQueryString() { + MultiValueMap body = new LinkedMultiValueMap<>(); + assertThrows( + SecurityException.class, + () -> client.post("/api/v1/misc/compress-pdf?redirect=evil", body)); + } + + @Test + void postRejectsEmptySegment() { + MultiValueMap body = new LinkedMultiValueMap<>(); + assertThrows(SecurityException.class, () -> client.post("/api/v1/misc//foo", body)); + } + + @Test + void postRejectsTrailingSlash() { + MultiValueMap body = new LinkedMultiValueMap<>(); + assertThrows(SecurityException.class, () -> client.post("/api/v1/misc/foo/", body)); + } + + @Test + void postRejectsNullPath() { + MultiValueMap body = new LinkedMultiValueMap<>(); + assertThrows(SecurityException.class, () -> client.post(null, body)); + } + + /** Create a ByteArrayResource with a filename (required for multipart). */ + private static Resource namedResource(String filename, String content) { + return new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8)) { + @Override + public String getFilename() { + return filename; + } + }; + } + + /** Simulate a successful HTTP response through a RestTemplate ResponseExtractor. */ + @SuppressWarnings("unchecked") + private static ResponseEntity fakeOkResponse(Object extractorArg) throws Exception { + var extractor = (ResponseExtractor>) extractorArg; + ClientHttpResponse response = mock(ClientHttpResponse.class); + when(response.getBody()) + .thenReturn(new ByteArrayInputStream("ok".getBytes(StandardCharsets.UTF_8))); + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"out.pdf\""); + when(response.getHeaders()).thenReturn(headers); + lenient().when(response.getStatusCode()).thenReturn(HttpStatus.OK); + return extractor.extractData(response); + } +} diff --git a/app/common/src/test/java/stirling/software/common/util/FormUtilsAdditionalTest.java b/app/common/src/test/java/stirling/software/common/util/FormUtilsAdditionalTest.java index 266639f690..5a605af353 100644 --- a/app/common/src/test/java/stirling/software/common/util/FormUtilsAdditionalTest.java +++ b/app/common/src/test/java/stirling/software/common/util/FormUtilsAdditionalTest.java @@ -3,6 +3,7 @@ package stirling.software.common.util; import static org.junit.jupiter.api.Assertions.*; import java.io.IOException; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -249,6 +250,60 @@ class FormUtilsAdditionalTest { } } + // Regression: PDFBOX-5962. Flattening with an empty values map used to force + // setNeedAppearances(true), triggering PDFBox's refreshAppearances loop which + // could hang indefinitely. The call must complete quickly and clear form fields. + @Test + void testApplyFieldValues_emptyValuesWithFlatten_completesAndFlattens() throws IOException { + try (PDDocument doc = new PDDocument()) { + SetupDocument setup = createBasicDocument(doc); + PDTextField textField = new PDTextField(setup.acroForm); + textField.setPartialName("company"); + attachWidget(setup, textField, new PDRectangle(60, 720, 220, 20)); + + assertTrue(setup.acroForm.getNeedAppearances()); + + assertTimeoutPreemptively( + Duration.ofSeconds(10), + () -> FormUtils.applyFieldValues(doc, Map.of(), true, false)); + + PDAcroForm after = doc.getDocumentCatalog().getAcroForm(); + assertTrue(after == null || after.getFields().isEmpty()); + } + } + + @Test + void testApplyFieldValues_nullValuesWithFlatten_completesAndFlattens() throws IOException { + try (PDDocument doc = new PDDocument()) { + SetupDocument setup = createBasicDocument(doc); + PDTextField textField = new PDTextField(setup.acroForm); + textField.setPartialName("company"); + attachWidget(setup, textField, new PDRectangle(60, 720, 220, 20)); + + assertTimeoutPreemptively( + Duration.ofSeconds(10), + () -> FormUtils.applyFieldValues(doc, null, true, false)); + + PDAcroForm after = doc.getDocumentCatalog().getAcroForm(); + assertTrue(after == null || after.getFields().isEmpty()); + } + } + + @Test + void testApplyFieldValues_valuesWithFlatten_appliesValueAndFlattens() throws IOException { + try (PDDocument doc = new PDDocument()) { + SetupDocument setup = createBasicDocument(doc); + PDTextField textField = new PDTextField(setup.acroForm); + textField.setPartialName("company"); + attachWidget(setup, textField, new PDRectangle(60, 720, 220, 20)); + + FormUtils.applyFieldValues(doc, Map.of("company", "Stirling"), true, false); + + PDAcroForm after = doc.getDocumentCatalog().getAcroForm(); + assertTrue(after == null || after.getFields().isEmpty()); + } + } + // --- filterSingleChoiceSelection --- @Test 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 459b64a45c..69db94c34b 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 @@ -9,6 +9,7 @@ import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -29,6 +30,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.ZipSecurity; @@ -59,6 +61,19 @@ class PDFToFileTest { .thenAnswer( invocation -> Files.createTempFile("test", invocation.getArgument(0)).toFile()); + lenient() + .when(mockTempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + invocation -> { + File f = + Files.createTempFile("test", invocation.getArgument(0)) + .toFile(); + TempFile tf = org.mockito.Mockito.mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + lenient().when(tf.getAbsolutePath()).thenReturn(f.getAbsolutePath()); + return tf; + }); lenient() .when(mockTempFileManager.createTempDirectory()) .thenAnswer(invocation -> Files.createTempDirectory("test")); @@ -68,6 +83,12 @@ class PDFToFileTest { pdfToFile = new PDFToFile(mockTempFileManager, mockRuntimePathConfig); } + private static byte[] drain(ResponseEntity response) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } + @Test void testProcessPdfToMarkdown_InvalidContentType() throws IOException, InterruptedException { // Prepare @@ -79,7 +100,7 @@ class PDFToFileTest { "This is not a PDF".getBytes()); // Execute - ResponseEntity response = pdfToFile.processPdfToMarkdown(nonPdfFile); + ResponseEntity response = pdfToFile.processPdfToMarkdown(nonPdfFile); // Verify assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); @@ -96,7 +117,7 @@ class PDFToFileTest { "This is not a PDF".getBytes()); // Execute - ResponseEntity response = pdfToFile.processPdfToHtml(nonPdfFile); + ResponseEntity response = pdfToFile.processPdfToHtml(nonPdfFile); // Verify assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); @@ -114,7 +135,7 @@ class PDFToFileTest { "This is not a PDF".getBytes()); // Execute - ResponseEntity response = + ResponseEntity response = pdfToFile.processPdfToOfficeFormat(nonPdfFile, "docx", "draw_pdf_import"); // Verify @@ -133,7 +154,7 @@ class PDFToFileTest { "Fake PDF content".getBytes()); // Execute with invalid format - ResponseEntity response = + ResponseEntity response = pdfToFile.processPdfToOfficeFormat(pdfFile, "invalid_format", "draw_pdf_import"); // Verify @@ -184,12 +205,14 @@ class PDFToFileTest { }); // Execute the method - ResponseEntity response = pdfToFile.processPdfToMarkdown(pdfFile); + ResponseEntity response = + pdfToFile.processPdfToMarkdown(pdfFile); // Verify - should now return a ZIP file instead of plain markdown assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); // Verify content disposition indicates a ZIP file assertTrue( @@ -201,7 +224,7 @@ class PDFToFileTest { // Verify the content by unzipping it try (ZipInputStream zipStream = ZipSecurity.createHardenedInputStream( - new java.io.ByteArrayInputStream(response.getBody()))) { + new java.io.ByteArrayInputStream(bodyBytes))) { ZipEntry entry; boolean foundMdFile = false; boolean foundImageInFolder = false; @@ -275,12 +298,14 @@ class PDFToFileTest { }); // Execute the method - ResponseEntity response = pdfToFile.processPdfToMarkdown(pdfFile); + ResponseEntity response = + pdfToFile.processPdfToMarkdown(pdfFile); // Verify assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); // Verify content disposition indicates a zip file assertTrue( @@ -292,7 +317,7 @@ class PDFToFileTest { // Verify the content by unzipping it try (ZipInputStream zipStream = ZipSecurity.createHardenedInputStream( - new java.io.ByteArrayInputStream(response.getBody()))) { + new java.io.ByteArrayInputStream(bodyBytes))) { ZipEntry entry; boolean foundMdFiles = false; boolean foundImage = false; @@ -352,12 +377,13 @@ class PDFToFileTest { }); // Execute the method - ResponseEntity response = pdfToFile.processPdfToHtml(pdfFile); + ResponseEntity response = pdfToFile.processPdfToHtml(pdfFile); // Verify assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); // Verify content disposition indicates a zip file assertTrue( @@ -369,7 +395,7 @@ class PDFToFileTest { // Verify the content by unzipping it try (ZipInputStream zipStream = ZipSecurity.createHardenedInputStream( - new java.io.ByteArrayInputStream(response.getBody()))) { + new java.io.ByteArrayInputStream(bodyBytes))) { ZipEntry entry; boolean foundMainHtml = false; boolean foundIndexHtml = false; @@ -437,13 +463,14 @@ class PDFToFileTest { }); // Execute the method with docx format - ResponseEntity response = + ResponseEntity response = pdfToFile.processPdfToOfficeFormat(pdfFile, "docx", "draw_pdf_import"); // Verify assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); // Verify content disposition has correct filename assertTrue( @@ -508,13 +535,14 @@ class PDFToFileTest { }); // Execute the method with ODP format - ResponseEntity response = + ResponseEntity response = pdfToFile.processPdfToOfficeFormat(pdfFile, "odp", "draw_pdf_import"); // Verify assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); // Verify content disposition for zip file assertTrue( @@ -526,7 +554,7 @@ class PDFToFileTest { // Verify the content by unzipping it try (ZipInputStream zipStream = ZipSecurity.createHardenedInputStream( - new java.io.ByteArrayInputStream(response.getBody()))) { + new java.io.ByteArrayInputStream(bodyBytes))) { ZipEntry entry; boolean foundMainFile = false; boolean foundMediaFiles = false; @@ -592,13 +620,14 @@ class PDFToFileTest { }); // Execute the method with text format - ResponseEntity response = + ResponseEntity response = pdfToFile.processPdfToOfficeFormat(pdfFile, "txt:Text", "draw_pdf_import"); // Verify assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); // Verify content disposition has txt extension assertTrue( @@ -650,13 +679,14 @@ class PDFToFileTest { }); // Execute the method - ResponseEntity response = + ResponseEntity response = pdfToFile.processPdfToOfficeFormat(pdfFile, "docx", "draw_pdf_import"); // Verify assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); // Verify content disposition contains output.docx assertTrue( @@ -696,12 +726,13 @@ class PDFToFileTest { return mockExecutorResult; }); - ResponseEntity response = + ResponseEntity response = pdfToFileWithUno.processPdfToOfficeFormat(pdfFile, "docx", "writer_pdf_import"); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); assertTrue( response.getHeaders() .getContentDisposition() @@ -759,12 +790,13 @@ class PDFToFileTest { return mockExecutorResult; }); - ResponseEntity response = + ResponseEntity response = pdfToFileWithUno.processPdfToOfficeFormat(pdfFile, "docx", "writer_pdf_import"); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); assertTrue( response.getHeaders() .getContentDisposition() diff --git a/app/core/build.gradle b/app/core/build.gradle index 48d488b9ab..202351a650 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -14,6 +14,8 @@ spotless { target 'src/**/java/**/*.java' targetExclude 'src/main/resources/static/**', 'src/main/java/org/apache/**' googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false) + // google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25 + suppressLintsFor { setStep('google-java-format') } importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling") trimTrailingWhitespace() @@ -66,7 +68,7 @@ dependencies { implementation 'commons-io:commons-io:2.21.0' implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion" - implementation 'io.micrometer:micrometer-core:1.16.2' + implementation 'io.micrometer:micrometer-core' implementation 'com.google.zxing:core:3.5.4' implementation "org.commonmark:commonmark:$commonmarkVersion" // https://mvnrepository.com/artifact/org.commonmark/commonmark implementation "org.commonmark:commonmark-ext-gfm-tables:$commonmarkVersion" @@ -82,7 +84,7 @@ dependencies { // 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.6' + implementation 'com.sun.xml.bind:jaxb-core:4.0.7' implementation 'org.apache.poi:poi-ooxml:5.5.1' // https://mvnrepository.com/artifact/technology.tabula/tabula @@ -176,6 +178,7 @@ springBoot { // Frontend build tasks - only enabled with -PbuildWithFrontend=true def buildWithFrontend = project.hasProperty('buildWithFrontend') && project.property('buildWithFrontend') == 'true' +def buildPrototypes = project.hasProperty('prototypesMode') && project.property('prototypesMode') == 'true' def frontendDir = file('../../frontend') def frontendDistDir = file('../../frontend/dist') def resourcesStaticDir = file('src/main/resources/static') @@ -243,9 +246,8 @@ tasks.register('npmBuild', Exec) { enabled = buildWithFrontend group = 'frontend' description = 'Build frontend application' - workingDir frontendDir - commandLine = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c', 'npm', 'run', 'build'] : ['npm', 'run', 'build'] - dependsOn npmInstall + workingDir file('../..') + commandLine = buildPrototypes ? ['task', 'frontend:build:prototypes'] : ['task', 'frontend:build'] inputs.dir(new File(frontendDir, 'src')) inputs.dir(new File(frontendDir, 'public')) inputs.file(new File(frontendDir, 'package.json')) 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 1a75c60f31..c08e53e1ab 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 @@ -73,7 +73,8 @@ public class ExternalAppDepConfig { tmp.put("tesseract", List.of("tesseract")); tmp.put("rar", List.of("rar")); // Required for real CBR output tmp.put(calibrePath, List.of("Calibre")); - tmp.put("ffmpeg", List.of("FFmpeg")); + // ffmpeg disabled due to raised CVEs + // tmp.put("ffmpeg", List.of("FFmpeg")); tmp.put("magick", List.of("ImageMagick")); this.commandToGroupMapping = Collections.unmodifiableMap(tmp); } diff --git a/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index a3712bf975..205a0d5734 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -47,7 +47,7 @@ public class OpenApiConfig { .version(version) .license( new License() - .name("MIT") + .name("Open-Core - MIT Licensed") .url( "https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/LICENSE")) .termsOfService("https://www.stirlingpdf.com/terms") diff --git a/app/core/src/main/java/stirling/software/SPDF/config/SpringDocConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/SpringDocConfig.java index 6733eb22dc..42c9e97b35 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/SpringDocConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/SpringDocConfig.java @@ -28,7 +28,17 @@ public class SpringDocConfig { "/api/v1/proprietary/ui-data/**", "/api/v1/info/**", "/api/v1/general/job/**", - "/api/v1/general/files/**") + "/api/v1/general/files/**", + "/api/v1/general/signatures/**", + "/api/v1/database/**", + "/api/v1/storage/**", + "/api/v1/proprietary/signatures/**", + "/api/v1/workflow/participant/**", + "/api/v1/security/cert-sign/sessions", + "/api/v1/security/cert-sign/sessions/**", + "/api/v1/security/cert-sign/sign-requests", + "/api/v1/security/cert-sign/sign-requests/**", + "/api/v1/security/cert-sign/validate-certificate") .addOpenApiCustomizer(pdfFileOneOfCustomizer) .addOpenApiCustomizer( openApi -> { @@ -53,7 +63,16 @@ public class SpringDocConfig { "/api/v1/team/**", "/api/v1/auth/**", "/api/v1/invite/**", - "/api/v1/audit/**") + "/api/v1/audit/**", + "/api/v1/database/**", + "/api/v1/storage/**", + "/api/v1/proprietary/signatures/**", + "/api/v1/workflow/participant/**", + "/api/v1/security/cert-sign/sessions", + "/api/v1/security/cert-sign/sessions/**", + "/api/v1/security/cert-sign/sign-requests", + "/api/v1/security/cert-sign/sign-requests/**", + "/api/v1/security/cert-sign/validate-certificate") .addOpenApiCustomizer( openApi -> { openApi.info( @@ -75,7 +94,8 @@ public class SpringDocConfig { "/api/v1/proprietary/ui-data/**", "/api/v1/info/**", "/api/v1/general/job/**", - "/api/v1/general/files/**") + "/api/v1/general/files/**", + "/api/v1/general/signatures/**") .addOpenApiCustomizer( openApi -> { openApi.info( diff --git a/app/core/src/main/java/stirling/software/SPDF/config/TauriProcessMonitor.java b/app/core/src/main/java/stirling/software/SPDF/config/TauriProcessMonitor.java index 9ec1bb37e6..2d5ca8b17c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/TauriProcessMonitor.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/TauriProcessMonitor.java @@ -1,7 +1,6 @@ package stirling.software.SPDF.config; import java.lang.management.ManagementFactory; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -106,25 +105,27 @@ public class TauriProcessMonitor { logger.info("Orphaned Java backend detected. Shutting down gracefully..."); // Shutdown asynchronously to avoid blocking the monitor thread - CompletableFuture.runAsync( - () -> { - try { - // Give a small delay to ensure logging completes - Thread.sleep(1000); + Thread.ofVirtual() + .name("tauri-graceful-shutdown") + .start( + () -> { + try { + // Give a small delay to ensure logging completes + Thread.sleep(1000); - if (applicationContext instanceof ConfigurableApplicationContext) { - ((ConfigurableApplicationContext) applicationContext).close(); - } else { - // Fallback to system exit - logger.warn( - "Unable to shutdown Spring context gracefully, using System.exit"); - System.exit(0); - } - } catch (Exception e) { - logger.error("Error during graceful shutdown", e); - System.exit(1); - } - }); + if (applicationContext instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) applicationContext).close(); + } else { + // Fallback to system exit + logger.warn( + "Unable to shutdown Spring context gracefully, using System.exit"); + System.exit(0); + } + } catch (Exception e) { + logger.error("Error during graceful shutdown", e); + System.exit(1); + } + }); } @PreDestroy 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 fdf3c716f2..8a3313e881 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 @@ -1,7 +1,6 @@ package stirling.software.SPDF.controller.api; import java.awt.*; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -19,6 +18,7 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -30,6 +30,7 @@ import stirling.software.SPDF.model.api.general.BookletImpositionRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @RestController @@ -39,6 +40,7 @@ import stirling.software.common.util.WebResponseUtils; public class BookletImpositionController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping( value = "/booklet-imposition", @@ -49,7 +51,7 @@ public class BookletImpositionController { "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") - public ResponseEntity createBookletImposition( + public ResponseEntity createBookletImposition( @ModelAttribute BookletImpositionRequest request) throws IOException { MultipartFile file = request.getFileInput(); @@ -85,15 +87,12 @@ public class BookletImpositionController { duplexPass, flipOnShortEdge)) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - newDocument.save(baos); - - byte[] result = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse( - result, + return WebResponseUtils.pdfDocToWebResponse( + newDocument, GeneralUtils.generateFilename( Filenames.toSimpleFileName(file.getOriginalFilename()), - "_booklet.pdf")); + "_booklet.pdf"), + tempFileManager); } } } 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 5e9eca5510..845d3519cd 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 @@ -1,10 +1,7 @@ package stirling.software.SPDF.controller.api; import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.List; import org.apache.pdfbox.multipdf.LayerUtility; @@ -18,6 +15,7 @@ import org.apache.pdfbox.rendering.PDFRenderer; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -32,6 +30,8 @@ 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.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -46,6 +46,7 @@ public class CropController { private static final String PDF_EXTENSION = ".pdf"; private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; private static int[] detectContentBounds(BufferedImage image) { int width = image.getWidth(); @@ -131,7 +132,8 @@ public class CropController { description = "This operation takes an input PDF file and crops it according to the given" + " coordinates. Input:PDF Output:PDF Type:SISO") - public ResponseEntity cropPdf(@ModelAttribute CropPdfForm request) throws IOException { + public ResponseEntity cropPdf(@ModelAttribute CropPdfForm request) + throws IOException { if (request.isAutoCrop()) { return cropWithAutomaticDetection(request); } @@ -151,8 +153,8 @@ public class CropController { } } - private ResponseEntity cropWithAutomaticDetection(@ModelAttribute CropPdfForm request) - throws IOException { + private ResponseEntity cropWithAutomaticDetection( + @ModelAttribute CropPdfForm request) throws IOException { try (PDDocument sourceDocument = pdfDocumentFactory.load(request)) { try (PDDocument newDocument = @@ -196,20 +198,17 @@ public class CropController { cropBounds.height)); } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - newDocument.save(baos); - byte[] pdfContent = baos.toByteArray(); - - return WebResponseUtils.bytesToWebResponse( - pdfContent, + return WebResponseUtils.pdfDocToWebResponse( + newDocument, GeneralUtils.generateFilename( - request.getFileInput().getOriginalFilename(), "_cropped.pdf")); + request.getFileInput().getOriginalFilename(), "_cropped.pdf"), + tempFileManager); } } } - private ResponseEntity cropWithPDFBox(@ModelAttribute CropPdfForm request) - throws IOException { + private ResponseEntity cropWithPDFBox( + @ModelAttribute CropPdfForm request) throws IOException { try (PDDocument sourceDocument = pdfDocumentFactory.load(request)) { try (PDDocument newDocument = @@ -255,22 +254,19 @@ public class CropController { request.getHeight())); } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - newDocument.save(baos); - - byte[] pdfContent = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse( - pdfContent, + return WebResponseUtils.pdfDocToWebResponse( + newDocument, GeneralUtils.generateFilename( - request.getFileInput().getOriginalFilename(), "_cropped.pdf")); + request.getFileInput().getOriginalFilename(), "_cropped.pdf"), + tempFileManager); } } } - private ResponseEntity cropWithGhostscript(@ModelAttribute CropPdfForm request) - throws IOException { - Path tempInputFile = null; - Path tempOutputFile = null; + private ResponseEntity cropWithGhostscript( + @ModelAttribute CropPdfForm request) throws IOException { + TempFile tempInputFile = null; + TempFile tempOutputFile = null; try (PDDocument sourceDocument = pdfDocumentFactory.load(request)) { for (int i = 0; i < sourceDocument.getNumberOfPages(); i++) { @@ -284,11 +280,11 @@ public class CropController { page.setCropBox(cropBox); } - tempInputFile = Files.createTempFile(TEMP_INPUT_PREFIX, PDF_EXTENSION); - tempOutputFile = Files.createTempFile(TEMP_OUTPUT_PREFIX, PDF_EXTENSION); + tempInputFile = tempFileManager.createManagedTempFile(PDF_EXTENSION); + tempOutputFile = tempFileManager.createManagedTempFile(PDF_EXTENSION); // Save the source document with crop boxes - sourceDocument.save(tempInputFile.toFile()); + sourceDocument.save(tempInputFile.getFile()); // Execute Ghostscript to process the crop boxes ProcessExecutor processExecutor = @@ -299,15 +295,15 @@ public class CropController { "-sDEVICE=pdfwrite", "-dUseCropBox", "-o", - tempOutputFile.toString(), - tempInputFile.toString()); + tempOutputFile.getAbsolutePath(), + tempInputFile.getAbsolutePath()); processExecutor.runCommandWithOutputHandling(command); - byte[] pdfContent = Files.readAllBytes(tempOutputFile); - - return WebResponseUtils.bytesToWebResponse( - pdfContent, + TempFile out = tempOutputFile; + tempOutputFile = null; // ownership transferred to StreamingResponseBody + return WebResponseUtils.pdfFileToWebResponse( + out, GeneralUtils.generateFilename( request.getFileInput().getOriginalFilename(), "_cropped.pdf")); @@ -316,10 +312,10 @@ public class CropController { throw ExceptionUtils.createProcessingInterruptedException("Ghostscript", e); } finally { if (tempInputFile != null) { - Files.deleteIfExists(tempInputFile); + tempInputFile.close(); } if (tempOutputFile != null) { - Files.deleteIfExists(tempOutputFile); + tempOutputFile.close(); } } } 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 c97ca11256..bd51dfbd16 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 @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api; -import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -14,6 +13,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -27,6 +27,7 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; import tools.jackson.core.type.TypeReference; @@ -39,6 +40,7 @@ public class EditTableOfContentsController { private final CustomPDFDocumentFactory pdfDocumentFactory; private final ObjectMapper objectMapper; + private final TempFileManager tempFileManager; @AutoJobPostMapping( value = "/extract-bookmarks", @@ -149,12 +151,11 @@ public class EditTableOfContentsController { @Operation( summary = "Edit Table of Contents", description = "Add or edit bookmarks/table of contents in a PDF document.") - public ResponseEntity editTableOfContents( + public ResponseEntity editTableOfContents( @ModelAttribute EditTableOfContentsRequest request) throws Exception { MultipartFile file = request.getFileInput(); - try (PDDocument document = pdfDocumentFactory.load(file); - ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + try (PDDocument document = pdfDocumentFactory.load(file)) { // Parse the bookmark data from JSON List bookmarks = @@ -168,13 +169,10 @@ public class EditTableOfContentsController { // Add bookmarks to the outline addBookmarksToOutline(document, outline, bookmarks); - // Save the document to a byte array - document.save(baos); - - return WebResponseUtils.bytesToWebResponse( - baos.toByteArray(), + return WebResponseUtils.pdfDocToWebResponse( + document, GeneralUtils.generateFilename(file.getOriginalFilename(), "_with_toc.pdf"), - MediaType.APPLICATION_PDF); + tempFileManager); } } 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 7a4f12d697..3a7718fe8e 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 @@ -3,7 +3,6 @@ package stirling.software.SPDF.controller.api; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -29,6 +28,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -279,7 +279,7 @@ public class MergeController { "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") - public ResponseEntity mergePdfs( + public ResponseEntity mergePdfs( @ModelAttribute MergePdfsRequest request, @RequestParam(value = "fileOrder", required = false) String fileOrder) throws IOException { @@ -399,12 +399,6 @@ public class MergeController { String mergedFileName = GeneralUtils.generateFilename(firstFilename, "_merged_unsigned.pdf"); - byte[] pdfBytes; - try { - pdfBytes = Files.readAllBytes(outputTempFile.getPath()); - } finally { - outputTempFile.close(); - } - return WebResponseUtils.bytesToWebResponse(pdfBytes, mergedFileName); + return WebResponseUtils.pdfFileToWebResponse(outputTempFile, mergedFileName); } } 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 7066a47c9a..4fad2c5b1b 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 @@ -1,7 +1,6 @@ package stirling.software.SPDF.controller.api; import java.awt.Color; -import java.io.ByteArrayOutputStream; import java.io.IOException; import org.apache.pdfbox.multipdf.LayerUtility; @@ -15,6 +14,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -28,6 +28,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralFormCopyUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -36,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils; public class MultiPageLayoutController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping( value = "/multi-page-layout", @@ -45,7 +47,7 @@ public class MultiPageLayoutController { 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") - public ResponseEntity mergeMultiplePagesIntoOne( + public ResponseEntity mergeMultiplePagesIntoOne( @ModelAttribute MergeMultiplePagesRequest request) throws IOException { int MAX_PAGES = 100000; @@ -338,13 +340,11 @@ public class MultiPageLayoutController { } } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - newDocument.save(baos); - byte[] result = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse( - result, + return WebResponseUtils.pdfDocToWebResponse( + newDocument, GeneralUtils.generateFilename( - file.getOriginalFilename(), "_multi_page_layout.pdf")); + file.getOriginalFilename(), "_multi_page_layout.pdf"), + tempFileManager); } // newDocument is closed here } // sourceDocument is closed here } 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 df146ea9a0..826ffcb1fb 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 @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -16,6 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -28,6 +28,8 @@ import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -35,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils; public class PdfOverlayController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(value = "/overlay-pdfs", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @StandardPdfResponse @@ -43,8 +46,8 @@ public class PdfOverlayController { description = "Overlay PDF files onto a base PDF with different modes: Sequential," + " Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO") - public ResponseEntity overlayPdfs(@ModelAttribute OverlayPdfsRequest request) - throws IOException { + public ResponseEntity overlayPdfs( + @ModelAttribute OverlayPdfsRequest request) throws IOException { MultipartFile baseFile = request.getFileInput(); int overlayPos = request.getOverlayPosition(); @@ -52,6 +55,7 @@ public class PdfOverlayController { File[] overlayPdfFiles = new File[overlayFiles.length]; List tempFiles = new ArrayList<>(); // List to keep track of temporary files + TempFile tempOut = null; try { for (int i = 0; i < overlayFiles.length; i++) { overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]); @@ -62,8 +66,7 @@ public class PdfOverlayController { int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode try (PDDocument basePdf = pdfDocumentFactory.load(baseFile); - Overlay overlay = new Overlay(); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + Overlay overlay = new Overlay()) { Map overlayGuide = prepareOverlayGuide( basePdf.getNumberOfPages(), @@ -79,15 +82,21 @@ public class PdfOverlayController { overlay.setOverlayPosition(Overlay.Position.BACKGROUND); } - overlay.overlay(overlayGuide).save(outputStream); - byte[] data = outputStream.toByteArray(); + tempOut = tempFileManager.createManagedTempFile(".pdf"); + overlay.overlay(overlayGuide).save(tempOut.getFile()); String outputFilename = GeneralUtils.generateFilename( baseFile.getOriginalFilename(), "_overlayed.pdf"); - return WebResponseUtils.bytesToWebResponse( - data, outputFilename, MediaType.APPLICATION_PDF); + TempFile out = tempOut; + tempOut = null; // ownership transferred to StreamingResponseBody + return WebResponseUtils.pdfFileToWebResponse(out, outputFilename); } + } catch (Exception e) { + if (tempOut != null) { + tempOut.close(); + } + throw e; } finally { for (File overlayPdfFile : overlayPdfFiles) { if (overlayPdfFile != null) { 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 9ad66a2dd6..de58a882c8 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 @@ -12,6 +12,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -27,6 +28,7 @@ import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -35,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils; public class RearrangePagesPDFController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-pages") @StandardPdfResponse @@ -44,8 +47,8 @@ public class RearrangePagesPDFController { "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") - public ResponseEntity deletePages(@ModelAttribute PDFWithPageNums request) - throws IOException { + public ResponseEntity deletePages( + @ModelAttribute PDFWithPageNums request) throws IOException { MultipartFile pdfFile = request.getFileInput(); String pagesToDelete = request.getPageNumbers(); @@ -67,7 +70,8 @@ public class RearrangePagesPDFController { return WebResponseUtils.pdfDocToWebResponse( document, GeneralUtils.generateFilename( - pdfFile.getOriginalFilename(), "_removed_pages.pdf")); + pdfFile.getOriginalFilename(), "_removed_pages.pdf"), + tempFileManager); } } @@ -224,8 +228,8 @@ public class RearrangePagesPDFController { + " 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") - public ResponseEntity rearrangePages(@ModelAttribute RearrangePagesRequest request) - throws IOException { + public ResponseEntity rearrangePages( + @ModelAttribute RearrangePagesRequest request) throws IOException { MultipartFile pdfFile = request.getFileInput(); String pageOrder = request.getPageNumbers(); String sortType = request.getCustomMode(); @@ -264,7 +268,8 @@ public class RearrangePagesPDFController { return WebResponseUtils.pdfDocToWebResponse( rearrangedDocument, GeneralUtils.generateFilename( - pdfFile.getOriginalFilename(), "_rearranged.pdf")); + pdfFile.getOriginalFilename(), "_rearranged.pdf"), + tempFileManager); } } } catch (IOException e) { 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 9b305fdc5c..9049595cb2 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 @@ -9,6 +9,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -21,6 +22,7 @@ import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -28,6 +30,7 @@ import stirling.software.common.util.WebResponseUtils; public class RotationController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/rotate-pdf") @StandardPdfResponse @@ -36,7 +39,7 @@ public class RotationController { 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") - public ResponseEntity rotatePDF(@ModelAttribute RotatePDFRequest request) + public ResponseEntity rotatePDF(@ModelAttribute RotatePDFRequest request) throws IOException { MultipartFile pdfFile = request.getFileInput(); Integer angle = request.getAngle(); @@ -60,7 +63,8 @@ public class RotationController { // Return the rotated PDF as a response return WebResponseUtils.pdfDocToWebResponse( document, - GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_rotated.pdf")); + GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_rotated.pdf"), + tempFileManager); } } } 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 06053274cc..1cc77452d9 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 @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -16,6 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -28,6 +28,7 @@ import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -36,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils; public class ScalePagesController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; private static PDRectangle getTargetSize(String targetPDRectangle, PDDocument sourceDocument) { if ("KEEP".equals(targetPDRectangle)) { @@ -118,16 +120,15 @@ public class ScalePagesController { 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") - public ResponseEntity scalePages(@ModelAttribute ScalePagesRequest request) - throws IOException { + public ResponseEntity scalePages( + @ModelAttribute ScalePagesRequest request) throws IOException { MultipartFile file = request.getFileInput(); String targetPDRectangle = request.getPageSize(); float scaleFactor = request.getScaleFactor(); try (PDDocument sourceDocument = pdfDocumentFactory.load(file); PDDocument outputDocument = - pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument); - ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument)) { PDRectangle targetSize = getTargetSize(targetPDRectangle, sourceDocument); @@ -168,11 +169,10 @@ public class ScalePagesController { } } - outputDocument.save(baos); - - return WebResponseUtils.bytesToWebResponse( - baos.toByteArray(), - GeneralUtils.generateFilename(file.getOriginalFilename(), "_scaled.pdf")); + return WebResponseUtils.pdfDocToWebResponse( + outputDocument, + GeneralUtils.generateFilename(file.getOriginalFilename(), "_scaled.pdf"), + tempFileManager); } } } 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 784754b44c..e623cb6bdf 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 @@ -124,7 +124,9 @@ public class SplitPdfByChaptersController { @MultiFileResponse @Operation( summary = "Split PDFs by Chapters", - description = "Splits a PDF into chapters and returns a ZIP file.") + description = + "Splits a PDF into chapters and returns a ZIP file. Input:PDF Output:ZIP-PDF" + + " Type:SISO") 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 216c618c40..58c150aa37 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 @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; import java.util.*; @@ -20,6 +19,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -59,8 +59,8 @@ public class SplitPdfBySectionsController { + " which page to split, and how to split" + " ( halves, thirds, quarters, etc.), both vertically and horizontally." + " Input:PDF Output:ZIP-PDF Type:SISO") - public ResponseEntity splitPdf(@Valid @ModelAttribute SplitPdfBySectionsRequest request) - throws Exception { + public ResponseEntity splitPdf( + @Valid @ModelAttribute SplitPdfBySectionsRequest request) throws Exception { MultipartFile file = request.getFileInput(); String pageNumbers = request.getPageNumbers(); SplitTypes splitMode = @@ -80,9 +80,7 @@ public class SplitPdfBySectionsController { if (merge) { try (PDDocument mergedDoc = - pdfDocumentFactory.createNewDocumentBasedOnOldDocument( - sourceDocument); - ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument)) { LayerUtility layerUtility = new LayerUtility(mergedDoc); for (int pageIndex = 0; pageIndex < sourceDocument.getNumberOfPages(); @@ -99,11 +97,12 @@ public class SplitPdfBySectionsController { addPageToTarget(sourceDocument, pageIndex, mergedDoc, layerUtility); } } - mergedDoc.save(baos); - return WebResponseUtils.baosToWebResponse(baos, filename + ".pdf"); + return WebResponseUtils.pdfDocToWebResponse( + mergedDoc, filename + ".pdf", tempFileManager); } } else { - try (TempFile zipTempFile = new TempFile(tempFileManager, ".zip")) { + TempFile zipTempFile = tempFileManager.createManagedTempFile(".zip"); + try { try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipTempFile.getPath()))) { for (int pageIndex = 0; @@ -161,9 +160,10 @@ public class SplitPdfBySectionsController { log.error("Error creating ZIP file with split PDF sections", e); throw e; } - byte[] zipBytes = Files.readAllBytes(zipTempFile.getPath()); - return WebResponseUtils.bytesToWebResponse( - zipBytes, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.zipFileToWebResponse(zipTempFile, filename + ".zip"); + } catch (Exception ex) { + zipTempFile.close(); + throw ex; } } } catch (Exception e) { 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 2cf2517747..da6322a087 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 @@ -1,7 +1,6 @@ package stirling.software.SPDF.controller.api; import java.awt.geom.AffineTransform; -import java.io.ByteArrayOutputStream; import java.io.IOException; import org.apache.pdfbox.multipdf.LayerUtility; @@ -12,6 +11,7 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -23,6 +23,7 @@ import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -30,6 +31,7 @@ import stirling.software.common.util.WebResponseUtils; public class ToSinglePageController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping( consumes = MediaType.MULTIPART_FORM_DATA_VALUE, @@ -42,7 +44,7 @@ public class ToSinglePageController { + " 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") - public ResponseEntity pdfToSinglePage(@ModelAttribute PDFFile request) + public ResponseEntity pdfToSinglePage(@ModelAttribute PDFFile request) throws IOException { // Load the source document @@ -85,14 +87,11 @@ public class ToSinglePageController { pageIndex++; } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - newDocument.save(baos); - - byte[] result = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse( - result, + return WebResponseUtils.pdfDocToWebResponse( + newDocument, GeneralUtils.generateFilename( - request.getFileInput().getOriginalFilename(), "_singlePage.pdf")); + request.getFileInput().getOriginalFilename(), "_singlePage.pdf"), + tempFileManager); } } } 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 b9af74ddbf..c1afe8c406 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 @@ -95,9 +95,8 @@ public class UIDataController { Resource resource = new ClassPathResource("static/3rdPartyLicenses.json"); try (InputStream is = resource.getInputStream()) { - String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); Map> licenseData = - objectMapper.readValue(json, new TypeReference<>() {}); + objectMapper.readValue(is, new TypeReference<>() {}); data.setDependencies(licenseData.get("dependencies")); } catch (IOException e) { log.error("Failed to load licenses data", e); 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 50157aa970..136eba30ef 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 @@ -16,6 +16,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -31,6 +32,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @@ -60,7 +62,7 @@ public class ConvertEbookToPDFController { description = "This endpoint converts common eBook formats (EPUB, MOBI, AZW3, FB2, TXT, DOCX)" + " to PDF using Calibre. Input:BOOK Output:PDF Type:SISO") - public ResponseEntity convertEbookToPdf( + public ResponseEntity convertEbookToPdf( @ModelAttribute ConvertEbookToPdfRequest request) throws Exception { if (!isCalibreEnabled()) { throw new IllegalStateException("Calibre support is disabled"); @@ -140,24 +142,35 @@ public class ConvertEbookToPDFController { String outputFilename = GeneralUtils.generateFilename(originalFilename, "_convertedToPDF.pdf"); + TempFile tempOut = null; try { + tempOut = tempFileManager.createManagedTempFile(".pdf"); if (optimizeForEbook) { byte[] pdfBytes = Files.readAllBytes(outputPath); try { byte[] optimizedPdf = GeneralUtils.optimizePdfWithGhostscript(pdfBytes); - return WebResponseUtils.bytesToWebResponse(optimizedPdf, outputFilename); + Files.write(tempOut.getPath(), optimizedPdf); } catch (IOException e) { log.warn( "Ghostscript optimization failed for ebook conversion, returning" + " original PDF", e); - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); + Files.write(tempOut.getPath(), pdfBytes); + } + } else { + try (PDDocument document = pdfDocumentFactory.load(outputPath.toFile())) { + document.save(tempOut.getFile()); } } - - try (PDDocument document = pdfDocumentFactory.load(outputPath.toFile())) { - return WebResponseUtils.pdfDocToWebResponse(document, outputFilename); + ResponseEntity response = + WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename); + tempOut = null; + return response; + } catch (Exception e) { + if (tempOut != null) { + tempOut.close(); } + throw e; } finally { cleanupTempFiles(workingDirectory, inputPath, outputPath); } 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 5cf98556f3..42bbe6b020 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 @@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api.converters; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Locale; import org.jetbrains.annotations.NotNull; @@ -10,6 +11,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import org.springframework.web.util.HtmlUtils; import io.github.pixee.security.Filenames; @@ -26,6 +28,7 @@ import stirling.software.common.model.api.converters.EmlToPdfRequest; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.CustomHtmlSanitizer; import stirling.software.common.util.EmlToPdf; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @@ -48,7 +51,8 @@ public class ConvertEmlToPDF { + " 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") - public ResponseEntity convertEmlToPdf(@ModelAttribute EmlToPdfRequest request) { + public ResponseEntity convertEmlToPdf( + @ModelAttribute EmlToPdfRequest request) { MultipartFile inputFile = request.getFileInput(); String originalFilename = inputFile.getOriginalFilename(); @@ -56,22 +60,19 @@ public class ConvertEmlToPDF { // Validate input if (inputFile.isEmpty()) { log.error("No file provided for EML/MSG to PDF conversion."); - return ResponseEntity.badRequest() - .body("No file provided".getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.BAD_REQUEST, "No file provided"); } if (originalFilename == null || originalFilename.trim().isEmpty()) { log.error("Filename is null or empty."); - return ResponseEntity.badRequest() - .body("Please provide a valid filename".getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.BAD_REQUEST, "Please provide a valid filename"); } // Validate file type - support EML and MSG (Outlook) files String lowerFilename = originalFilename.toLowerCase(Locale.ROOT); if (!lowerFilename.endsWith(".eml") && !lowerFilename.endsWith(".msg")) { log.error("Invalid file type for EML/MSG to PDF: {}", originalFilename); - return ResponseEntity.badRequest() - .body("Please upload a valid EML or MSG file".getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.BAD_REQUEST, "Please upload a valid EML or MSG file"); } String baseFilename = Filenames.toSimpleFileName(originalFilename); // Use Filenames utility @@ -84,16 +85,20 @@ public class ConvertEmlToPDF { String htmlContent = EmlToPdf.convertEmlToHtml(fileBytes, request, customHtmlSanitizer); log.info("Successfully converted email to HTML: {}", originalFilename); - return WebResponseUtils.bytesToWebResponse( - htmlContent.getBytes(StandardCharsets.UTF_8), - baseFilename + ".html", - MediaType.TEXT_HTML); + TempFile tempOut = tempFileManager.createManagedTempFile(".html"); + try { + Files.writeString(tempOut.getPath(), htmlContent, StandardCharsets.UTF_8); + } catch (Exception ex) { + tempOut.close(); + throw ex; + } + return WebResponseUtils.fileToWebResponse( + tempOut, baseFilename + ".html", MediaType.TEXT_HTML); } catch (IOException | IllegalArgumentException e) { log.error("HTML conversion failed for {}", originalFilename, e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body( - ("HTML conversion failed: " + e.getMessage()) - .getBytes(StandardCharsets.UTF_8)); + return errorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + "HTML conversion failed: " + e.getMessage()); } } @@ -111,20 +116,25 @@ public class ConvertEmlToPDF { if (pdfBytes == null || pdfBytes.length == 0) { log.error("PDF conversion failed - empty output for {}", originalFilename); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body( - "PDF conversion failed - empty output" - .getBytes(StandardCharsets.UTF_8)); + return errorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + "PDF conversion failed - empty output"); } log.info("Successfully converted email to PDF: {}", originalFilename); - return WebResponseUtils.bytesToWebResponse( - pdfBytes, baseFilename + ".pdf", MediaType.APPLICATION_PDF); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), pdfBytes); + } catch (Exception ex) { + tempOut.close(); + throw ex; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, baseFilename + ".pdf"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("Email to PDF conversion was interrupted for {}", originalFilename, e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Conversion was interrupted".getBytes(StandardCharsets.UTF_8)); + return errorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, "Conversion was interrupted"); } catch (IllegalArgumentException e) { String errorMessage = buildErrorMessage(e, originalFilename); log.error( @@ -132,8 +142,7 @@ public class ConvertEmlToPDF { originalFilename, errorMessage, e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(errorMessage.getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, errorMessage); } catch (RuntimeException e) { String errorMessage = buildErrorMessage(e, originalFilename); log.error( @@ -141,17 +150,25 @@ public class ConvertEmlToPDF { originalFilename, errorMessage, e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(errorMessage.getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, errorMessage); } } catch (IOException e) { log.error("File processing error for email to PDF: {}", originalFilename, e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("File processing error".getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "File processing error"); } } + private ResponseEntity errorResponse(HttpStatus status, String message) { + byte[] body = message.getBytes(StandardCharsets.UTF_8); + StreamingResponseBody streaming = + os -> { + os.write(body); + os.flush(); + }; + return ResponseEntity.status(status).body(streaming); + } + private static @NotNull String buildErrorMessage(Exception e, String originalFilename) { String safeFilename = HtmlUtils.htmlEscape(originalFilename); String exceptionMessage = e.getMessage(); 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 85ef34313d..7d09eeb3b5 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 @@ -1,9 +1,12 @@ package stirling.software.SPDF.controller.api.converters; +import java.nio.file.Files; + import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -37,7 +40,7 @@ public class ConvertHtmlToPDF { description = "This endpoint takes an HTML or ZIP file input and converts it to a PDF format." + " Input:HTML Output:PDF Type:SISO") - public ResponseEntity HtmlToPdf(@ModelAttribute HTMLToPdfRequest request) + public ResponseEntity HtmlToPdf(@ModelAttribute HTMLToPdfRequest request) throws Exception { MultipartFile fileInput = request.getFileInput(); @@ -65,6 +68,13 @@ public class ConvertHtmlToPDF { String outputFilename = GeneralUtils.generateFilename(originalFilename, ".pdf"); - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), pdfBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename); } } 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 f96e17f95a..83384a21ed 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 @@ -1,5 +1,6 @@ package stirling.software.SPDF.controller.api.converters; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; @@ -14,6 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -46,8 +48,8 @@ public class ConvertMarkdownToPdf { 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") - public ResponseEntity markdownToPdf(@ModelAttribute GeneralFile generalFile) - throws Exception { + public ResponseEntity markdownToPdf( + @ModelAttribute GeneralFile generalFile) throws Exception { MultipartFile fileInput = generalFile.getFileInput(); if (fileInput == null) { @@ -79,7 +81,7 @@ public class ConvertMarkdownToPdf { java.nio.file.Path tempDirPath = tempDir.getPath(); try (java.util.zip.ZipInputStream zipIn = io.github.pixee.security.ZipSecurity.createHardenedInputStream( - new java.io.ByteArrayInputStream(fileInput.getBytes()))) { + fileInput.getInputStream())) { java.util.zip.ZipEntry entry; while ((entry = zipIn.getNextEntry()) != null) { if (!entry.isDirectory()) { @@ -141,7 +143,7 @@ public class ConvertMarkdownToPdf { List extensions = List.of(TablesExtension.create()); Parser parser = Parser.builder().extensions(extensions).build(); - Node document = parser.parse(new String(fileInput.getBytes())); + Node document = parser.parse(new String(fileInput.getBytes(), StandardCharsets.UTF_8)); HtmlRenderer renderer = HtmlRenderer.builder() .attributeProviderFactory(context -> new TableAttributeProvider()) @@ -154,7 +156,7 @@ public class ConvertMarkdownToPdf { FileToPdf.convertHtmlToPdf( runtimePathConfig.getWeasyPrintPath(), null, - htmlContent.getBytes(), + htmlContent.getBytes(StandardCharsets.UTF_8), "converted.html", tempFileManager, customHtmlSanitizer); @@ -163,7 +165,15 @@ public class ConvertMarkdownToPdf { } pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes); - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); + + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + java.nio.file.Files.write(tempOut.getPath(), pdfBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename); } /** 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 492febc496..ba4be7b8ea 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 @@ -17,6 +17,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -36,6 +37,8 @@ import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; import stirling.software.common.util.RegexPatternUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ConvertApi @@ -47,6 +50,7 @@ public class ConvertOfficeController { private final RuntimePathConfig runtimePathConfig; private final CustomHtmlSanitizer customHtmlSanitizer; private final EndpointConfiguration endpointConfiguration; + private final TempFileManager tempFileManager; private boolean isUnoconvertAvailable() { return endpointConfiguration.isGroupEnabled("Unoconvert") @@ -202,21 +206,32 @@ public class ConvertOfficeController { description = "This endpoint converts a given file to a PDF using LibreOffice API Input:ANY" + " Output:PDF Type:SISO") - public ResponseEntity processFileToPDF(@ModelAttribute GeneralFile generalFile) - throws Exception { + public ResponseEntity processFileToPDF( + @ModelAttribute GeneralFile generalFile) throws Exception { MultipartFile inputFile = generalFile.getFileInput(); // unused but can start server instance if startup time is to long // LibreOfficeListener.getInstance().start(); File file = null; + TempFile tempOut = null; try { file = convertToPdf(inputFile); + tempOut = tempFileManager.createManagedTempFile(".pdf"); try (PDDocument doc = pdfDocumentFactory.load(file)) { - return WebResponseUtils.pdfDocToWebResponse( - doc, - GeneralUtils.generateFilename( - inputFile.getOriginalFilename(), "_convertedToPDF.pdf")); + doc.save(tempOut.getFile()); } + String filename = + GeneralUtils.generateFilename( + inputFile.getOriginalFilename(), "_convertedToPDF.pdf"); + ResponseEntity response = + WebResponseUtils.pdfFileToWebResponse(tempOut, filename); + tempOut = null; + return response; + } catch (Exception e) { + if (tempOut != null) { + tempOut.close(); + } + throw e; } finally { if (file != null && file.getParent() != null) { FileUtils.deleteDirectory(file.getParentFile()); 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 61faf63677..3fd8c46dba 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 @@ -13,6 +13,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -29,6 +30,7 @@ import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @@ -85,8 +87,8 @@ public class ConvertPDFToEpubController { description = "Convert a PDF file to a high-quality EPUB or AZW3 ebook using Calibre. Input:PDF" + " Output:EPUB/AZW3 Type:SISO") - public ResponseEntity convertPdfToEpub(@ModelAttribute ConvertPdfToEpubRequest request) - throws Exception { + public ResponseEntity convertPdfToEpub( + @ModelAttribute ConvertPdfToEpubRequest request) throws Exception { if (!endpointConfiguration.isGroupEnabled(CALIBRE_GROUP)) { throw new IllegalStateException( @@ -170,9 +172,16 @@ public class ConvertPDFToEpubController { + "." + outputFormat.getExtension()); - byte[] outputBytes = Files.readAllBytes(outputPath); MediaType mediaType = MediaType.valueOf(outputFormat.getMediaType()); - return WebResponseUtils.bytesToWebResponse(outputBytes, outputFilename, mediaType); + TempFile tempOut = + tempFileManager.createManagedTempFile("." + outputFormat.getExtension()); + try { + Files.copy(outputPath, tempOut.getPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.fileToWebResponse(tempOut, outputFilename, mediaType); } finally { cleanupTempFiles(workingDirectory, inputPath, outputPath); } 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 bafaf86310..974e5451f7 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 @@ -1,6 +1,7 @@ package stirling.software.SPDF.controller.api.converters; -import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.nio.file.Files; import java.util.List; import java.util.Locale; @@ -11,11 +12,10 @@ import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.WorkbookUtil; import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import org.springframework.http.ContentDisposition; -import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -27,6 +27,9 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; +import stirling.software.common.util.WebResponseUtils; import technology.tabula.ObjectExtractor; import technology.tabula.Page; @@ -40,6 +43,7 @@ import technology.tabula.extractors.SpreadsheetExtractionAlgorithm; public class ConvertPDFToExcelController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(value = "/pdf/xlsx", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation( @@ -47,11 +51,12 @@ public class ConvertPDFToExcelController { 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") - public ResponseEntity pdfToExcel(@ModelAttribute PDFWithPageNums request) + public ResponseEntity pdfToExcel(@ModelAttribute PDFWithPageNums request) throws Exception { String baseName = GeneralUtils.removeExtension(request.getFileInput().getOriginalFilename()); + TempFile tempOut = tempFileManager.createManagedTempFile(".xlsx"); try (PDDocument document = pdfDocumentFactory.load(request); XSSFWorkbook workbook = new XSSFWorkbook(); ObjectExtractor extractor = new ObjectExtractor(document)) { @@ -89,21 +94,22 @@ public class ConvertPDFToExcelController { } if (sheetCount == 0) { + tempOut.close(); return ResponseEntity.noContent().build(); } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - workbook.write(baos); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentDisposition( - ContentDisposition.builder("attachment").filename(baseName + ".xlsx").build()); - headers.setContentType( - MediaType.parseMediaType( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")); - - return ResponseEntity.ok().headers(headers).body(baos.toByteArray()); + try (OutputStream os = Files.newOutputStream(tempOut.getPath())) { + workbook.write(os); + } + } catch (Exception e) { + tempOut.close(); + throw e; } + + MediaType mediaType = + MediaType.parseMediaType( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + return WebResponseUtils.fileToWebResponse(tempOut, baseName + ".xlsx", mediaType); } private String getUniqueSheetName(Workbook workbook, 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 849e0af84b..13558d61aa 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 @@ -4,6 +4,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -28,7 +29,8 @@ public class ConvertPDFToHtml { summary = "Convert PDF to HTML", description = "This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO") - public ResponseEntity processPdfToHTML(@ModelAttribute PDFFile file) throws Exception { + public ResponseEntity processPdfToHTML(@ModelAttribute PDFFile file) + throws Exception { MultipartFile inputFile = file.getFileInput(); PDFToFile pdfToFile = new PDFToFile(tempFileManager, runtimePathConfig); return pdfToFile.processPdfToHtml(inputFile); 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 950a9600d6..391ce52398 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 @@ -1,6 +1,8 @@ package stirling.software.SPDF.controller.api.converters; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; @@ -8,6 +10,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -23,6 +26,7 @@ import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PDFToFile; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @@ -40,7 +44,7 @@ public class ConvertPDFToOffice { description = "This endpoint converts a given PDF file to a Presentation format. Input:PDF" + " Output:PPT Type:SISO") - public ResponseEntity processPdfToPresentation( + public ResponseEntity processPdfToPresentation( @ModelAttribute PdfToPresentationRequest request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); @@ -55,20 +59,24 @@ public class ConvertPDFToOffice { description = "This endpoint converts a given PDF file to Text or RTF format. Input:PDF" + " Output:TXT Type:SISO") - public ResponseEntity processPdfToRTForTXT( + public ResponseEntity processPdfToRTForTXT( @ModelAttribute PdfToTextOrRTFRequest request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); String outputFormat = request.getOutputFormat(); if ("txt".equals(request.getOutputFormat())) { + String fileName = + GeneralUtils.generateFilename(inputFile.getOriginalFilename(), ".txt"); + TempFile finalOut = tempFileManager.createManagedTempFile(".txt"); try (PDDocument document = pdfDocumentFactory.load(inputFile)) { PDFTextStripper stripper = new PDFTextStripper(); String text = stripper.getText(document); - return WebResponseUtils.bytesToWebResponse( - text.getBytes(), - GeneralUtils.generateFilename(inputFile.getOriginalFilename(), ".txt"), - MediaType.TEXT_PLAIN); + Files.writeString(finalOut.getPath(), text, StandardCharsets.UTF_8); + } catch (Exception e) { + finalOut.close(); + throw e; } + return WebResponseUtils.fileToWebResponse(finalOut, fileName, MediaType.TEXT_PLAIN); } else { PDFToFile pdfToFile = new PDFToFile(tempFileManager, runtimePathConfig); return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); @@ -81,8 +89,8 @@ public class ConvertPDFToOffice { description = "This endpoint converts a given PDF file to a Word document format. Input:PDF" + " Output:WORD Type:SISO") - public ResponseEntity processPdfToWord(@ModelAttribute PdfToWordRequest request) - throws IOException, InterruptedException { + public ResponseEntity processPdfToWord( + @ModelAttribute PdfToWordRequest request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); String outputFormat = request.getOutputFormat(); PDFToFile pdfToFile = new PDFToFile(tempFileManager, runtimePathConfig); @@ -95,7 +103,8 @@ public class ConvertPDFToOffice { description = "This endpoint converts a PDF file to an XML file. Input:PDF Output:XML" + " Type:SISO") - public ResponseEntity processPdfToXML(@ModelAttribute PDFFile file) throws Exception { + public ResponseEntity processPdfToXML(@ModelAttribute PDFFile file) + throws Exception { MultipartFile inputFile = file.getFileInput(); PDFToFile pdfToFile = new PDFToFile(tempFileManager, runtimePathConfig); 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 d544d6ddab..98706bc98f 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 @@ -77,6 +77,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -92,6 +93,8 @@ import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ConvertApi @@ -102,6 +105,7 @@ 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; + private final TempFileManager tempFileManager; private static final String ICC_RESOURCE_PATH = "/icc/sRGB2014.icc"; private static final int PDFA_COMPATIBILITY_POLICY = 1; @@ -573,7 +577,7 @@ public class ConvertPDFToPDFA { 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") - public ResponseEntity pdfToPdfA(@ModelAttribute PdfToPdfARequest request) + public ResponseEntity pdfToPdfA(@ModelAttribute PdfToPdfARequest request) throws Exception { MultipartFile inputFile = request.getFileInput(); String outputFormat = request.getOutputFormat(); @@ -609,7 +613,7 @@ public class ConvertPDFToPDFA { return missing; } - private ResponseEntity handlePdfXConversion( + private ResponseEntity handlePdfXConversion( MultipartFile inputFile, String outputFormat) throws Exception { PdfXProfile profile = PdfXProfile.fromRequest(outputFormat); @@ -640,8 +644,14 @@ public class ConvertPDFToPDFA { log.info("PDF/X conversion completed successfully to {}", profile.getDisplayName()); - return WebResponseUtils.bytesToWebResponse( - converted, outputFilename, MediaType.APPLICATION_PDF); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), converted); + } catch (Exception ex) { + tempOut.close(); + throw ex; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename); } catch (IOException | InterruptedException e) { log.error("PDF/X conversion failed", e); @@ -1796,7 +1806,7 @@ public class ConvertPDFToPDFA { return Files.readAllBytes(outputPdf); } - private ResponseEntity handlePdfAConversion( + private ResponseEntity handlePdfAConversion( MultipartFile inputFile, String outputFormat, boolean strict) throws Exception { PdfaProfile profile = PdfaProfile.fromRequest(outputFormat); @@ -1830,8 +1840,14 @@ public class ConvertPDFToPDFA { verifyStrictCompliance(converted); } - return WebResponseUtils.bytesToWebResponse( - converted, outputFilename, MediaType.APPLICATION_PDF); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), converted); + } catch (Exception ex) { + tempOut.close(); + throw ex; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename); } catch (IOException | InterruptedException e) { log.warn( "Ghostscript conversion failed, falling back to PDFBox/LibreOffice method", @@ -1851,8 +1867,14 @@ public class ConvertPDFToPDFA { verifyStrictCompliance(converted); } - return WebResponseUtils.bytesToWebResponse( - converted, outputFilename, MediaType.APPLICATION_PDF); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), converted); + } catch (Exception ex) { + tempOut.close(); + throw ex; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename); } finally { deleteQuietly(workingDir); } 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 2ab4a32798..7b7924ba3c 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 @@ -1,6 +1,7 @@ package stirling.software.SPDF.controller.api.converters; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Optional; import java.util.UUID; import java.util.regex.Pattern; @@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -31,6 +33,8 @@ import stirling.software.common.model.api.GeneralFile; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.JobOwnershipService; import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @Slf4j @@ -42,6 +46,7 @@ public class ConvertPdfJsonController { private static final Pattern WHITESPACE_PATTERN = Pattern.compile("[\\r\\n\\t]+"); private static final Pattern NON_PRINTABLE_PATTERN = Pattern.compile("[^\\x20-\\x7E]"); private final PdfJsonConversionService pdfJsonConversionService; + private final TempFileManager tempFileManager; @Autowired(required = false) private JobOwnershipService jobOwnershipService; @@ -51,7 +56,7 @@ public class ConvertPdfJsonController { 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") - public ResponseEntity convertPdfToJson( + public ResponseEntity convertPdfToJson( @ModelAttribute PDFFile request, @RequestParam(value = "lightweight", defaultValue = "false") boolean lightweight) throws Exception { @@ -60,6 +65,8 @@ public class ConvertPdfJsonController { throw ExceptionUtils.createNullArgumentException("fileInput"); } + // TODO: Refactor PdfJsonConversionService to write directly to an OutputStream + // instead of returning byte[], avoiding the intermediate heap allocation + temp file write byte[] jsonBytes = pdfJsonConversionService.convertPdfToJson(inputFile, lightweight); logJsonResponse("pdf/text-editor", jsonBytes); String originalName = inputFile.getOriginalFilename(); @@ -70,7 +77,14 @@ public class ConvertPdfJsonController { .replaceFirst("") : "document"; String docName = baseName + ".json"; - return WebResponseUtils.bytesToWebResponse(jsonBytes, docName, MediaType.APPLICATION_JSON); + TempFile tempOut = tempFileManager.createManagedTempFile(".json"); + try { + Files.write(tempOut.getPath(), jsonBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.fileToWebResponse(tempOut, docName, MediaType.APPLICATION_JSON); } @AutoJobPostMapping(consumes = "multipart/form-data", value = "/text-editor/pdf") @@ -79,8 +93,8 @@ public class ConvertPdfJsonController { 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") - public ResponseEntity convertJsonToPdf(@ModelAttribute GeneralFile request) - throws Exception { + public ResponseEntity convertJsonToPdf( + @ModelAttribute GeneralFile request) throws Exception { MultipartFile jsonFile = request.getFileInput(); if (jsonFile == null) { throw ExceptionUtils.createNullArgumentException("fileInput"); @@ -95,7 +109,14 @@ public class ConvertPdfJsonController { .replaceFirst("") : "document"; String docName = baseName.endsWith(".pdf") ? baseName : baseName + ".pdf"; - return WebResponseUtils.bytesToWebResponse(pdfBytes, docName); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), pdfBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, docName); } @AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/text-editor/metadata") @@ -105,17 +126,15 @@ public class ConvertPdfJsonController { "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") - public ResponseEntity extractPdfMetadata(@ModelAttribute PDFFile request) + public ResponseEntity extractPdfMetadata(@ModelAttribute PDFFile request) throws Exception { MultipartFile inputFile = request.getFileInput(); if (inputFile == null) { throw ExceptionUtils.createNullArgumentException("fileInput"); } - // Generate server-side UUID for job String baseJobId = UUID.randomUUID().toString(); - // Scope job to authenticated user if security is enabled String scopedJobKey = getScopedJobKey(baseJobId); log.debug("Extracting metadata for PDF, assigned jobId: {}", scopedJobKey); @@ -123,20 +142,27 @@ public class ConvertPdfJsonController { byte[] jsonBytes = pdfJsonConversionService.extractDocumentMetadata(inputFile, scopedJobKey); logJsonResponse("pdf/text-editor/metadata", jsonBytes); - String originalName = inputFile.getOriginalFilename(); - String baseName = - (originalName != null && !originalName.isBlank()) - ? FILE_EXTENSION_PATTERN - .matcher(Filenames.toSimpleFileName(originalName)) - .replaceFirst("") - : "document"; - String docName = baseName + "_metadata.json"; - // Return jobId in response header for client + TempFile tempOut = tempFileManager.createManagedTempFile(".json"); + try { + Files.write(tempOut.getPath(), jsonBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } return ResponseEntity.ok() .header("X-Job-Id", scopedJobKey) .contentType(MediaType.APPLICATION_JSON) - .body(jsonBytes); + .contentLength(java.nio.file.Files.size(tempOut.getPath())) + .body( + os -> { + try (os) { + Files.copy(tempOut.getPath(), os); + os.flush(); + } finally { + tempOut.close(); + } + }); } @AutoJobPostMapping( @@ -149,7 +175,7 @@ public class ConvertPdfJsonController { "Applies edits for the specified pages of a cached PDF and returns an updated PDF." + " Requires the PDF to have been previously cached via the text editor metadata endpoint." + " The jobId must be obtained from the metadata extraction endpoint.") - public ResponseEntity exportPartialPdf( + public ResponseEntity exportPartialPdf( @PathVariable String jobId, @RequestBody PdfJsonDocument document, @RequestParam(value = "filename", required = false) String filename) @@ -158,7 +184,6 @@ public class ConvertPdfJsonController { throw ExceptionUtils.createNullArgumentException("document"); } - // Validate job ownership validateJobAccess(jobId); byte[] pdfBytes = pdfJsonConversionService.exportUpdatedPages(jobId, document); @@ -173,7 +198,14 @@ public class ConvertPdfJsonController { .filter(title -> title != null && !title.isBlank()) .orElse("document"); String docName = baseName.endsWith(".pdf") ? baseName : baseName + ".pdf"; - return WebResponseUtils.bytesToWebResponse(pdfBytes, docName); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), pdfBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, docName); } @GetMapping(value = "/pdf/text-editor/page/{jobId}/{pageNumber}") @@ -183,16 +215,22 @@ public class ConvertPdfJsonController { "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") - public ResponseEntity extractSinglePage( + public ResponseEntity extractSinglePage( @PathVariable String jobId, @PathVariable int pageNumber) throws Exception { - // Validate job ownership validateJobAccess(jobId); byte[] jsonBytes = pdfJsonConversionService.extractSinglePage(jobId, pageNumber); logJsonResponse("pdf/text-editor/page", jsonBytes); String docName = "page_" + pageNumber + ".json"; - return WebResponseUtils.bytesToWebResponse(jsonBytes, docName, MediaType.APPLICATION_JSON); + TempFile tempOut = tempFileManager.createManagedTempFile(".json"); + try { + Files.write(tempOut.getPath(), jsonBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.fileToWebResponse(tempOut, docName, MediaType.APPLICATION_JSON); } @GetMapping(value = "/pdf/text-editor/fonts/{jobId}/{pageNumber}") @@ -202,16 +240,22 @@ public class ConvertPdfJsonController { "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") - public ResponseEntity extractPageFonts( + public ResponseEntity extractPageFonts( @PathVariable String jobId, @PathVariable int pageNumber) throws Exception { - // Validate job ownership validateJobAccess(jobId); byte[] jsonBytes = pdfJsonConversionService.extractPageFonts(jobId, pageNumber); logJsonResponse("pdf/text-editor/fonts/page", jsonBytes); String docName = "page_fonts_" + pageNumber + ".json"; - return WebResponseUtils.bytesToWebResponse(jsonBytes, docName, MediaType.APPLICATION_JSON); + TempFile tempOut = tempFileManager.createManagedTempFile(".json"); + try { + Files.write(tempOut.getPath(), jsonBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.fileToWebResponse(tempOut, docName, MediaType.APPLICATION_JSON); } @AutoJobPostMapping( @@ -225,24 +269,16 @@ public class ConvertPdfJsonController { + " authenticated user.") public ResponseEntity clearCache(@PathVariable String jobId) { - // Validate job ownership validateJobAccess(jobId); pdfJsonConversionService.clearCachedDocument(jobId); return ResponseEntity.ok().build(); } - /** - * Get a scoped job key that includes user ownership when security is enabled. - * - * @param baseJobId the base job identifier - * @return scoped job key, or just baseJobId if no ownership service available - */ private String getScopedJobKey(String baseJobId) { if (jobOwnershipService != null) { return jobOwnershipService.createScopedJobKey(baseJobId); } - // Security disabled, return unsecured job key return baseJobId; } @@ -252,7 +288,6 @@ public class ConvertPdfJsonController { return; } - // Only perform expensive tail extraction if debug logging is enabled if (log.isDebugEnabled()) { int length = jsonBytes.length; boolean endsWithJson = @@ -431,16 +466,9 @@ public class ConvertPdfJsonController { return WHITESPACE_PATTERN.matcher(value.substring(0, max)).replaceAll(" ") + "..."; } - /** - * Validate that the current user has access to the given job. - * - * @param jobId the job identifier to validate - * @throws SecurityException if current user does not own the job - */ private void validateJobAccess(String jobId) { if (jobOwnershipService != null) { jobOwnershipService.validateJobAccess(jobId); } - // If jobOwnershipService is null (security disabled), allow all access } } 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 56780fa901..001e09305e 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 @@ -8,10 +8,7 @@ import java.awt.Graphics2D; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; -import java.text.DecimalFormat; -import java.text.DecimalFormatSymbols; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -23,31 +20,17 @@ import javax.imageio.ImageIO; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.PDFRenderer; -import org.springframework.http.HttpStatus; 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 stirling.software.SPDF.model.api.converters.PdfToVideoRequest; -import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ApplicationContextProvider; -import stirling.software.common.util.CheckProgramInstall; import stirling.software.common.util.ExceptionUtils; -import stirling.software.common.util.ProcessExecutor; -import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; -import stirling.software.common.util.TempDirectory; import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; -import stirling.software.common.util.WebResponseUtils; @ConvertApi @RequiredArgsConstructor @@ -64,6 +47,8 @@ public class ConvertPdfToVideoController { private final CustomPDFDocumentFactory pdfDocumentFactory; private final TempFileManager tempFileManager; + // ffmpeg disabled due to raised CVEs + /* @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/video") @Operation( summary = "Convert PDF to Video Slideshow", @@ -163,6 +148,7 @@ public class ConvertPdfToVideoController { return WebResponseUtils.bytesToWebResponse(videoBytes, outputName, mediaType); } } + */ private void generateFrames( Path inputPdf, 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 0e90f01f53..dc79805236 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 @@ -14,6 +14,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -53,7 +54,8 @@ public class ConvertSvgToPDF { + "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") - public ResponseEntity convertSvgToPdf(@ModelAttribute SvgToPdfRequest request) { + public ResponseEntity convertSvgToPdf( + @ModelAttribute SvgToPdfRequest request) { MultipartFile[] inputFiles = request.getFileInput(); boolean combineIntoSinglePdf = Boolean.TRUE.equals(request.getCombineIntoSinglePdf()); @@ -61,8 +63,7 @@ public class ConvertSvgToPDF { // Validate input if (inputFiles == null || inputFiles.length == 0) { log.error("No files provided for SVG to PDF conversion."); - return ResponseEntity.badRequest() - .body("No files provided".getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.BAD_REQUEST, "No files provided"); } try { @@ -103,8 +104,7 @@ public class ConvertSvgToPDF { if (sanitizedSvgs.isEmpty()) { log.error("No valid SVG files were found"); - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body("No valid SVG files were found".getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.BAD_REQUEST, "No valid SVG files were found"); } if (combineIntoSinglePdf) { @@ -115,14 +115,23 @@ public class ConvertSvgToPDF { } catch (Exception e) { log.error("Unexpected error during SVG to PDF conversion", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body( - "An unexpected error occurred during conversion" - .getBytes(StandardCharsets.UTF_8)); + return errorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + "An unexpected error occurred during conversion"); } } - private ResponseEntity handleCombinedConversion( + private ResponseEntity errorResponse(HttpStatus status, String message) { + byte[] body = message.getBytes(StandardCharsets.UTF_8); + StreamingResponseBody streaming = + os -> { + os.write(body); + os.flush(); + }; + return ResponseEntity.status(status).body(streaming); + } + + private ResponseEntity handleCombinedConversion( List sanitizedSvgs, List filenames) { try { log.info("Combining {} SVG files into single PDF", sanitizedSvgs.size()); @@ -131,10 +140,8 @@ public class ConvertSvgToPDF { if (pdfBytes == null || pdfBytes.length == 0) { log.error("PDF conversion failed - empty output"); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body( - "PDF conversion failed - empty output" - .getBytes(StandardCharsets.UTF_8)); + return errorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, "PDF conversion failed - empty output"); } pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes); @@ -146,19 +153,23 @@ public class ConvertSvgToPDF { log.info("Successfully combined {} SVGs into single PDF", sanitizedSvgs.size()); - return WebResponseUtils.bytesToWebResponse( - pdfBytes, outputFilename, MediaType.APPLICATION_PDF); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), pdfBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename); } catch (IOException e) { log.error("Error combining SVGs into PDF", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body( - ("Conversion failed: " + e.getMessage()) - .getBytes(StandardCharsets.UTF_8)); + return errorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, "Conversion failed: " + e.getMessage()); } } - private ResponseEntity handleSeparateConversion( + private ResponseEntity handleSeparateConversion( List sanitizedSvgs, List filenames) { List convertedPdfs = new ArrayList<>(); @@ -188,15 +199,21 @@ public class ConvertSvgToPDF { if (convertedPdfs.isEmpty()) { log.error("No files were successfully converted"); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("No files were successfully converted".getBytes(StandardCharsets.UTF_8)); + return errorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, "No files were successfully converted"); } try { if (convertedPdfs.size() == 1) { ConvertedPdf pdf = convertedPdfs.get(0); - return WebResponseUtils.bytesToWebResponse( - pdf.content, pdf.filename, MediaType.APPLICATION_PDF); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), pdf.content); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, pdf.filename); } String zipFilename = @@ -204,22 +221,18 @@ public class ConvertSvgToPDF { ? "converted_svgs.zip" : GeneralUtils.generateFilename( filenames.get(0), "_converted_svgs.zip"); - byte[] zipBytes = createZipFromPdfs(convertedPdfs); - - return WebResponseUtils.bytesToWebResponse( - zipBytes, zipFilename, MediaType.APPLICATION_OCTET_STREAM); + TempFile zipFile = createZipFromPdfs(convertedPdfs); + return WebResponseUtils.zipFileToWebResponse(zipFile, zipFilename); } catch (IOException e) { log.error("Failed to create response", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Failed to create response".getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to create response"); } } - private byte[] createZipFromPdfs(List pdfs) throws IOException { - try (TempFile tempZipFile = new TempFile(tempFileManager, ".zip"); - ZipOutputStream zipOut = - new ZipOutputStream(Files.newOutputStream(tempZipFile.getPath()))) { - + private TempFile createZipFromPdfs(List pdfs) throws IOException { + TempFile tempZipFile = tempFileManager.createManagedTempFile(".zip"); + try (ZipOutputStream zipOut = + new ZipOutputStream(Files.newOutputStream(tempZipFile.getPath()))) { for (ConvertedPdf pdf : pdfs) { ZipEntry pdfEntry = new ZipEntry(pdf.filename); zipOut.putNextEntry(pdfEntry); @@ -227,9 +240,11 @@ public class ConvertSvgToPDF { zipOut.closeEntry(); log.debug("Added {} to ZIP", pdf.filename); } - - return Files.readAllBytes(tempZipFile.getPath()); + } catch (IOException e) { + tempZipFile.close(); + throw e; } + return tempZipFile; } private static class ConvertedPdf { 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 00f5762858..1fa927c113 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 @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api.converters; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -39,6 +38,8 @@ import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.RegexPatternUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ConvertApi @@ -49,6 +50,7 @@ public class ConvertWebsiteToPDF { private final CustomPDFDocumentFactory pdfDocumentFactory; private final RuntimePathConfig runtimePathConfig; private final ApplicationProperties applicationProperties; + private final TempFileManager tempFileManager; private static final Pattern FILE_SCHEME_PATTERN = Pattern.compile("(? createZipResponse(List entries, String baseName) - throws IOException { + throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ZipOutputStream zipOut = new ZipOutputStream(baos)) { for (CsvEntry entry : entries) { @@ -101,14 +101,10 @@ public class ExtractCSVController { } } - HttpHeaders headers = new HttpHeaders(); - headers.setContentDisposition( - ContentDisposition.builder("attachment") - .filename(baseName + "_extracted.zip") - .build()); - headers.setContentType(MediaType.parseMediaType("application/zip")); - - return ResponseEntity.ok().headers(headers).body(baos.toByteArray()); + return WebResponseUtils.bytesToWebResponse( + baos.toByteArray(), + baseName + "_extracted.zip", + MediaType.APPLICATION_OCTET_STREAM); } private ResponseEntity createCsvResponse(CsvEntry entry, String 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 ce5549c9f4..fcee92f025 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 @@ -13,6 +13,7 @@ import org.apache.commons.io.FilenameUtils; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -38,7 +39,6 @@ import stirling.software.common.util.WebResponseUtils; @RequiredArgsConstructor public class PdfVectorExportController { - private static final MediaType PDF_MEDIA_TYPE = MediaType.APPLICATION_PDF; private static final Set GHOSTSCRIPT_INPUTS = Set.of("ps", "eps", "epsf"); // PCL/PXL/XPS require GhostPDL (gpcl6/gxps) @@ -51,7 +51,7 @@ public class PdfVectorExportController { description = "Converts PostScript vector inputs (PS, EPS, EPSF) to PDF using Ghostscript." + " Input:PS/EPS Output:PDF Type:SISO") - public ResponseEntity convertGhostscriptInputsToPdf( + public ResponseEntity convertGhostscriptInputsToPdf( @Valid @ModelAttribute PdfVectorExportRequest request) throws Exception { String originalName = @@ -63,9 +63,9 @@ public class PdfVectorExportController { ? FilenameUtils.getExtension(originalName).toLowerCase(Locale.ROOT) : ""; + TempFile outputTemp = tempFileManager.createManagedTempFile(".pdf"); try (TempFile inputTemp = - new TempFile(tempFileManager, extension.isEmpty() ? "" : "." + extension); - TempFile outputTemp = new TempFile(tempFileManager, ".pdf")) { + new TempFile(tempFileManager, extension.isEmpty() ? "" : "." + extension)) { request.getFileInput().transferTo(inputTemp.getFile()); @@ -83,11 +83,13 @@ public class PdfVectorExportController { "Unsupported Ghostscript input format {0}", extension); } - - byte[] pdfBytes = Files.readAllBytes(outputTemp.getPath()); - String outputName = GeneralUtils.generateFilename(originalName, "_converted.pdf"); - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputName, PDF_MEDIA_TYPE); + } catch (Exception e) { + outputTemp.close(); + throw e; } + + String outputName = GeneralUtils.generateFilename(originalName, "_converted.pdf"); + return WebResponseUtils.pdfFileToWebResponse(outputTemp, outputName); } @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/vector") @@ -96,7 +98,7 @@ public class PdfVectorExportController { description = "Converts PDF to Ghostscript vector formats (EPS, PS, PCL, or XPS)." + " Input:PDF Output:VECTOR Type:SISO") - public ResponseEntity convertPdfToVector( + public ResponseEntity convertPdfToVector( @Valid @ModelAttribute PdfVectorExportRequest request) throws Exception { String originalName = @@ -110,35 +112,37 @@ public class PdfVectorExportController { } outputFormat = outputFormat.toLowerCase(Locale.ROOT); - try (TempFile inputTemp = new TempFile(tempFileManager, ".pdf"); - TempFile outputTemp = new TempFile(tempFileManager, "." + outputFormat)) { + TempFile outputTemp = tempFileManager.createManagedTempFile("." + outputFormat); + try (TempFile inputTemp = new TempFile(tempFileManager, ".pdf")) { request.getFileInput().transferTo(inputTemp.getFile()); runGhostscriptPdfToVector(inputTemp.getPath(), outputTemp.getPath(), outputFormat); - - byte[] vectorBytes = Files.readAllBytes(outputTemp.getPath()); - String outputName = - GeneralUtils.generateFilename(originalName, "_converted." + outputFormat); - - MediaType mediaType; - switch (outputFormat.toLowerCase(Locale.ROOT)) { - case "eps": - case "ps": - mediaType = MediaType.parseMediaType("application/postscript"); - break; - case "pcl": - mediaType = MediaType.parseMediaType("application/vnd.hp-PCL"); - break; - case "xps": - mediaType = MediaType.parseMediaType("application/vnd.ms-xpsdocument"); - break; - default: - mediaType = MediaType.APPLICATION_OCTET_STREAM; - } - - return WebResponseUtils.bytesToWebResponse(vectorBytes, outputName, mediaType); + } catch (Exception e) { + outputTemp.close(); + throw e; } + + String outputName = + GeneralUtils.generateFilename(originalName, "_converted." + outputFormat); + + MediaType mediaType; + switch (outputFormat.toLowerCase(Locale.ROOT)) { + case "eps": + case "ps": + mediaType = MediaType.parseMediaType("application/postscript"); + break; + case "pcl": + mediaType = MediaType.parseMediaType("application/vnd.hp-PCL"); + break; + case "xps": + mediaType = MediaType.parseMediaType("application/vnd.ms-xpsdocument"); + break; + default: + mediaType = MediaType.APPLICATION_OCTET_STREAM; + } + + return WebResponseUtils.fileToWebResponse(outputTemp, outputName, mediaType); } private void runGhostscriptPdfToVector(Path inputPath, Path outputPath, String outputFormat) 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 795151970a..514d028c6b 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 @@ -9,6 +9,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -29,6 +30,7 @@ import stirling.software.common.annotations.api.FilterApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.PdfUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @FilterApi @@ -36,6 +38,7 @@ import stirling.software.common.util.WebResponseUtils; public class FilterController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping( consumes = MediaType.MULTIPART_FORM_DATA_VALUE, @@ -53,8 +56,8 @@ public class FilterController { description = "PDF did not pass filter", content = @Content()) }) - public ResponseEntity containsText(@ModelAttribute ContainsTextRequest request) - throws IOException, InterruptedException { + public ResponseEntity containsText( + @ModelAttribute ContainsTextRequest request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); String text = request.getText(); String pageNumber = request.getPageNumbers(); @@ -62,7 +65,9 @@ public class FilterController { try (PDDocument pdfDocument = pdfDocumentFactory.load(inputFile)) { if (PdfUtils.hasText(pdfDocument, pageNumber, text)) { return WebResponseUtils.pdfDocToWebResponse( - pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename())); + pdfDocument, + Filenames.toSimpleFileName(inputFile.getOriginalFilename()), + tempFileManager); } } return ResponseEntity.noContent().build(); @@ -84,15 +89,17 @@ public class FilterController { description = "PDF did not pass filter", content = @Content()) }) - public ResponseEntity containsImage(@ModelAttribute PDFWithPageNums request) - throws IOException, InterruptedException { + public ResponseEntity containsImage( + @ModelAttribute PDFWithPageNums request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); String pageNumber = request.getPageNumbers(); try (PDDocument pdfDocument = pdfDocumentFactory.load(inputFile)) { if (PdfUtils.hasImages(pdfDocument, pageNumber)) { return WebResponseUtils.pdfDocToWebResponse( - pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename())); + pdfDocument, + Filenames.toSimpleFileName(inputFile.getOriginalFilename()), + tempFileManager); } } return ResponseEntity.noContent().build(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/form/FormFillController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/form/FormFillController.java index 5c96c7030a..8692b04bcc 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/form/FormFillController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/form/FormFillController.java @@ -18,6 +18,7 @@ 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.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import com.opencsv.CSVWriter; @@ -34,6 +35,7 @@ import stirling.software.common.model.FormFieldWithCoordinates; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.FormUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; import tools.jackson.core.type.TypeReference; @@ -59,12 +61,11 @@ public class FormFillController { private final CustomPDFDocumentFactory pdfDocumentFactory; private final ObjectMapper objectMapper; + private final TempFileManager tempFileManager; - private static ResponseEntity saveDocument(PDDocument document, String baseName) + private ResponseEntity saveDocument(PDDocument document, String baseName) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); - return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), baseName + ".pdf"); + return WebResponseUtils.pdfDocToWebResponse(document, baseName + ".pdf", tempFileManager); } private static String buildBaseName(MultipartFile file, String suffix) { @@ -261,7 +262,7 @@ public class FormFillController { summary = "Modify existing form fields", description = "Updates existing fields in the provided PDF and returns the updated file") - public ResponseEntity modifyFields( + public ResponseEntity modifyFields( @Parameter( description = "The input PDF file", required = true, @@ -292,7 +293,7 @@ public class FormFillController { @Operation( summary = "Delete form fields", description = "Removes the specified fields from the PDF and returns the updated file") - public ResponseEntity deleteFields( + public ResponseEntity deleteFields( @Parameter( description = "The input PDF file", required = true, @@ -328,7 +329,7 @@ public class FormFillController { description = "Populates the supplied PDF form using values from the provided JSON payload" + " and returns the filled PDF") - public ResponseEntity fillForm( + public ResponseEntity fillForm( @Parameter( description = "The input PDF file", required = true, @@ -355,7 +356,7 @@ public class FormFillController { document -> FormUtils.applyFieldValues(document, values, flatten, true)); } - private ResponseEntity processSingleFile( + private ResponseEntity processSingleFile( MultipartFile file, String suffix, DocumentProcessor processor) throws IOException { requirePdf(file); 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 197a9f52db..9ee659c35c 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 @@ -1,7 +1,7 @@ package stirling.software.SPDF.controller.api.misc; -import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.file.Files; import java.util.List; import java.util.Optional; @@ -10,6 +10,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -30,6 +31,8 @@ import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -43,14 +46,16 @@ public class AttachmentController { private final ConvertPDFToPDFA convertPDFToPDFA; + private final TempFileManager tempFileManager; + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-attachments") @StandardPdfResponse @Operation( summary = "Add attachments to PDF", description = "This endpoint adds attachments to a PDF. Input:PDF, Output:PDF Type:MISO") - public ResponseEntity addAttachments(@ModelAttribute AddAttachmentRequest request) - throws Exception { + public ResponseEntity addAttachments( + @ModelAttribute AddAttachmentRequest request) throws Exception { MultipartFile fileInput = request.getFileInput(); List attachments = request.getAttachments(); boolean convertToPdfA3b = request.isConvertToPdfA3b(); @@ -79,13 +84,9 @@ public class AttachmentController { ConvertPDFToPDFA.fixType1FontCharSet(pdfaDocument); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - pdfaDocument.save(baos); - byte[] resultBytes = baos.toByteArray(); - String outputFilename = baseFileName + "_with_attachments_PDFA-3b.pdf"; - return WebResponseUtils.bytesToWebResponse( - resultBytes, outputFilename, MediaType.APPLICATION_PDF); + return WebResponseUtils.pdfDocToWebResponse( + pdfaDocument, outputFilename, tempFileManager); } } else { try (PDDocument document = pdfDocumentFactory.load(request, false)) { @@ -94,7 +95,8 @@ public class AttachmentController { document, GeneralUtils.generateFilename( Filenames.toSimpleFileName(fileInput.getOriginalFilename()), - "_with_attachments.pdf")); + "_with_attachments.pdf"), + tempFileManager); } } } @@ -141,7 +143,7 @@ public class AttachmentController { description = "This endpoint extracts all embedded attachments from a PDF into a ZIP archive." + " Input:PDF Output:ZIP Type:SISO") - public ResponseEntity extractAttachments( + public ResponseEntity extractAttachments( @ModelAttribute ExtractAttachmentsRequest request) throws IOException { try (PDDocument document = pdfDocumentFactory.load(request, true)) { Optional extracted = pdfAttachmentService.extractAttachments(document); @@ -159,8 +161,14 @@ public class AttachmentController { Filenames.toSimpleFileName( GeneralUtils.generateFilename(sourceName, "_attachments.zip")); - return WebResponseUtils.bytesToWebResponse( - extracted.get(), outputName, MediaType.APPLICATION_OCTET_STREAM); + TempFile tempOut = tempFileManager.createManagedTempFile(".zip"); + try { + Files.write(tempOut.getFile().toPath(), extracted.get()); + } catch (IOException e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.zipFileToWebResponse(tempOut, outputName); } } @@ -187,8 +195,8 @@ public class AttachmentController { summary = "Rename attachment in PDF", description = "This endpoint renames an embedded attachment in a PDF. Input:PDF Output:PDF Type:MISO") - public ResponseEntity renameAttachment(@ModelAttribute RenameAttachmentRequest request) - throws Exception { + public ResponseEntity renameAttachment( + @ModelAttribute RenameAttachmentRequest request) throws Exception { MultipartFile fileInput = request.getFileInput(); String attachmentName = request.getAttachmentName(); String newName = request.getNewName(); @@ -209,7 +217,8 @@ public class AttachmentController { document, GeneralUtils.generateFilename( Filenames.toSimpleFileName(fileInput.getOriginalFilename()), - "_attachment_renamed.pdf")); + "_attachment_renamed.pdf"), + tempFileManager); } } @@ -221,8 +230,8 @@ public class AttachmentController { summary = "Delete attachment from PDF", description = "This endpoint deletes an embedded attachment from a PDF. Input:PDF Output:PDF Type:MISO") - public ResponseEntity deleteAttachment(@ModelAttribute DeleteAttachmentRequest request) - throws Exception { + public ResponseEntity deleteAttachment( + @ModelAttribute DeleteAttachmentRequest request) throws Exception { MultipartFile fileInput = request.getFileInput(); String attachmentName = request.getAttachmentName(); @@ -238,7 +247,8 @@ public class AttachmentController { document, GeneralUtils.generateFilename( Filenames.toSimpleFileName(fileInput.getOriginalFilename()), - "_attachment_deleted.pdf")); + "_attachment_deleted.pdf"), + tempFileManager); } } } 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 32eef1a1e9..58fd995f6d 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 @@ -12,6 +12,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -24,6 +25,7 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.RegexPatternUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -35,6 +37,7 @@ public class AutoRenameController { private static final int LINE_LIMIT = 200; private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/auto-rename") @Operation( @@ -42,8 +45,8 @@ public class AutoRenameController { description = "This endpoint accepts a PDF file and attempts to extract its title or header" + " based on heuristics. Input:PDF Output:PDF Type:SISO") - public ResponseEntity extractHeader(@ModelAttribute ExtractHeaderRequest request) - throws Exception { + public ResponseEntity extractHeader( + @ModelAttribute ExtractHeaderRequest request) throws Exception { MultipartFile file = request.getFileInput(); boolean useFirstTextAsFallback = Boolean.TRUE.equals(request.getUseFirstTextAsFallback()); @@ -140,11 +143,14 @@ public class AutoRenameController { .matcher(header) .replaceAll("") .trim(); - return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf"); + return WebResponseUtils.pdfDocToWebResponse( + document, header + ".pdf", tempFileManager); } else { log.info("File has no good title to be found"); return WebResponseUtils.pdfDocToWebResponse( - document, Filenames.toSimpleFileName(file.getOriginalFilename())); + document, + Filenames.toSimpleFileName(file.getOriginalFilename()), + 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 eec08ba9b6..d771df580a 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 @@ -1,26 +1,31 @@ package stirling.software.SPDF.controller.api.misc; +import java.awt.Graphics2D; import java.awt.image.BufferedImage; -import java.awt.image.DataBufferByte; -import java.awt.image.DataBufferInt; -import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; import java.util.ArrayList; -import java.util.HashSet; +import java.util.EnumMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.rendering.PDFRenderer; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import com.google.zxing.*; +import com.google.zxing.common.GlobalHistogramBinarizer; import com.google.zxing.common.HybridBinarizer; import io.github.pixee.security.Filenames; @@ -35,7 +40,6 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; -import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.TempFile; @@ -48,61 +52,219 @@ import stirling.software.common.util.WebResponseUtils; public class AutoSplitPdfController { private static final Set VALID_QR_CONTENTS = - new HashSet<>( - Set.of( - "https://github.com/Stirling-Tools/Stirling-PDF", - "https://github.com/Frooodle/Stirling-PDF", - "https://stirlingpdf.com")); + Set.of( + "https://github.com/Stirling-Tools/Stirling-PDF", + "https://github.com/Frooodle/Stirling-PDF", + "https://stirlingpdf.com"); + + private static final int MAX_IMAGES_FOR_DIRECT_EXTRACTION = 3; + + // 150 DPI is sufficient for QR code detection — higher wastes memory and CPU + private static final int QR_DETECTION_DPI = 150; + + // Max total pixels before we downscale to avoid OOM on getRGB() allocation + private static final long MAX_IMAGE_PIXELS = 100_000_000L; // ~10000x10000 + + // Number of evenly-spaced pixel samples used for the blank image check + private static final int BLANK_CHECK_SAMPLES = 20; + + private static final Map DECODE_HINTS; + + static { + DECODE_HINTS = new EnumMap<>(DecodeHintType.class); + DECODE_HINTS.put(DecodeHintType.TRY_HARDER, Boolean.TRUE); + DECODE_HINTS.put(DecodeHintType.ALSO_INVERTED, Boolean.TRUE); + DECODE_HINTS.put(DecodeHintType.POSSIBLE_FORMATS, List.of(BarcodeFormat.QR_CODE)); + } private final CustomPDFDocumentFactory pdfDocumentFactory; private final TempFileManager tempFileManager; + private final ApplicationProperties applicationProperties; - private static String decodeQRCode(BufferedImage bufferedImage) { - LuminanceSource source; - - if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte dataBufferByte) { - byte[] pixels = dataBufferByte.getData(); - source = - new PlanarYUVLuminanceSource( - pixels, - bufferedImage.getWidth(), - bufferedImage.getHeight(), - 0, - 0, - bufferedImage.getWidth(), - bufferedImage.getHeight(), - false); - } else if (bufferedImage.getRaster().getDataBuffer() - instanceof DataBufferInt dataBufferInt) { - int[] pixels = dataBufferInt.getData(); - byte[] newPixels = new byte[pixels.length]; - for (int i = 0; i < pixels.length; i++) { - newPixels[i] = (byte) (pixels[i] & 0xff); - } - source = - new PlanarYUVLuminanceSource( - newPixels, - bufferedImage.getWidth(), - bufferedImage.getHeight(), - 0, - 0, - bufferedImage.getWidth(), - bufferedImage.getHeight(), - false); - } else { - throw new IllegalArgumentException( - "BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed" - + " int), byte gray, or 3-byte/4-byte RGB image data"); + /** + * Downscale an image if it exceeds the maximum pixel count. Scales uniformly based on the + * pixel-count ratio so both portrait and landscape images are handled correctly. + */ + private static BufferedImage downscaleIfNeeded(BufferedImage image) { + long totalPixels = (long) image.getWidth() * image.getHeight(); + if (totalPixels <= MAX_IMAGE_PIXELS) { + return image; } + double scale = Math.sqrt((double) MAX_IMAGE_PIXELS / totalPixels); + int newWidth = Math.max(1, (int) (image.getWidth() * scale)); + int newHeight = Math.max(1, (int) (image.getHeight() * scale)); + log.debug( + "Downscaling image from {}x{} to {}x{} for QR detection", + image.getWidth(), + image.getHeight(), + newWidth, + newHeight); + BufferedImage scaled = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); + Graphics2D g = scaled.createGraphics(); + g.drawImage(image, 0, 0, newWidth, newHeight, null); + g.dispose(); + return scaled; + } - BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + /** + * Quick check whether an image appears to be blank (single solid colour). Samples pixels at + * evenly-spaced positions — if all samples match the first pixel the image is almost certainly + * blank (e.g. a masked image that returned solid white). + */ + private static boolean isBlankImage(int[] pixels) { + if (pixels.length == 0) return true; + int first = pixels[0]; + int step = Math.max(1, pixels.length / BLANK_CHECK_SAMPLES); + for (int i = step; i < pixels.length; i += step) { + if (pixels[i] != first) { + return false; + } + } + return true; + } + /** + * Try to decode a QR code from pre-extracted RGB pixel data using multiple binarization + * strategies. Returns the decoded text or null. + * + *

Strategy 1: HybridBinarizer — good for variable brightness (digital PDFs). + * + *

Strategy 2: GlobalHistogramBinarizer — better for scanned/noisy images with uniform + * lighting, and for QR codes with embedded logos that confuse the hybrid approach. + */ + private static String tryDecodeQR(int[] pixels, int width, int height) { + RGBLuminanceSource source = new RGBLuminanceSource(width, height, pixels); + MultiFormatReader reader = new MultiFormatReader(); + + // Strategy 1: HybridBinarizer — good for variable brightness (digital PDFs) try { - Result result = new MultiFormatReader().decode(bitmap); + BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + Result result = reader.decode(bitmap, DECODE_HINTS); + log.debug("QR detected via HybridBinarizer: '{}'", result.getText()); return result.getText(); } catch (NotFoundException e) { - return null; // there is no QR code in the image + // continue } + + // Strategy 2: GlobalHistogramBinarizer — better for scanned/noisy images + try { + BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source)); + Result result = reader.decode(bitmap, DECODE_HINTS); + log.debug("QR detected via GlobalHistogramBinarizer: '{}'", result.getText()); + return result.getText(); + } catch (NotFoundException e) { + return null; + } + } + + /** + * Attempt to decode a QR code from a BufferedImage. Handles downscaling for oversized images + * and skips blank images early. + */ + private static String decodeQRCode(BufferedImage bufferedImage) { + bufferedImage = downscaleIfNeeded(bufferedImage); + + int width = bufferedImage.getWidth(); + int height = bufferedImage.getHeight(); + int[] pixels = new int[width * height]; + bufferedImage.getRGB(0, 0, width, height, pixels, 0, width); + + // Skip blank images early (e.g. masked images that decode to solid white) + if (isBlankImage(pixels)) { + log.debug("Skipping blank {}x{} image", width, height); + return null; + } + + return tryDecodeQR(pixels, width, height); + } + + /** Count the number of images embedded in a page's resources. */ + private static int countPageImages(PDPage page) { + if (page.getResources() == null || page.getResources().getXObjectNames() == null) { + return 0; + } + int count = 0; + for (COSName name : page.getResources().getXObjectNames()) { + if (page.getResources().isImageXObject(name)) { + count++; + } + } + return count; + } + + /** + * Extract images directly from a page's resources and check each for a QR code. Returns the QR + * code text if found, null otherwise. + */ + private static String checkPageImagesDirect(PDPage page) throws IOException { + if (page.getResources() == null || page.getResources().getXObjectNames() == null) { + return null; + } + for (COSName name : page.getResources().getXObjectNames()) { + if (!page.getResources().isImageXObject(name)) { + continue; + } + PDImageXObject imageObject = (PDImageXObject) page.getResources().getXObject(name); + + BufferedImage image; + try { + image = imageObject.getImage(); + } catch (OutOfMemoryError e) { + log.warn( + "Skipping oversized embedded image '{}' ({}x{}) - out of memory", + name.getName(), + imageObject.getWidth(), + imageObject.getHeight()); + continue; + } + + String result = decodeQRCode(image); + if (result != null) { + return result; + } + } + return null; + } + + /** + * Render the full page to an image and scan it for a QR code. Tries a low DPI first (fast, low + * memory) and only retries at the system's maxDPI if detection fails. The first rendered image + * is released before the retry to allow GC to reclaim it. + */ + private String checkPageByRendering(PDFRenderer pdfRenderer, int pageNum) throws IOException { + log.debug("Rendering page {} at {} DPI for QR detection", pageNum + 1, QR_DETECTION_DPI); + + BufferedImage bim = + ExceptionUtils.handleOomRendering( + pageNum + 1, + QR_DETECTION_DPI, + () -> pdfRenderer.renderImageWithDPI(pageNum, QR_DETECTION_DPI)); + String result = decodeQRCode(bim); + bim = null; // allow GC before potential high-DPI retry + + if (result == null) { + int maxDpi = getSystemMaxDpi(); + if (maxDpi > QR_DETECTION_DPI) { + log.debug( + "Retrying page {} at {} DPI (low-DPI detection failed)", + pageNum + 1, + maxDpi); + BufferedImage highRes = + ExceptionUtils.handleOomRendering( + pageNum + 1, + maxDpi, + () -> pdfRenderer.renderImageWithDPI(pageNum, maxDpi)); + result = decodeQRCode(highRes); + } + } + return result; + } + + private int getSystemMaxDpi() { + if (applicationProperties != null && applicationProperties.getSystem() != null) { + return applicationProperties.getSystem().getMaxDPI(); + } + return QR_DETECTION_DPI; } @AutoJobPostMapping(value = "/auto-split-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @@ -111,42 +273,56 @@ public class AutoSplitPdfController { 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" + + " 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") - public ResponseEntity autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) - throws IOException { + public ResponseEntity autoSplitPdf( + @ModelAttribute AutoSplitPdfRequest request) throws IOException { MultipartFile file = request.getFileInput(); boolean duplexMode = Boolean.TRUE.equals(request.getDuplexMode()); + log.info( + "Auto-split starting: filename='{}', size={} bytes, duplexMode={}", + file.getOriginalFilename(), + file.getSize(), + duplexMode); + List splitDocuments = new ArrayList<>(); - try (TempFile outputTempFile = new TempFile(tempFileManager, ".zip"); - PDDocument document = pdfDocumentFactory.load(file.getInputStream())) { + TempFile outputTempFile = new TempFile(tempFileManager, ".zip"); + try (PDDocument document = pdfDocumentFactory.load(file.getInputStream())) { + int totalPages = document.getNumberOfPages(); + log.info("PDF loaded, totalPages={}", totalPages); + PDFRenderer pdfRenderer = new PDFRenderer(document); pdfRenderer.setSubsamplingAllowed(true); - for (int page = 0; page < document.getNumberOfPages(); ++page) { - BufferedImage bim; + for (int page = 0; page < totalPages; ++page) { + PDPage pdPage = document.getPage(page); + int imageCount = countPageImages(pdPage); - // Use global maximum DPI setting, fallback to 300 if not set - int renderDpi = 150; // Default fallback - ApplicationProperties properties = - ApplicationContextProvider.getBean(ApplicationProperties.class); - if (properties != null && properties.getSystem() != null) { - renderDpi = properties.getSystem().getMaxDPI(); + String qrResult; + if (imageCount > 0 && imageCount <= MAX_IMAGES_FOR_DIRECT_EXTRACTION) { + // Try extracting images directly from the PDF (faster, avoids rendering) + qrResult = checkPageImagesDirect(pdPage); + if (qrResult == null) { + // Fall back to rendering — the image may use masking/compositing + // that getImage() doesn't resolve, or the QR may be vector-drawn + qrResult = checkPageByRendering(pdfRenderer, page); + } + } else { + // Too many images or no images — render the full page + qrResult = checkPageByRendering(pdfRenderer, page); } - final int dpi = renderDpi; - final int pageNum = page; - bim = - ExceptionUtils.handleOomRendering( - pageNum + 1, - dpi, - () -> pdfRenderer.renderImageWithDPI(pageNum, dpi)); - String result = decodeQRCode(bim); + boolean isValidQrCode = qrResult != null && VALID_QR_CONTENTS.contains(qrResult); + if (isValidQrCode) { + log.info( + "Page {}/{} contains QR divider ('{}')", + page + 1, + totalPages, + qrResult); + } - boolean isValidQrCode = VALID_QR_CONTENTS.contains(result); - log.debug("detected qr code {}, code is vale={}", result, isValidQrCode); if (isValidQrCode && page != 0) { splitDocuments.add(new PDDocument()); } @@ -159,45 +335,36 @@ public class AutoSplitPdfController { splitDocuments.add(firstDocument); } - // If duplexMode is true and current page is a divider, then skip next page if (duplexMode && isValidQrCode) { - page++; + page++; // skip back of divider page } } - // Remove split documents that have no pages splitDocuments.removeIf(pdDocument -> pdDocument.getNumberOfPages() == 0); + log.info("Split complete, {} output documents", splitDocuments.size()); String filename = GeneralUtils.removeExtension( Filenames.toSimpleFileName(file.getOriginalFilename())); - try (ZipOutputStream zipOut = - new ZipOutputStream(Files.newOutputStream(outputTempFile.getPath()))) { + // Stream split documents directly into zip — avoids holding all PDFs in memory + try (OutputStream fileOut = Files.newOutputStream(outputTempFile.getPath()); + ZipOutputStream zipOut = new ZipOutputStream(fileOut)) { for (int i = 0; i < splitDocuments.size(); i++) { String fileName = filename + "_" + (i + 1) + ".pdf"; - PDDocument splitDocument = splitDocuments.get(i); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - splitDocument.save(baos); - byte[] pdf = baos.toByteArray(); - - ZipEntry pdfEntry = new ZipEntry(fileName); - zipOut.putNextEntry(pdfEntry); - zipOut.write(pdf); + zipOut.putNextEntry(new ZipEntry(fileName)); + splitDocuments.get(i).save(zipOut); zipOut.closeEntry(); } } - byte[] data = Files.readAllBytes(outputTempFile.getPath()); - return WebResponseUtils.bytesToWebResponse( - data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.zipFileToWebResponse(outputTempFile, filename + ".zip"); } catch (Exception e) { + outputTempFile.close(); log.error("Error in auto split", e); throw e; } finally { - // Clean up split documents for (PDDocument splitDoc : splitDocuments) { try { splitDoc.close(); 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 486e9d1176..98364d9c2f 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 @@ -1,8 +1,9 @@ package stirling.software.SPDF.controller.api.misc; import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -19,6 +20,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -35,6 +37,8 @@ import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PdfUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -43,6 +47,7 @@ import stirling.software.common.util.WebResponseUtils; public class BlankPageController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; public static boolean isBlankImage( BufferedImage image, int threshold, double whitePercent, int blurSize) { @@ -83,7 +88,8 @@ public class BlankPageController { "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") - public ResponseEntity removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request) + public ResponseEntity removeBlankPages( + @ModelAttribute RemoveBlankPagesRequest request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); int threshold = request.getThreshold(); @@ -149,28 +155,29 @@ public class BlankPageController { pageIndex++; } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ZipOutputStream zos = new ZipOutputStream(baos); - String filename = GeneralUtils.removeExtension( Filenames.toSimpleFileName(inputFile.getOriginalFilename())); - if (!nonBlankPages.isEmpty()) { - createZipEntry(zos, nonBlankPages, filename + "_nonBlankPages.pdf"); - } else { - createZipEntry(zos, blankPages, filename + "_allBlankPages.pdf"); - } + TempFile tempOut = tempFileManager.createManagedTempFile(".zip"); + try (OutputStream fos = Files.newOutputStream(tempOut.getFile().toPath()); + ZipOutputStream zos = new ZipOutputStream(fos)) { + if (!nonBlankPages.isEmpty()) { + createZipEntry(zos, nonBlankPages, filename + "_nonBlankPages.pdf"); + } else { + createZipEntry(zos, blankPages, filename + "_allBlankPages.pdf"); + } - if (!nonBlankPages.isEmpty() && !blankPages.isEmpty()) { - createZipEntry(zos, blankPages, filename + "_blankPages.pdf"); + if (!nonBlankPages.isEmpty() && !blankPages.isEmpty()) { + createZipEntry(zos, blankPages, filename + "_blankPages.pdf"); + } + } catch (IOException e) { + tempOut.close(); + throw e; } - zos.close(); - log.info("Returning ZIP file: {}", filename + "_processed.zip"); - return WebResponseUtils.baosToWebResponse( - baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.zipFileToWebResponse(tempOut, filename + "_processed.zip"); } catch (ExceptionUtils.OutOfMemoryDpiException e) { throw e; 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 0a321ff3b8..d4ad134ece 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 @@ -38,6 +38,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -328,7 +329,8 @@ public class CompressController { + "_" + image.getBitsPerComponent(); - return bytesToHexString(generateMD5(enhancedData.getBytes())); + return bytesToHexString( + generateMD5(enhancedData.getBytes(StandardCharsets.UTF_8))); } return "empty-stream"; } @@ -727,7 +729,8 @@ public class CompressController { params.append("_").append(image.getDecode().toString()); } - return bytesToHexString(generateMD5(params.toString().getBytes())); + return bytesToHexString( + generateMD5(params.toString().getBytes(StandardCharsets.UTF_8))); } catch (Exception e) { return "fallback-decode-" + System.identityHashCode(image); } @@ -798,7 +801,8 @@ public class CompressController { metadata.append("_softmask"); } - return bytesToHexString(generateMD5(metadata.toString().getBytes())); + return bytesToHexString( + generateMD5(metadata.toString().getBytes(StandardCharsets.UTF_8))); } catch (Exception e) { return "fallback-meta-" + System.identityHashCode(image); } @@ -924,8 +928,8 @@ public class CompressController { description = "This endpoint accepts a PDF file and optimizes it based on the provided" + " parameters. Input:PDF Output:PDF Type:SISO") - public ResponseEntity optimizePdf(@ModelAttribute OptimizePdfRequest request) - throws Exception { + public ResponseEntity optimizePdf( + @ModelAttribute OptimizePdfRequest request) throws Exception { MultipartFile inputFile = request.getFileInput(); // Validate input file @@ -1097,7 +1101,8 @@ public class CompressController { try { try (PDDocument document = pdfDocumentFactory.load(currentFile.toFile())) { - return WebResponseUtils.pdfDocToWebResponse(document, outputFilename); + return WebResponseUtils.pdfDocToWebResponse( + document, outputFilename, tempFileManager); } } catch (IOException e) { throw ExceptionUtils.handlePdfException(e, "PDF optimization"); 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 439436a44c..1ea0ddd0aa 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 @@ -23,6 +23,7 @@ import stirling.software.common.configuration.AppConfig; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.ServerCertificateServiceInterface; import stirling.software.common.service.UserServiceInterface; +import stirling.software.common.util.GeneralUtils; @ConfigApi @Hidden @@ -122,8 +123,17 @@ public class ConfigController { configData.put("contextPath", appConfig.getContextPath()); configData.put("serverPort", appConfig.getServerPort()); - // Add frontendUrl for mobile scanner QR codes String frontendUrl = applicationProperties.getSystem().getFrontendUrl(); + if ((frontendUrl == null || frontendUrl.isBlank()) + && Boolean.parseBoolean( + System.getProperty("STIRLING_PDF_TAURI_MODE", "false"))) { + String localIp = GeneralUtils.getLocalNetworkIp(); + if (localIp != null) { + String scheme = + appConfig.getBackendUrl().startsWith("https") ? "https" : "http"; + frontendUrl = scheme + "://" + localIp + ":" + appConfig.getServerPort(); + } + } configData.put("frontendUrl", frontendUrl != null ? frontendUrl : ""); // Add mobile scanner settings 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 2abfaa8af6..9afe8ba235 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 @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.HashSet; @@ -14,6 +13,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -26,6 +26,8 @@ import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -34,12 +36,13 @@ import stirling.software.common.util.WebResponseUtils; public class DecompressPdfController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(value = "/decompress-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation( summary = "Decompress PDF streams", description = "Fully decompresses all PDF streams including text content") - public ResponseEntity decompressPdf(@ModelAttribute PDFFile request) + public ResponseEntity decompressPdf(@ModelAttribute PDFFile request) throws IOException { MultipartFile file = request.getFileInput(); @@ -48,13 +51,18 @@ public class DecompressPdfController { // Process all objects in document processAllObjects(document); - // Save with explicit no compression - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos, CompressParameters.NO_COMPRESSION); + // Save with explicit no compression to a temp file + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + document.save(tempOut.getFile(), CompressParameters.NO_COMPRESSION); + } catch (IOException e) { + tempOut.close(); + throw e; + } - // Return the PDF as a response - return WebResponseUtils.bytesToWebResponse( - baos.toByteArray(), + // Return the PDF as a streaming response + return WebResponseUtils.pdfFileToWebResponse( + tempOut, GeneralUtils.generateFilename(file.getOriginalFilename(), "_decompressed.pdf")); } } 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 bcf57d17a9..c5cfef2c79 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 @@ -1,8 +1,8 @@ package stirling.software.SPDF.controller.api.misc; import java.awt.image.BufferedImage; -import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -21,6 +21,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -39,6 +40,8 @@ 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.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -49,6 +52,7 @@ public class ExtractImageScansController { private static final String REPLACEFIRST = "[.][^.]+$"; private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping( consumes = MediaType.MULTIPART_FORM_DATA_VALUE, @@ -61,7 +65,7 @@ public class ExtractImageScansController { + " parameters. Users can specify angle threshold, tolerance, minimum area," + " minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP" + " Type:SIMO") - public ResponseEntity extractImageScans( + public ResponseEntity extractImageScans( @ModelAttribute ExtractImageScansRequest request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); @@ -71,9 +75,8 @@ public class ExtractImageScansController { List images = new ArrayList<>(); - List tempImageFiles = new ArrayList<>(); - Path tempInputFile; - Path tempZipFile = null; + List tempImageFiles = new ArrayList<>(); + TempFile tempInputFile = null; List tempDirs = new ArrayList<>(); if (!CheckProgramInstall.isPythonAvailable()) { @@ -83,6 +86,8 @@ public class ExtractImageScansController { String pythonVersion = CheckProgramInstall.getAvailablePythonCommand(); Path splitPhotosScript = GeneralUtils.extractScript("split_photos.py"); + TempFile finalOutput = null; + boolean finalOutputOwnershipTransferred = false; try { // Check if input file is a PDF if ("pdf".equalsIgnoreCase(extension)) { @@ -96,7 +101,8 @@ public class ExtractImageScansController { // Create images of all pages for (int i = 0; i < pageCount; i++) { // Create temp file to save the image - Path tempFile = Files.createTempFile("image_", ".png"); + TempFile tempImage = tempFileManager.createManagedTempFile(".png"); + tempImageFiles.add(tempImage); // Render image and save as temp file BufferedImage image; @@ -116,18 +122,17 @@ public class ExtractImageScansController { pageIndex + 1, dpi, () -> pdfRenderer.renderImageWithDPI(pageIndex, dpi)); - ImageIO.write(image, "png", tempFile.toFile()); + ImageIO.write(image, "png", tempImage.getFile()); // Add temp file path to images list - images.add(tempFile.toString()); - tempImageFiles.add(tempFile); + images.add(tempImage.getAbsolutePath()); } } } else { - tempInputFile = Files.createTempFile("input_", "." + extension); - inputFile.transferTo(tempInputFile); + tempInputFile = tempFileManager.createManagedTempFile("." + extension); + inputFile.transferTo(tempInputFile.getFile()); // Add input file path to images list - images.add(tempInputFile.toString()); + images.add(tempInputFile.getAbsolutePath()); } List processedImageBytes = new ArrayList<>(); @@ -177,10 +182,10 @@ public class ExtractImageScansController { if (processedImageBytes.size() > 1) { String outputZipFilename = GeneralUtils.generateFilename(fileName, "_processed.zip"); - tempZipFile = Files.createTempFile("output_", ".zip"); + finalOutput = tempFileManager.createManagedTempFile(".zip"); try (ZipOutputStream zipOut = - new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) { + new ZipOutputStream(Files.newOutputStream(finalOutput.getPath()))) { // Add processed images to the zip for (int i = 0; i < processedImageBytes.size(); i++) { ZipEntry entry = @@ -193,13 +198,10 @@ public class ExtractImageScansController { } } - byte[] zipBytes = Files.readAllBytes(tempZipFile); - - // Clean up the temporary zip file - Files.deleteIfExists(tempZipFile); - - return WebResponseUtils.bytesToWebResponse( - zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); + ResponseEntity response = + WebResponseUtils.zipFileToWebResponse(finalOutput, outputZipFilename); + finalOutputOwnershipTransferred = true; + return response; } if (processedImageBytes.isEmpty()) { throw ExceptionUtils.createIllegalArgumentException( @@ -208,28 +210,28 @@ public class ExtractImageScansController { // Return the processed image as a response byte[] imageBytes = processedImageBytes.get(0); - return WebResponseUtils.bytesToWebResponse( - imageBytes, - GeneralUtils.generateFilename(fileName, ".png"), - MediaType.IMAGE_PNG); + finalOutput = tempFileManager.createManagedTempFile(".png"); + try (OutputStream out = Files.newOutputStream(finalOutput.getPath())) { + out.write(imageBytes); + } + + ResponseEntity response = + WebResponseUtils.fileToWebResponse( + finalOutput, + GeneralUtils.generateFilename(fileName, ".png"), + MediaType.IMAGE_PNG); + finalOutputOwnershipTransferred = true; + return response; } } finally { + if (finalOutput != null && !finalOutputOwnershipTransferred) { + finalOutput.close(); + } // Cleanup logic for all temporary files and directories - tempImageFiles.forEach( - path -> { - try { - Files.deleteIfExists(path); - } catch (IOException e) { - log.error("Failed to delete temporary image file: {}", path, e); - } - }); + tempImageFiles.forEach(TempFile::close); - if (tempZipFile != null && Files.exists(tempZipFile)) { - try { - Files.deleteIfExists(tempZipFile); - } catch (IOException e) { - log.error("Failed to delete temporary zip file: {}", tempZipFile, e); - } + if (tempInputFile != null) { + tempInputFile.close(); } tempDirs.forEach( 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 eecf1269f9..7eac5c2b83 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 @@ -15,6 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -30,6 +31,7 @@ import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -38,6 +40,7 @@ import stirling.software.common.util.WebResponseUtils; public class FlattenController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/flatten") @StandardPdfResponse @@ -46,7 +49,8 @@ public class FlattenController { description = "Flattening just PDF form fields or converting each page to images to make text" + " unselectable. Input:PDF, Output:PDF. Type:SISO") - public ResponseEntity flatten(@ModelAttribute FlattenRequest request) throws Exception { + public ResponseEntity flatten(@ModelAttribute FlattenRequest request) + throws Exception { MultipartFile file = request.getFileInput(); try (PDDocument document = pdfDocumentFactory.load(file)) { @@ -58,7 +62,9 @@ public class FlattenController { acroForm.flatten(); } return WebResponseUtils.pdfDocToWebResponse( - document, Filenames.toSimpleFileName(file.getOriginalFilename())); + document, + Filenames.toSimpleFileName(file.getOriginalFilename()), + tempFileManager); } else { // flatten whole page aka convert each page to image and re-add it (making text // unselectable) @@ -143,7 +149,9 @@ public class FlattenController { } } return WebResponseUtils.pdfDocToWebResponse( - newDocument, Filenames.toSimpleFileName(file.getOriginalFilename())); + newDocument, + Filenames.toSimpleFileName(file.getOriginalFilename()), + tempFileManager); } } } 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 1eb8f92681..9a7f7dcb24 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 @@ -13,6 +13,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -28,6 +29,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.PdfMetadataService; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.RegexPatternUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.propertyeditor.StringToMapPropertyEditor; @@ -37,6 +39,7 @@ import stirling.software.common.util.propertyeditor.StringToMapPropertyEditor; public class MetadataController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; private String checkUndefined(String entry) { // Check if the string is "undefined" @@ -61,7 +64,7 @@ public class MetadataController { "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") - public ResponseEntity metadata(@ModelAttribute MetadataRequest request) + public ResponseEntity metadata(@ModelAttribute MetadataRequest request) throws IOException { // Extract PDF file from the request object @@ -179,7 +182,8 @@ public class MetadataController { document, GeneralUtils.removeExtension( Filenames.toSimpleFileName(pdfFile.getOriginalFilename())) - + "_metadata.pdf"); + + "_metadata.pdf", + tempFileManager); } } } 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 22cf60dcbf..990f6dd982 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 @@ -25,6 +25,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -89,7 +90,7 @@ public class OCRController { + " 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") - public ResponseEntity processPdfWithOCR( + public ResponseEntity processPdfWithOCR( @ModelAttribute ProcessPdfWithOcrRequest request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); @@ -121,9 +122,11 @@ public class OCRController { throw ExceptionUtils.createOcrInvalidLanguagesException(); } - // Use try-with-resources for proper temp file management + TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf"); + TempFile tempZipFile = null; + boolean pdfOwnershipTransferred = false; + boolean zipOwnershipTransferred = false; try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf"); - TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf"); TempFile sidecarTextFile = sidecar ? new TempFile(tempFileManager, ".txt") : null) { inputFile.transferTo(tempInputFile.getFile()); @@ -156,9 +159,6 @@ public class OCRController { throw ExceptionUtils.createOcrToolsUnavailableException(); } - // Read the processed PDF file - byte[] pdfBytes = Files.readAllBytes(tempOutputFile.getPath()); - // Return the OCR processed PDF as a response String outputFilename = GeneralUtils.removeExtension( @@ -172,14 +172,14 @@ public class OCRController { Filenames.toSimpleFileName(inputFile.getOriginalFilename())) + "_OCR.zip"; - try (TempFile tempZipFile = new TempFile(tempFileManager, ".zip"); - ZipOutputStream zipOut = - new ZipOutputStream(Files.newOutputStream(tempZipFile.getPath()))) { + tempZipFile = new TempFile(tempFileManager, ".zip"); + try (ZipOutputStream zipOut = + new ZipOutputStream(Files.newOutputStream(tempZipFile.getPath()))) { // Add PDF file to the zip ZipEntry pdfEntry = new ZipEntry(outputFilename); zipOut.putNextEntry(pdfEntry); - zipOut.write(pdfBytes); + Files.copy(tempOutputFile.getPath(), zipOut); zipOut.closeEntry(); // Add text file to the zip @@ -189,16 +189,28 @@ public class OCRController { zipOut.closeEntry(); zipOut.finish(); - - byte[] zipBytes = Files.readAllBytes(tempZipFile.getPath()); - - // Return the zip file containing both the PDF and the text file - return WebResponseUtils.bytesToWebResponse( - zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); } + + // The intermediate PDF temp file is no longer needed; only the zip is streamed. + tempOutputFile.close(); + pdfOwnershipTransferred = true; + ResponseEntity response = + WebResponseUtils.fileToWebResponse( + tempZipFile, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); + zipOwnershipTransferred = true; + return response; } else { - // Return the OCR processed PDF as a response - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); + ResponseEntity response = + WebResponseUtils.pdfFileToWebResponse(tempOutputFile, outputFilename); + pdfOwnershipTransferred = true; + return response; + } + } finally { + if (!pdfOwnershipTransferred) { + tempOutputFile.close(); + } + if (tempZipFile != null && !zipOwnershipTransferred) { + tempZipFile.close(); } } } 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 3b893c17ab..f7e36340c7 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 @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import java.io.ByteArrayOutputStream; import java.io.IOException; import org.apache.pdfbox.pdmodel.PDDocument; @@ -12,6 +11,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -24,6 +24,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -32,6 +34,7 @@ import stirling.software.common.util.WebResponseUtils; public class OverlayImageController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-image") @Operation( @@ -42,7 +45,8 @@ public class OverlayImageController { + "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") - public ResponseEntity overlayImage(@ModelAttribute OverlayImageRequest request) { + public ResponseEntity overlayImage( + @ModelAttribute OverlayImageRequest request) { MultipartFile pdfFile = request.getFileInput(); MultipartFile imageFile = request.getImageFile(); float x = request.getX(); @@ -82,14 +86,17 @@ public class OverlayImageController { } } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); - - byte[] result = baos.toByteArray(); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + document.save(tempOut.getFile()); + } catch (IOException e) { + tempOut.close(); + throw e; + } log.info("PDF with overlaid image successfully created"); - return WebResponseUtils.bytesToWebResponse( - result, + return WebResponseUtils.pdfFileToWebResponse( + tempOut, GeneralUtils.generateFilename( pdfFile.getOriginalFilename(), "_overlayed.pdf")); } 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 6d0d05a28d..4e12a694a3 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 @@ -1,7 +1,6 @@ package stirling.software.SPDF.controller.api.misc; import java.awt.Color; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; import java.util.Locale; @@ -16,6 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -28,6 +28,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -35,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils; public class PageNumbersController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(value = "/add-page-numbers", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @StandardPdfResponse @@ -43,8 +46,8 @@ public class PageNumbersController { description = "This operation takes an input PDF file and adds page numbers to it. Input:PDF" + " Output:PDF Type:SISO") - public ResponseEntity addPageNumbers(@ModelAttribute AddPageNumbersRequest request) - throws IOException { + public ResponseEntity addPageNumbers( + @ModelAttribute AddPageNumbersRequest request) throws IOException { MultipartFile file = request.getFileInput(); String customMargin = request.getCustomMargin(); @@ -175,11 +178,16 @@ public class PageNumbersController { pageNumber++; } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + document.save(tempOut.getFile()); + } catch (IOException e) { + tempOut.close(); + throw e; + } - return WebResponseUtils.bytesToWebResponse( - baos.toByteArray(), + return WebResponseUtils.pdfFileToWebResponse( + tempOut, GeneralUtils.generateFilename( file.getOriginalFilename(), "_page_numbers_added.pdf")); } 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 53871f973b..33fcea588b 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 @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -17,6 +16,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -29,6 +29,8 @@ import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -37,6 +39,7 @@ import stirling.software.common.util.WebResponseUtils; public class RemoveImagesController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-image-pdf") @Operation( @@ -44,7 +47,8 @@ public class RemoveImagesController { description = "This endpoint removes all embedded images from a PDF file and returns the" + " modified document. Input:PDF Output:PDF Type:SISO") - public ResponseEntity removeImages(@ModelAttribute PDFFile request) throws IOException { + public ResponseEntity removeImages(@ModelAttribute PDFFile request) + throws IOException { MultipartFile inputFile = request.getFileInput(); @@ -60,12 +64,16 @@ public class RemoveImagesController { log.info("Removed {} images from PDF with {} pages", imagesRemoved, totalPages); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - pdfDoc.save(baos); - byte[] pdfContent = baos.toByteArray(); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + pdfDoc.save(tempOut.getFile()); + } catch (IOException e) { + tempOut.close(); + throw e; + } - return WebResponseUtils.bytesToWebResponse( - pdfContent, + return WebResponseUtils.pdfFileToWebResponse( + tempOut, GeneralUtils.generateFilename( inputFile.getOriginalFilename(), "_images_removed.pdf")); 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 8eaf3a892b..b0dc0a03a5 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 @@ -8,6 +8,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -53,13 +54,12 @@ public class RepairController { "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") - public ResponseEntity repairPdf(@ModelAttribute PDFFile file) + public ResponseEntity repairPdf(@ModelAttribute PDFFile file) throws IOException, InterruptedException { MultipartFile inputFile = file.getFileInput(); - // Use TempFile with try-with-resources for automatic cleanup - try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf"); - TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) { + TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf"); + try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf")) { // Save the uploaded file to the temporary location inputFile.transferTo(tempInputFile.getFile()); @@ -121,14 +121,17 @@ public class RepairController { } } - // Read the repaired PDF file - byte[] pdfBytes = pdfDocumentFactory.loadToBytes(tempOutputFile.getFile()); - - // Return the repaired PDF as a response - return WebResponseUtils.bytesToWebResponse( - pdfBytes, + // Return the repaired PDF as a streaming response + return WebResponseUtils.pdfFileToWebResponse( + tempOutputFile, GeneralUtils.generateFilename( inputFile.getOriginalFilename(), "_repaired.pdf")); + } catch (IOException | InterruptedException e) { + tempOutputFile.close(); + throw e; + } catch (RuntimeException e) { + tempOutputFile.close(); + throw e; } } } 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 72820acb97..defee45756 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 @@ -1,11 +1,15 @@ package stirling.software.SPDF.controller.api.misc; import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import org.springframework.core.io.InputStreamResource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -16,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.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -23,6 +29,7 @@ import stirling.software.common.util.WebResponseUtils; public class ReplaceAndInvertColorController { private final ReplaceAndInvertColorService replaceAndInvertColorService; + private final TempFileManager tempFileManager; @AutoJobPostMapping( consumes = MediaType.MULTIPART_FORM_DATA_VALUE, @@ -32,7 +39,7 @@ public class ReplaceAndInvertColorController { 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") - public ResponseEntity replaceAndInvertColor( + public ResponseEntity replaceAndInvertColor( @ModelAttribute ReplaceAndInvertColorRequest request) throws IOException { InputStreamResource resource = @@ -47,7 +54,15 @@ public class ReplaceAndInvertColorController { String filename = GeneralUtils.generateFilename( request.getFileInput().getOriginalFilename(), "_inverted.pdf"); - return WebResponseUtils.bytesToWebResponse( - resource.getContentAsByteArray(), filename, MediaType.APPLICATION_PDF); + + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try (InputStream in = resource.getInputStream()) { + Files.copy(in, tempOut.getFile().toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + tempOut.close(); + throw e; + } + + return WebResponseUtils.pdfFileToWebResponse(tempOut, filename); } } 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 e581eb8952..8318960ff9 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 @@ -6,7 +6,6 @@ import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; @@ -35,6 +34,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -51,6 +51,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -59,6 +60,7 @@ import stirling.software.common.util.WebResponseUtils; public class ScannerEffectController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; private static final int MAX_IMAGE_WIDTH = 8192; private static final int MAX_IMAGE_HEIGHT = 8192; private static final long MAX_IMAGE_PIXELS = 16_777_216; // 4096x4096 @@ -562,8 +564,8 @@ public class ScannerEffectController { 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") - public ResponseEntity scannerEffect(@Valid @ModelAttribute ScannerEffectRequest request) - throws IOException { + public ResponseEntity scannerEffect( + @Valid @ModelAttribute ScannerEffectRequest request) throws IOException { MultipartFile file = request.getFileInput(); List tempFiles = new ArrayList<>(); @@ -624,8 +626,7 @@ public class ScannerEffectController { sharedPdfBytes != null ? pdfDocumentFactory.load(sharedPdfBytes) : pdfDocumentFactory.load(processingInput); - PDDocument outputDocument = new PDDocument(); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + PDDocument outputDocument = new PDDocument()) { int totalPages = document.getNumberOfPages(); if (totalPages == 0) { @@ -708,12 +709,11 @@ public class ScannerEffectController { writeProcessedPagesToDocument(processedPages, outputDocument); - outputDocument.save(outputStream); - - return WebResponseUtils.bytesToWebResponse( - outputStream.toByteArray(), + return WebResponseUtils.pdfDocToWebResponse( + outputDocument, GeneralUtils.generateFilename( - file.getOriginalFilename(), "_scanner_effect.pdf")); + file.getOriginalFilename(), "_scanner_effect.pdf"), + tempFileManager); } } } finally { 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 29dc983507..0d7dc1d1d2 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 @@ -1,6 +1,7 @@ package stirling.software.SPDF.controller.api.misc; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Map; import org.apache.pdfbox.pdmodel.PDDocument; @@ -10,6 +11,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -21,6 +23,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -28,13 +32,15 @@ import stirling.software.common.util.WebResponseUtils; public class ShowJavascript { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/show-javascript") @JavaScriptResponse @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") - public ResponseEntity extractHeader(@ModelAttribute PDFFile file) throws Exception { + public ResponseEntity extractHeader(@ModelAttribute PDFFile file) + throws Exception { MultipartFile inputFile = file.getFileInput(); StringBuilder script = new StringBuilder(); boolean foundScript = false; @@ -77,8 +83,17 @@ public class ShowJavascript { .append("' does not contain Javascript"); } - return WebResponseUtils.bytesToWebResponse( - script.toString().getBytes(StandardCharsets.UTF_8), + TempFile tempOut = tempFileManager.createManagedTempFile(".js"); + try { + Files.write( + tempOut.getFile().toPath(), + script.toString().getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.fileToWebResponse( + tempOut, Filenames.toSimpleFileName(inputFile.getOriginalFilename()) + ".js", MediaType.TEXT_PLAIN); } 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 78f381fa7f..0e6fc1d004 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 @@ -37,6 +37,7 @@ import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -92,7 +93,7 @@ public class StampController { "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") - public ResponseEntity addStamp(@ModelAttribute AddStampRequest request) + public ResponseEntity addStamp(@ModelAttribute AddStampRequest request) throws IOException, Exception { MultipartFile pdfFile = request.getFileInput(); String pdfFileName = pdfFile.getOriginalFilename(); @@ -199,7 +200,8 @@ public class StampController { // Return the stamped PDF as a response return WebResponseUtils.pdfDocToWebResponse( document, - GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_stamped.pdf")); + GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_stamped.pdf"), + tempFileManager); } } @@ -483,9 +485,19 @@ public class StampController { y = overrideY; } else { x = calculatePositionX(pageSize, position, desiredPhysicalWidth, margin); - y = calculatePositionY(pageSize, position, desiredPhysicalHeight, margin); + // drawImage() places the lower-left corner at (x, y); use image-specific Y logic + y = calculateImagePositionY(pageSize, position, desiredPhysicalHeight, margin); } + float llx = pageSize.getLowerLeftX(); + float lly = pageSize.getLowerLeftY(); + float urx = pageSize.getUpperRightX(); + float ury = pageSize.getUpperRightY(); + float xMax = Math.max(llx, urx - desiredPhysicalWidth); + float yMax = Math.max(lly, ury - desiredPhysicalHeight); + x = Math.min(xMax, Math.max(llx, x)); + y = Math.min(yMax, Math.max(lly, y)); + contentStream.saveGraphicsState(); contentStream.transform(Matrix.getTranslateInstance(x, y)); contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0)); @@ -495,18 +507,37 @@ public class StampController { private float calculatePositionX( PDRectangle pageSize, int position, float contentWidth, float margin) { + float llx = pageSize.getLowerLeftX(); + float urx = pageSize.getUpperRightX(); return switch (position % 3) { case 1: // Left - yield pageSize.getLowerLeftX() + margin; + yield llx + margin; case 2: // Center - yield (pageSize.getWidth() - contentWidth) / 2; + yield llx + (pageSize.getWidth() - contentWidth) / 2; case 0: // Right - yield pageSize.getUpperRightX() - contentWidth - margin; + yield urx - contentWidth - margin; default: yield 0; }; } + private float calculateImagePositionY( + PDRectangle pageSize, int position, float imageHeight, float margin) { + float lly = pageSize.getLowerLeftY(); + float pageHeight = pageSize.getHeight(); + float ury = pageSize.getUpperRightY(); + return switch ((position - 1) / 3) { + case 0: // Top - upper image edge flush below top margin + yield ury - margin - imageHeight; + case 1: // Middle - center image on page + yield lly + (pageHeight - imageHeight) / 2; + case 2: // Bottom - lower image edge at bottom margin + yield lly + margin; + default: + yield lly; + }; + } + private float calculatePositionY( PDRectangle pageSize, int position, float height, float margin) { return switch ((position - 1) / 3) { 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 faa98a7ea8..22117b5f35 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 @@ -13,6 +13,7 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -26,15 +27,19 @@ import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.RegexPatternUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @Slf4j public class UnlockPDFFormsController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; - public UnlockPDFFormsController(CustomPDFDocumentFactory pdfDocumentFactory) { + public UnlockPDFFormsController( + CustomPDFDocumentFactory pdfDocumentFactory, TempFileManager tempFileManager) { this.pdfDocumentFactory = pdfDocumentFactory; + this.tempFileManager = tempFileManager; } @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/unlock-pdf-forms") @@ -44,7 +49,7 @@ public class UnlockPDFFormsController { description = "Removing read-only property from form fields making them fillable" + "Input:PDF, Output:PDF. Type:SISO") - public ResponseEntity unlockPDFForms(@ModelAttribute PDFFile file) { + public ResponseEntity unlockPDFForms(@ModelAttribute PDFFile file) { try (PDDocument document = pdfDocumentFactory.load(file)) { PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); @@ -116,7 +121,7 @@ public class UnlockPDFFormsController { GeneralUtils.generateFilename( file.getFileInput().getOriginalFilename(), "_unlocked_forms.pdf"); return WebResponseUtils.pdfDocToWebResponse( - document, Filenames.toSimpleFileName(mergedFileName)); + document, Filenames.toSimpleFileName(mergedFileName), tempFileManager); } catch (Exception e) { log.error(e.getMessage(), e); } 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 1eac93b227..fde2cfa900 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 @@ -11,36 +11,26 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.web.client.RequestCallback; -import org.springframework.web.client.RestTemplate; import org.springframework.web.multipart.MultipartFile; import io.github.pixee.security.Filenames; -import io.github.pixee.security.ZipSecurity; - -import jakarta.servlet.ServletContext; import lombok.extern.slf4j.Slf4j; -import stirling.software.SPDF.SPDFApplication; import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineResult; import stirling.software.SPDF.service.ApiDocService; -import stirling.software.common.model.enumeration.Role; -import stirling.software.common.service.UserServiceInterface; -import stirling.software.common.util.TempFile; +import stirling.software.common.service.InternalApiClient; import stirling.software.common.util.TempFileManager; +import stirling.software.common.util.ZipExtractionUtils; @Service @Slf4j @@ -48,20 +38,16 @@ public class PipelineProcessor { private final ApiDocService apiDocService; - private final UserServiceInterface userService; - - private final ServletContext servletContext; + private final InternalApiClient internalApiClient; private final TempFileManager tempFileManager; public PipelineProcessor( ApiDocService apiDocService, - @Autowired(required = false) UserServiceInterface userService, - ServletContext servletContext, + InternalApiClient internalApiClient, TempFileManager tempFileManager) { this.apiDocService = apiDocService; - this.userService = userService; - this.servletContext = servletContext; + this.internalApiClient = internalApiClient; this.tempFileManager = tempFileManager; } @@ -84,17 +70,6 @@ public class PipelineProcessor { return name.substring(0, underscoreIndex) + extension; } - private String getApiKeyForUser() { - if (userService == null) return ""; - return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); - } - - private String getBaseUrl() { - String contextPath = servletContext.getContextPath(); - String port = SPDFApplication.getStaticPort(); - return "http://localhost:" + port + contextPath + "/"; - } - PipelineResult runPipelineAgainstFiles(List outputFiles, PipelineConfig config) throws Exception { PipelineResult result = new PipelineResult(); @@ -122,7 +97,6 @@ public class PipelineProcessor { "Invalid operation: " + operation + " with parameters: " + parameters); } - String url = getBaseUrl() + operation; List newOutputFiles = new ArrayList<>(); if (!isMultiInputOperation) { for (Resource file : outputFiles) { @@ -144,12 +118,15 @@ public class PipelineProcessor { body.add(entry.getKey(), entry.getValue()); } } - ResponseEntity response = sendWebRequest(url, body); + ResponseEntity response = + internalApiClient.post(operation, body); // If the operation is filter and the response body is null or empty, // skip // this // file - if (response.getBody() instanceof TempFileResource tempFileResource) { + if (response.getBody() + instanceof + InternalApiClient.TempFileResource tempFileResource) { result.addTempFile(tempFileResource.getTempFile()); } @@ -226,8 +203,9 @@ public class PipelineProcessor { body.add(entry.getKey(), entry.getValue()); } } - ResponseEntity response = sendWebRequest(url, body); - if (response.getBody() instanceof TempFileResource tempFileResource) { + ResponseEntity response = internalApiClient.post(operation, body); + if (response.getBody() + instanceof InternalApiClient.TempFileResource tempFileResource) { result.addTempFile(tempFileResource.getTempFile()); } // Handle the response @@ -281,42 +259,6 @@ public class PipelineProcessor { return result; } - /* package */ ResponseEntity sendWebRequest( - String url, MultiValueMap body) { - RestTemplate restTemplate = new RestTemplate(); - // Set up headers, including API key - HttpHeaders headers = new HttpHeaders(); - String apiKey = getApiKeyForUser(); - if (apiKey != null && !apiKey.isEmpty()) { - headers.add("X-API-KEY", apiKey); - } - - // Let the message converter set the multipart boundary/content type - HttpEntity> entity = new HttpEntity<>(body, headers); - - RequestCallback requestCallback = - restTemplate.httpEntityCallback(entity, Resource.class /* response type hint */); - return restTemplate.execute( - url, - HttpMethod.POST, - requestCallback, - response -> { - try { - TempFile tempFile = tempFileManager.createManagedTempFile("pipeline"); - Files.copy( - response.getBody(), - tempFile.getPath(), - java.nio.file.StandardCopyOption.REPLACE_EXISTING); - TempFileResource resource = new TempFileResource(tempFile); - return ResponseEntity.status(response.getStatusCode()) - .headers(response.getHeaders()) - .body(resource); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); - } - private List processOutputFiles( String operation, ResponseEntity response, @@ -335,13 +277,15 @@ public class PipelineProcessor { newFilename = removeTrailingNaming(extractFilename(response)); } // Check if the response body is a zip file - if (isZip(response.getBody(), newFilename)) { + if (ZipExtractionUtils.isZip(response.getBody(), newFilename)) { // Unzip the file and add all the files to the new output files - newOutputFiles.addAll(unzip(response.getBody(), result)); + newOutputFiles.addAll( + ZipExtractionUtils.extractZip( + response.getBody(), tempFileManager, result::addTempFile)); } else { final Resource tempResource = response.getBody(); - if (tempResource instanceof TempFileResource) { - result.addTempFile(((TempFileResource) tempResource).getTempFile()); + if (tempResource instanceof InternalApiClient.TempFileResource tfr) { + result.addTempFile(tfr.getTempFile()); } Resource outputResource = new FileSystemResource(tempResource.getFile()) { @@ -424,97 +368,4 @@ public class PipelineProcessor { log.info("Files successfully loaded. Starting processing..."); return outputFiles; } - - private boolean isZip(Resource data, String filename) throws IOException { - if (data == null || data.contentLength() < 4) { - return false; - } - if (filename != null) { - String lower = filename.toLowerCase(); - if (lower.endsWith(".cbz")) { - // Treat CBZ as non-zip for our unzipping purposes - return false; - } - } - // Check the first four bytes of the data against the standard zip magic number - try (InputStream is = data.getInputStream()) { - byte[] header = new byte[4]; - if (is.read(header) < 4) { - return false; - } - return header[0] == 0x50 && header[1] == 0x4B && header[2] == 0x03 && header[3] == 0x04; - } - } - - private boolean isZip(Resource data) throws IOException { - return isZip(data, null); - } - - private static final int MAX_UNZIP_DEPTH = 10; - - private List unzip(Resource data, PipelineResult result) throws IOException { - return unzip(data, result, 0); - } - - private List unzip(Resource data, PipelineResult result, int depth) - throws IOException { - if (depth > MAX_UNZIP_DEPTH) { - log.warn( - "ZIP nesting depth {} exceeds limit {}, treating as file", - depth, - MAX_UNZIP_DEPTH); - return List.of(data); - } - log.info("Unzipping data of length: {}", data.contentLength()); - List unzippedFiles = new ArrayList<>(); - try (InputStream bais = data.getInputStream(); - ZipInputStream zis = ZipSecurity.createHardenedInputStream(bais)) { - ZipEntry entry; - while ((entry = zis.getNextEntry()) != null) { - if (entry.isDirectory()) { - continue; - } - TempFile tempFile = tempFileManager.createManagedTempFile("unzip"); - result.addTempFile(tempFile); - try (OutputStream os = Files.newOutputStream(tempFile.getPath())) { - byte[] buffer = new byte[4096]; - int count; - while ((count = zis.read(buffer)) != -1) { - os.write(buffer, 0, count); - } - } - final String filename = entry.getName(); - Resource fileResource = - new FileSystemResource(tempFile.getFile()) { - - @Override - public String getFilename() { - return filename; - } - }; - // If the unzipped file is a zip file, unzip it - if (isZip(fileResource, filename)) { - log.info("File {} is a zip file. Unzipping...", filename); - unzippedFiles.addAll(unzip(fileResource, result, depth + 1)); - } else { - unzippedFiles.add(fileResource); - } - } - } - log.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size()); - return unzippedFiles; - } - - private static class TempFileResource extends FileSystemResource { - private final TempFile tempFile; - - public TempFileResource(TempFile tempFile) { - super(tempFile.getFile()); - this.tempFile = tempFile; - } - - public TempFile getTempFile() { - return tempFile; - } - } } 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 5c45f5ab0f..f1b085dea2 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 @@ -64,6 +64,7 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.micrometer.common.util.StringUtils; import io.swagger.v3.oas.annotations.Operation; @@ -78,6 +79,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.ServerCertificateServiceInterface; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @RestController @@ -104,13 +107,15 @@ public class CertSignController { private final CustomPDFDocumentFactory pdfDocumentFactory; private final ServerCertificateServiceInterface serverCertificateService; + private final TempFileManager tempFileManager; public CertSignController( CustomPDFDocumentFactory pdfDocumentFactory, - @Autowired(required = false) - ServerCertificateServiceInterface serverCertificateService) { + @Autowired(required = false) ServerCertificateServiceInterface serverCertificateService, + TempFileManager tempFileManager) { this.pdfDocumentFactory = pdfDocumentFactory; this.serverCertificateService = serverCertificateService; + this.tempFileManager = tempFileManager; } public static void sign( @@ -163,8 +168,8 @@ public class CertSignController { "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") - public ResponseEntity signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) - throws Exception { + public ResponseEntity signPDFWithCert( + @ModelAttribute SignPDFWithCertRequest request) throws Exception { MultipartFile pdf = request.getFileInput(); String certType = request.getCertType(); MultipartFile privateKeyFile = request.getPrivateKeyFile(); @@ -246,22 +251,26 @@ public class CertSignController { } CreateSignature createSignature = new CreateSignature(ks, keystorePassword.toCharArray()); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - sign( - pdfDocumentFactory, - pdf, - baos, - createSignature, - showSignature, - pageNumber, - name, - location, - reason, - showLogo); + TempFile signedOut = tempFileManager.createManagedTempFile(".pdf"); + try (OutputStream os = new FileOutputStream(signedOut.getFile())) { + sign( + pdfDocumentFactory, + pdf, + os, + createSignature, + showSignature, + pageNumber, + name, + location, + reason, + showLogo); + } catch (IOException e) { + signedOut.close(); + throw e; + } // Return the signed PDF - return WebResponseUtils.bytesToWebResponse( - baos.toByteArray(), - GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_signed.pdf")); + return WebResponseUtils.pdfFileToWebResponse( + signedOut, GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_signed.pdf")); } private MultipartFile validateFilePresent( 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 567f1dd2ab..e9fa41ff05 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 @@ -9,6 +9,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -22,6 +23,7 @@ import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @SecurityApi @@ -29,6 +31,7 @@ import stirling.software.common.util.WebResponseUtils; public class PasswordController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-password") @StandardPdfResponse @@ -37,8 +40,8 @@ public class PasswordController { description = "This endpoint removes the password from a protected PDF file. Users need to" + " provide the existing password. Input:PDF Output:PDF Type:SISO") - public ResponseEntity removePassword(@ModelAttribute PDFPasswordRequest request) - throws IOException { + public ResponseEntity removePassword( + @ModelAttribute PDFPasswordRequest request) throws IOException { MultipartFile fileInput = request.getFileInput(); String password = request.getPassword(); @@ -47,7 +50,8 @@ public class PasswordController { return WebResponseUtils.pdfDocToWebResponse( document, GeneralUtils.generateFilename( - fileInput.getOriginalFilename(), "_password_removed.pdf")); + fileInput.getOriginalFilename(), "_password_removed.pdf"), + tempFileManager); } catch (IOException e) { // Handle password errors specifically if (ExceptionUtils.isPasswordError(e)) { @@ -66,8 +70,8 @@ public class PasswordController { "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") - public ResponseEntity addPassword(@ModelAttribute AddPasswordRequest request) - throws IOException { + public ResponseEntity addPassword( + @ModelAttribute AddPasswordRequest request) throws IOException { MultipartFile fileInput = request.getFileInput(); String ownerPassword = request.getOwnerPassword(); String password = request.getPassword(); @@ -108,11 +112,13 @@ public class PasswordController { return WebResponseUtils.pdfDocToWebResponse( document, GeneralUtils.generateFilename( - fileInput.getOriginalFilename(), "_permissions.pdf")); + fileInput.getOriginalFilename(), "_permissions.pdf"), + tempFileManager); return WebResponseUtils.pdfDocToWebResponse( document, GeneralUtils.generateFilename( - fileInput.getOriginalFilename(), "_passworded.pdf")); + fileInput.getOriginalFilename(), "_passworded.pdf"), + tempFileManager); } } } 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 0011fdb34d..377529b5fb 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,7 +1,6 @@ package stirling.software.SPDF.controller.api.security; import java.awt.Color; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -40,6 +39,7 @@ import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -64,6 +64,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PdfUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.propertyeditor.StringToArrayListPropertyEditor; @@ -85,6 +87,7 @@ public class RedactController { private static final COSString EMPTY_COS_STRING = new COSString(""); private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; private String removeFileExtension(String filename) { return GeneralUtils.removeExtension(filename); @@ -105,8 +108,8 @@ public class RedactController { "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") - public ResponseEntity redactPDF(@ModelAttribute ManualRedactPdfRequest request) - throws IOException { + public ResponseEntity redactPDF( + @ModelAttribute ManualRedactPdfRequest request) throws IOException { MultipartFile file = request.getFileInput(); List redactionAreas = request.getRedactions(); @@ -120,30 +123,24 @@ public class RedactController { if (Boolean.TRUE.equals(request.getConvertPDFToImage())) { try (PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document)) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - convertedPdf.save(baos); - byte[] pdfContent = baos.toByteArray(); - - return WebResponseUtils.bytesToWebResponse( - pdfContent, + return WebResponseUtils.pdfDocToWebResponse( + convertedPdf, removeFileExtension( Objects.requireNonNull( Filenames.toSimpleFileName( file.getOriginalFilename()))) - + "_redacted.pdf"); + + "_redacted.pdf", + tempFileManager); } } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); - byte[] pdfContent = baos.toByteArray(); - - return WebResponseUtils.bytesToWebResponse( - pdfContent, + return WebResponseUtils.pdfDocToWebResponse( + document, removeFileExtension( Objects.requireNonNull( Filenames.toSimpleFileName(file.getOriginalFilename()))) - + "_redacted.pdf"); + + "_redacted.pdf", + tempFileManager); } } @@ -504,7 +501,8 @@ public class RedactController { "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") - public ResponseEntity redactPdf(@ModelAttribute RedactPdfRequest request) { + public ResponseEntity redactPdf( + @ModelAttribute RedactPdfRequest request) { String[] listOfText = request.getListOfText().split("\n"); boolean useRegex = Boolean.TRUE.equals(request.getUseRegex()); boolean wholeWordSearchBool = Boolean.TRUE.equals(request.getWholeWordSearch()); @@ -545,20 +543,15 @@ public class RedactController { if (allFoundTextsByPage.isEmpty()) { log.info("No text found matching redaction patterns"); - byte[] originalContent; - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - document.save(baos); - originalContent = baos.toByteArray(); - } - - return WebResponseUtils.bytesToWebResponse( - originalContent, + return WebResponseUtils.pdfDocToWebResponse( + document, removeFileExtension( Objects.requireNonNull( Filenames.toSimpleFileName( request.getFileInput() .getOriginalFilename()))) - + "_redacted.pdf"); + + "_redacted.pdf", + tempFileManager); } boolean fallbackToBoxOnlyMode; @@ -587,7 +580,7 @@ public class RedactController { findTextToRedact( fallbackDocument, listOfText, useRegex, wholeWordSearchBool); - byte[] pdfContent = + TempFile finalized = finalizeRedaction( fallbackDocument, allFoundTextsByPage, @@ -596,8 +589,8 @@ public class RedactController { request.getConvertPDFToImage(), false); // Box-only mode, use original box sizes - return WebResponseUtils.bytesToWebResponse( - pdfContent, + return WebResponseUtils.pdfFileToWebResponse( + finalized, removeFileExtension( Objects.requireNonNull( Filenames.toSimpleFileName( @@ -606,7 +599,7 @@ public class RedactController { + "_redacted.pdf"); } - byte[] pdfContent = + TempFile finalized = finalizeRedaction( document, allFoundTextsByPage, @@ -615,8 +608,8 @@ public class RedactController { request.getConvertPDFToImage(), true); // Text removal mode, use reduced box sizes - return WebResponseUtils.bytesToWebResponse( - pdfContent, + return WebResponseUtils.pdfFileToWebResponse( + finalized, removeFileExtension( Objects.requireNonNull( Filenames.toSimpleFileName( @@ -733,7 +726,7 @@ public class RedactController { } } - private byte[] finalizeRedaction( + private TempFile finalizeRedaction( PDDocument document, Map> allFoundTextsByPage, String colorString, @@ -759,29 +752,37 @@ public class RedactController { try (PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document)) { cleanDocumentMetadata(convertedPdf); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - convertedPdf.save(baos); - byte[] out = baos.toByteArray(); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + convertedPdf.save(tempOut.getFile()); + } catch (IOException e) { + tempOut.close(); + throw e; + } log.info( "Redaction finalized (image mode): {} pages ➜ {} KB", convertedPdf.getNumberOfPages(), - out.length / 1024); + tempOut.getFile().length() / 1024); - return out; + return tempOut; } } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); - byte[] out = baos.toByteArray(); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + document.save(tempOut.getFile()); + } catch (IOException e) { + tempOut.close(); + throw e; + } log.info( "Redaction finalized: {} pages ➜ {} KB", document.getNumberOfPages(), - out.length / 1024); + tempOut.getFile().length() / 1024); - return out; + return tempOut; } private void cleanDocumentMetadata(PDDocument document) { 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 91644747e6..f23d63765e 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 @@ -11,6 +11,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -22,6 +23,7 @@ import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @SecurityApi @@ -29,6 +31,7 @@ import stirling.software.common.util.WebResponseUtils; public class RemoveCertSignController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-cert-sign") @StandardPdfResponse @@ -37,7 +40,7 @@ public class RemoveCertSignController { description = "This endpoint accepts a PDF file and returns the PDF file without the digital" + " signature. Input:PDF, Output:PDF Type:SISO") - public ResponseEntity removeCertSignPDF(@ModelAttribute PDFFile request) + public ResponseEntity removeCertSignPDF(@ModelAttribute PDFFile request) throws Exception { MultipartFile pdf = request.getFileInput(); @@ -63,7 +66,8 @@ public class RemoveCertSignController { // Return the modified PDF as a response return WebResponseUtils.pdfDocToWebResponse( document, - GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_unsigned.pdf")); + GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_unsigned.pdf"), + tempFileManager); } } } 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 9a6f44692f..7db9f2a8a0 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 @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api.security; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; @@ -29,6 +28,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -41,6 +41,7 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @Slf4j @@ -49,6 +50,7 @@ import stirling.software.common.util.WebResponseUtils; public class SanitizeController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/sanitize-pdf") @StandardPdfResponse @@ -57,8 +59,8 @@ public class SanitizeController { description = "This endpoint processes a PDF file and removes specific elements based on the" + " provided options. Input:PDF Output:PDF Type:SISO") - public ResponseEntity sanitizePDF(@ModelAttribute SanitizePdfRequest request) - throws IOException { + public ResponseEntity sanitizePDF( + @ModelAttribute SanitizePdfRequest request) throws IOException { MultipartFile inputFile = request.getFileInput(); boolean removeJavaScript = Boolean.TRUE.equals(request.getRemoveJavaScript()); boolean removeEmbeddedFiles = Boolean.TRUE.equals(request.getRemoveEmbeddedFiles()); @@ -92,14 +94,11 @@ public class SanitizeController { sanitizeFonts(document); } - // Save the sanitized document to output stream - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - document.save(outputStream); - - return WebResponseUtils.bytesToWebResponse( - outputStream.toByteArray(), + return WebResponseUtils.pdfDocToWebResponse( + document, GeneralUtils.generateFilename( - inputFile.getOriginalFilename(), "_sanitized.pdf")); + inputFile.getOriginalFilename(), "_sanitized.pdf"), + tempFileManager); } } 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 f4259e682c..ef744330f7 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 @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api.security; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -33,6 +32,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -46,6 +46,8 @@ import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @Slf4j @@ -74,6 +76,7 @@ public class TimestampController { private final CustomPDFDocumentFactory pdfDocumentFactory; private final ApplicationProperties applicationProperties; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/timestamp-pdf") @StandardPdfResponse @@ -84,8 +87,8 @@ public class TimestampController { + " 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") - public ResponseEntity timestampPdf(@ModelAttribute TimestampPdfRequest request) - throws Exception { + public ResponseEntity timestampPdf( + @ModelAttribute TimestampPdfRequest request) throws Exception { MultipartFile inputFile = request.getFileInput(); ApplicationProperties.Security.Timestamp tsConfig = applicationProperties.getSecurity().getTimestamp(); @@ -124,9 +127,10 @@ public class TimestampController { + " via settings.yml (security.timestamp.customTsaUrls)."); } - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - - try (PDDocument document = pdfDocumentFactory.load(inputFile)) { + TempFile tempOutputFile = tempFileManager.createManagedTempFile(".pdf"); + try (PDDocument document = pdfDocumentFactory.load(inputFile); + OutputStream outputStream = + java.nio.file.Files.newOutputStream(tempOutputFile.getPath())) { PDSignature signature = new PDSignature(); signature.setType(COSName.DOC_TIME_STAMP); signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); @@ -136,10 +140,13 @@ public class TimestampController { document.addSignature(signature, content -> requestTimestampToken(content, tsaUrl)); document.saveIncremental(outputStream); + } catch (Exception e) { + tempOutputFile.close(); + throw e; } - return WebResponseUtils.bytesToWebResponse( - outputStream.toByteArray(), + return WebResponseUtils.pdfFileToWebResponse( + tempOutputFile, GeneralUtils.generateFilename(inputFile.getOriginalFilename(), "_timestamped.pdf")); } @@ -163,7 +170,8 @@ public class TimestampController { TimeStampRequest tsaRequest = generator.generate(digestAlgorithm, hash, nonce); byte[] requestBytes = tsaRequest.getEncoded(); - // Contact the TSA server (redirects disabled to prevent SSRF via redirect) + // Contact the TSA server — tsaUrl is validated against an allowlist above, + // and redirects are disabled below to prevent SSRF via redirect. connection = (HttpURLConnection) URI.create(tsaUrl).toURL().openConnection(); connection.setInstanceFollowRedirects(false); connection.setDoOutput(true); 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 773c0e21f2..5e51fd55c6 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 @@ -30,6 +30,7 @@ import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -45,6 +46,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PdfUtils; import stirling.software.common.util.RegexPatternUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @SecurityApi @@ -52,6 +54,7 @@ import stirling.software.common.util.WebResponseUtils; public class WatermarkController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @InitBinder public void initBinder(WebDataBinder binder) { @@ -73,8 +76,8 @@ public class WatermarkController { "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") - public ResponseEntity addWatermark(@Valid @ModelAttribute AddWatermarkRequest request) - throws IOException, Exception { + public ResponseEntity addWatermark( + @Valid @ModelAttribute AddWatermarkRequest request) throws IOException, Exception { MultipartFile pdfFile = request.getFileInput(); String pdfFileName = pdfFile.getOriginalFilename(); if (pdfFileName != null && (pdfFileName.contains("..") || pdfFileName.startsWith("/"))) { @@ -151,14 +154,16 @@ public class WatermarkController { return WebResponseUtils.pdfDocToWebResponse( convertedPdf, GeneralUtils.generateFilename( - pdfFile.getOriginalFilename(), "_watermarked.pdf")); + pdfFile.getOriginalFilename(), "_watermarked.pdf"), + tempFileManager); } } else { // Return the watermarked PDF as a response return WebResponseUtils.pdfDocToWebResponse( document, GeneralUtils.generateFilename( - pdfFile.getOriginalFilename(), "_watermarked.pdf")); + pdfFile.getOriginalFilename(), "_watermarked.pdf"), + tempFileManager); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureImageController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureImageController.java index 5d69d60c8c..92f9dc8cab 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureImageController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureImageController.java @@ -11,21 +11,18 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import io.swagger.v3.oas.annotations.tags.Tag; + import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.service.SharedSignatureService; import stirling.software.common.service.PersonalSignatureServiceInterface; import stirling.software.common.service.UserServiceInterface; -/** - * Unified signature image controller that works for both authenticated and unauthenticated users. - * Uses composition pattern: - Core SharedSignatureService (always available): reads shared - * signatures - PersonalSignatureService (proprietary, optional): reads personal signatures For - * authenticated signature management (save/delete), see proprietary SignatureController. - */ @Slf4j @RestController @RequestMapping("/api/v1/general") +@Tag(name = "Signature Assets", description = "Retrieve saved signature images") public class SignatureImageController { private final SharedSignatureService sharedSignatureService; 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 5b820e6073..013aaa01ef 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 @@ -4,6 +4,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -28,7 +29,7 @@ public class ConvertPDFToMarkdown { summary = "Convert PDF to Markdown", description = "This endpoint converts a PDF file to Markdown format. Input:PDF Output:Markdown Type:SISO") - public ResponseEntity processPdfToMarkdown(@ModelAttribute PDFFile file) + public ResponseEntity processPdfToMarkdown(@ModelAttribute PDFFile file) throws Exception { MultipartFile inputFile = file.getFileInput(); PDFToFile pdfToFile = new PDFToFile(tempFileManager); diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java index 030a0df42b..43695de3d4 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java @@ -13,7 +13,8 @@ public class RotatePDFRequest extends PDFFile { @Schema( description = - "The angle by which to rotate the PDF file. This should be a multiple of 90.", + "The clockwise angle by which to rotate the PDF file. Must be a multiple of" + + " 90.", type = "integer", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"0", "90", "180", "270"}) diff --git a/app/core/src/main/java/stirling/software/SPDF/pdf/TextFinder.java b/app/core/src/main/java/stirling/software/SPDF/pdf/TextFinder.java index aa727035aa..a49476004e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/pdf/TextFinder.java +++ b/app/core/src/main/java/stirling/software/SPDF/pdf/TextFinder.java @@ -34,6 +34,7 @@ public class TextFinder extends PDFTextStripper { this.useRegex = useRegex; this.wholeWordSearch = wholeWordSearch; this.setWordSeparator(" "); + this.setLineSeparator("\n"); } @Override 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 e60c74e223..b0d2e5a4a0 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 @@ -6,6 +6,7 @@ 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; @@ -30,7 +31,14 @@ import tools.jackson.databind.ObjectMapper; @Service @Slf4j -public class ApiDocService { +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); private final Map apiDocumentation = new HashMap<>(); @@ -149,6 +157,7 @@ public class ApiDocService { return endpoint.areParametersValid(parameters); } + @Override public boolean isMultiInput(String operationName) { if (apiDocsJsonRootNode == null || apiDocumentation.isEmpty()) { loadApiDocumentation(); @@ -166,5 +175,36 @@ public class ApiDocService { } 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/resources/application.properties b/app/core/src/main/resources/application.properties index 751eabd77e..701adb2c2c 100644 --- a/app/core/src/main/resources/application.properties +++ b/app/core/src/main/resources/application.properties @@ -25,6 +25,11 @@ server.http2.enabled=true # Enable virtual threads (Java 21+, pinning fix in Java 25) spring.threads.virtual.enabled=true +# Only run security filters on REQUEST and ERROR dispatches (not ASYNC). +# StreamingResponseBody triggers an ASYNC dispatch on completion; without this, +# Spring Security re-evaluates authorization after the response is already committed. +spring.security.filter.dispatcher-types=REQUEST,ERROR + # Response compression server.compression.enabled=true server.compression.min-response-size=1024 diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index c78a515bd3..2540c2f512 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -325,6 +325,11 @@ processExecutor: ghostscriptTimeoutMinutes: 30 ocrMyPdfTimeoutMinutes: 30 +aiEngine: + enabled: false # Set to 'true' to enable the AI engine integration + url: http://localhost:5001 # URL of the Python AI engine + timeoutSeconds: 120 # Timeout in seconds for AI engine requests + pdfEditor: fallback-font: classpath:/static/fonts/NotoSans-Regular.ttf # Override to point at a custom fallback font cache: diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/BookletImpositionControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/BookletImpositionControllerTest.java index 93e819c8c3..0e260e3822 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/BookletImpositionControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/BookletImpositionControllerTest.java @@ -1,8 +1,10 @@ package stirling.software.SPDF.controller.api; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -11,6 +13,7 @@ import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -21,17 +24,47 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.general.BookletImpositionRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class BookletImpositionControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @TempDir Path tempDir; @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private BookletImpositionController 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; + }); + } + private MockMultipartFile createRealPdf(int numPages) throws IOException { Path path = tempDir.resolve("test.pdf"); try (PDDocument doc = new PDDocument()) { @@ -61,11 +94,12 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); - try (PDDocument result = Loader.loadPDF(response.getBody())) { + assertThat(drainBody(response)).isNotEmpty(); + try (PDDocument result = Loader.loadPDF(drainBody(response))) { assertThat(result.getNumberOfPages()).isGreaterThan(0); } } @@ -92,10 +126,11 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); + assertThat(drainBody(response)).isNotEmpty(); } @Test @@ -109,7 +144,8 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -126,7 +162,8 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -143,7 +180,8 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -160,7 +198,8 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -177,7 +216,8 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -192,7 +232,8 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -220,7 +261,8 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/CropControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/CropControllerTest.java index 3c526a84c5..05389a840a 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/CropControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/CropControllerTest.java @@ -1,9 +1,11 @@ package stirling.software.SPDF.controller.api; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import java.awt.image.BufferedImage; +import java.io.File; import java.io.IOException; import java.lang.reflect.Method; import java.nio.file.Files; @@ -29,21 +31,47 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.general.CropPdfForm; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) @DisplayName("CropController Tests") class CropControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @TempDir Path tempDir; @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private CropController cropController; private TestPdfFactory pdfFactory; @BeforeEach - void setUp() { + 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; + }); pdfFactory = new TestPdfFactory(); } @@ -177,7 +205,7 @@ class CropControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument)) .thenReturn(newDocument); - ResponseEntity response = cropController.cropPdf(request); + ResponseEntity response = cropController.cropPdf(request); assertThat(response) .isNotNull() @@ -214,7 +242,7 @@ class CropControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument)) .thenReturn(newDocument); - ResponseEntity response = cropController.cropPdf(request); + ResponseEntity response = cropController.cropPdf(request); assertThat(response).isNotNull(); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -234,7 +262,7 @@ class CropControllerTest { private TestPdfFactory autoCropPdfFactory; @BeforeEach - void setUp() { + void setUp() throws Exception { autoCropPdfFactory = new TestPdfFactory(); } @@ -254,13 +282,13 @@ class CropControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)) .thenReturn(newDoc); - ResponseEntity response = cropController.cropPdf(request); + ResponseEntity response = cropController.cropPdf(request); assertThat(response).isNotNull(); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); + assertThat(drainBody(response)).isNotEmpty(); - try (PDDocument result = Loader.loadPDF(response.getBody())) { + try (PDDocument result = Loader.loadPDF(drainBody(response))) { assertThat(result.getNumberOfPages()).isEqualTo(1); PDPage page = result.getPage(0); @@ -285,13 +313,13 @@ class CropControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)) .thenReturn(newDoc); - ResponseEntity response = cropController.cropPdf(request); + ResponseEntity response = cropController.cropPdf(request); assertThat(response).isNotNull(); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); Assertions.assertNotNull(response.getBody()); - try (PDDocument result = Loader.loadPDF(response.getBody())) { + try (PDDocument result = Loader.loadPDF(drainBody(response))) { assertThat(result.getNumberOfPages()).isEqualTo(1); } } @@ -648,7 +676,7 @@ class CropControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument)) .thenReturn(newDocument); - ResponseEntity response = cropController.cropPdf(request); + ResponseEntity response = cropController.cropPdf(request); assertThat(response).isNotNull(); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -676,7 +704,7 @@ class CropControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument)) .thenReturn(newDocument); - ResponseEntity response = cropController.cropPdf(request); + ResponseEntity response = cropController.cropPdf(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); verify(mockDocument, times(1)).close(); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/EditTableOfContentsControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/EditTableOfContentsControllerTest.java index 9ff4687d86..5f8f459a9d 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/EditTableOfContentsControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/EditTableOfContentsControllerTest.java @@ -1,10 +1,12 @@ package stirling.software.SPDF.controller.api; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; -import java.io.ByteArrayOutputStream; +import java.io.File; import java.lang.reflect.Method; +import java.nio.file.Files; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -26,10 +28,13 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.controller.api.EditTableOfContentsController.BookmarkItem; import stirling.software.SPDF.model.api.EditTableOfContentsRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import tools.jackson.core.type.TypeReference; import tools.jackson.databind.ObjectMapper; @@ -40,6 +45,7 @@ class EditTableOfContentsControllerTest { @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private ObjectMapper objectMapper; + @Mock private TempFileManager tempFileManager; @InjectMocks private EditTableOfContentsController editTableOfContentsController; @@ -53,7 +59,19 @@ class EditTableOfContentsControllerTest { private PDOutlineItem mockOutlineItem; @BeforeEach - void setUp() { + 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; + }); mockFile = new MockMultipartFile( "file", @@ -226,18 +244,19 @@ class EditTableOfContentsControllerTest { when(mockDocument.getNumberOfPages()).thenReturn(5); when(mockDocument.getPage(0)).thenReturn(mockPage1); - // Mock saving behavior - doAnswer( - invocation -> { - ByteArrayOutputStream baos = invocation.getArgument(0); - baos.write("mocked pdf content".getBytes()); + lenient() + .doAnswer( + inv -> { + File f = inv.getArgument(0); + java.nio.file.Files.write(f.toPath(), "mock pdf".getBytes()); return null; }) .when(mockDocument) - .save(any(ByteArrayOutputStream.class)); + .save(any(File.class)); // When - ResponseEntity result = editTableOfContentsController.editTableOfContents(request); + ResponseEntity result = + editTableOfContentsController.editTableOfContents(request); // Then assertNotNull(result); @@ -289,17 +308,19 @@ class EditTableOfContentsControllerTest { when(mockDocument.getPage(0)).thenReturn(mockPage1); when(mockDocument.getPage(1)).thenReturn(mockPage2); - doAnswer( - invocation -> { - ByteArrayOutputStream baos = invocation.getArgument(0); - baos.write("mocked pdf content".getBytes()); + lenient() + .doAnswer( + inv -> { + File f = inv.getArgument(0); + java.nio.file.Files.write(f.toPath(), "mock pdf".getBytes()); return null; }) .when(mockDocument) - .save(any(ByteArrayOutputStream.class)); + .save(any(File.class)); // When - ResponseEntity result = editTableOfContentsController.editTableOfContents(request); + ResponseEntity result = + editTableOfContentsController.editTableOfContents(request); // Then assertNotNull(result); @@ -341,17 +362,19 @@ class EditTableOfContentsControllerTest { when(mockDocument.getPage(0)).thenReturn(mockPage1); // For negative page number when(mockDocument.getPage(4)).thenReturn(mockPage2); // For page number exceeding bounds - doAnswer( - invocation -> { - ByteArrayOutputStream baos = invocation.getArgument(0); - baos.write("mocked pdf content".getBytes()); + lenient() + .doAnswer( + inv -> { + File f = inv.getArgument(0); + java.nio.file.Files.write(f.toPath(), "mock pdf".getBytes()); return null; }) .when(mockDocument) - .save(any(ByteArrayOutputStream.class)); + .save(any(File.class)); // When - ResponseEntity result = editTableOfContentsController.editTableOfContents(request); + ResponseEntity result = + editTableOfContentsController.editTableOfContents(request); // Then assertNotNull(result); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/MultiPageLayoutControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/MultiPageLayoutControllerTest.java index 0a1f3bc3b1..28276a3f39 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/MultiPageLayoutControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/MultiPageLayoutControllerTest.java @@ -1,5 +1,12 @@ package stirling.software.SPDF.controller.api; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; + +import java.io.File; +import java.nio.file.Files; + import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.junit.jupiter.api.Assertions; @@ -15,14 +22,28 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class MultiPageLayoutControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private MultiPageLayoutController controller; @@ -30,7 +51,19 @@ class MultiPageLayoutControllerTest { private MockMultipartFile fileNoExt; @BeforeEach - void setup() { + 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; + }); fileWithExt = new MockMultipartFile( "fileInput", "test.pdf", "application/pdf", new byte[] {1, 2, 3}); @@ -64,11 +97,11 @@ class MultiPageLayoutControllerTest { req.setAddBorder(Boolean.FALSE); req.setFileInput(fileWithExt); - ResponseEntity resp = controller.mergeMultiplePagesIntoOne(req); + ResponseEntity resp = controller.mergeMultiplePagesIntoOne(req); Assertions.assertEquals(HttpStatus.OK, resp.getStatusCode()); Assertions.assertEquals(MediaType.APPLICATION_PDF, resp.getHeaders().getContentType()); Assertions.assertNotNull(resp.getBody()); - Assertions.assertTrue(resp.getBody().length > 0); + Assertions.assertTrue(drainBody(resp).length > 0); Assertions.assertEquals( "test_multi_page_layout.pdf", resp.getHeaders().getContentDisposition().getFilename()); @@ -89,11 +122,11 @@ class MultiPageLayoutControllerTest { req.setAddBorder(Boolean.TRUE); req.setFileInput(fileWithExt); - ResponseEntity resp = controller.mergeMultiplePagesIntoOne(req); + ResponseEntity resp = controller.mergeMultiplePagesIntoOne(req); Assertions.assertEquals(HttpStatus.OK, resp.getStatusCode()); Assertions.assertEquals(MediaType.APPLICATION_PDF, resp.getHeaders().getContentType()); Assertions.assertNotNull(resp.getBody()); - Assertions.assertTrue(resp.getBody().length > 0); + Assertions.assertTrue(drainBody(resp).length > 0); } @Test @@ -112,7 +145,7 @@ class MultiPageLayoutControllerTest { req.setAddBorder(Boolean.TRUE); req.setFileInput(fileNoExt); - ResponseEntity resp = controller.mergeMultiplePagesIntoOne(req); + ResponseEntity resp = controller.mergeMultiplePagesIntoOne(req); Assertions.assertEquals( "name_multi_page_layout.pdf", resp.getHeaders().getContentDisposition().getFilename()); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/PdfOverlayControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/PdfOverlayControllerTest.java index 0983a0bb7c..66b3eebff5 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/PdfOverlayControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/PdfOverlayControllerTest.java @@ -2,16 +2,22 @@ package stirling.software.SPDF.controller.api; import static org.junit.jupiter.api.Assertions.*; 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.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,17 +30,47 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.general.OverlayPdfsRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class PdfOverlayControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @TempDir Path tempDir; @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private PdfOverlayController 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; + }); + } + private byte[] createPdf(int numPages) throws IOException { try (PDDocument doc = new PDDocument()) { for (int i = 0; i < numPages; i++) { @@ -71,12 +107,12 @@ class PdfOverlayControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes())); - ResponseEntity response = controller.overlayPdfs(request); + ResponseEntity response = controller.overlayPdfs(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -105,7 +141,7 @@ class PdfOverlayControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes())); - ResponseEntity response = controller.overlayPdfs(request); + ResponseEntity response = controller.overlayPdfs(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -137,7 +173,7 @@ class PdfOverlayControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes())); - ResponseEntity response = controller.overlayPdfs(request); + ResponseEntity response = controller.overlayPdfs(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -168,7 +204,7 @@ class PdfOverlayControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes())); - ResponseEntity response = controller.overlayPdfs(request); + ResponseEntity response = controller.overlayPdfs(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -258,7 +294,7 @@ class PdfOverlayControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes())); - ResponseEntity response = controller.overlayPdfs(request); + ResponseEntity response = controller.overlayPdfs(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -291,7 +327,7 @@ class PdfOverlayControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes())); - ResponseEntity response = controller.overlayPdfs(request); + ResponseEntity response = controller.overlayPdfs(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java index eaec8e9d19..81f85d60a2 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java @@ -1,12 +1,16 @@ package stirling.software.SPDF.controller.api; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; 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.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -15,18 +19,38 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.general.RearrangePagesRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class RearrangePagesPDFControllerTest { @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private RearrangePagesPDFController 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; + }); + } + private MockMultipartFile createMockPdf() { return new MockMultipartFile( "fileInput", "test.pdf", MediaType.APPLICATION_PDF_VALUE, new byte[] {1, 2, 3}); @@ -43,7 +67,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(mockDoc); when(mockDoc.getNumberOfPages()).thenReturn(5); - ResponseEntity response = controller.deletePages(request); + ResponseEntity response = controller.deletePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -73,7 +97,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -103,7 +127,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); verify(mockNewDoc).addPage(page1); @@ -131,7 +155,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); verify(mockNewDoc).addPage(page0); @@ -157,7 +181,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -184,7 +208,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -208,7 +232,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -232,7 +256,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -260,7 +284,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -284,7 +308,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); // 2 pages * 3 duplicates = 6 addPage calls @@ -309,7 +333,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/RotationControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/RotationControllerTest.java index ecdafc09a9..bc5f60977f 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/RotationControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/RotationControllerTest.java @@ -1,15 +1,20 @@ package stirling.software.SPDF.controller.api; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageTree; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -18,17 +23,37 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.general.RotatePDFRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) public class RotationControllerTest { @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private RotationController rotationController; + @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; + }); + } + @Test public void testRotatePDF() throws IOException { // Create a mock file @@ -50,7 +75,7 @@ public class RotationControllerTest { when(mockPage.getRotation()).thenReturn(0); // Act - ResponseEntity response = rotationController.rotatePDF(request); + ResponseEntity response = rotationController.rotatePDF(request); // Assert verify(mockPage).setRotation(90); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/ScalePagesControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/ScalePagesControllerTest.java index 41574bf3aa..4fee57b5d6 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/ScalePagesControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/ScalePagesControllerTest.java @@ -2,8 +2,10 @@ package stirling.software.SPDF.controller.api; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -11,6 +13,7 @@ import java.nio.file.Path; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -21,17 +24,47 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.general.ScalePagesRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class ScalePagesControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @TempDir Path tempDir; @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private ScalePagesController 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; + }); + } + private byte[] createRealPdf(PDRectangle pageSize, int numPages) throws IOException { try (PDDocument doc = new PDDocument()) { for (int i = 0; i < numPages; i++) { @@ -68,12 +101,12 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -90,7 +123,7 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -110,7 +143,7 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -130,7 +163,7 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -150,7 +183,7 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -187,7 +220,7 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -207,7 +240,7 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -248,7 +281,7 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsControllerTest.java index 49330eacad..2cada36a9a 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsControllerTest.java @@ -4,8 +4,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; 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.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -31,6 +34,7 @@ import org.springframework.web.multipart.MultipartFile; import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) @@ -44,6 +48,18 @@ class SplitPdfBySectionsControllerTest { @BeforeEach void setUp() throws IOException { + 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; + }); when(tempFileManager.createTempFile(anyString())) .thenAnswer( inv -> { diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFControllerTest.java index 95f0de648d..be2295eb08 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFControllerTest.java @@ -5,7 +5,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.File; @@ -19,6 +22,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import org.apache.pdfbox.pdmodel.PDDocument; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -29,6 +33,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.model.api.converters.ConvertEbookToPdfRequest; @@ -37,11 +42,22 @@ import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; import stirling.software.common.util.ProcessExecutor.Processes; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ConvertEbookToPDFControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private TempFileManager tempFileManager; @@ -49,6 +65,22 @@ class ConvertEbookToPDFControllerTest { @InjectMocks private ConvertEbookToPDFController 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; + }); + } + @Test void convertEbookToPdf_buildsCalibreCommandAndCleansUp() throws Exception { when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true); @@ -113,16 +145,14 @@ class ConvertEbookToPDFControllerTest { return execResult; }); - ResponseEntity expectedResponse = ResponseEntity.ok("result".getBytes()); - wr.when( - () -> - WebResponseUtils.pdfDocToWebResponse( - mockDocument, "ebook_convertedToPDF.pdf")) + ResponseEntity expectedResponse = + streamingOk("result".getBytes()); + wr.when(() -> WebResponseUtils.pdfFileToWebResponse(any(TempFile.class), anyString())) .thenReturn(expectedResponse); gu.when(() -> GeneralUtils.generateFilename("ebook.epub", "_convertedToPDF.pdf")) .thenReturn("ebook_convertedToPDF.pdf"); - ResponseEntity response = controller.convertEbookToPdf(request); + ResponseEntity response = controller.convertEbookToPdf(request); assertSame(expectedResponse, response); @@ -232,14 +262,11 @@ class ConvertEbookToPDFControllerTest { gu.when(() -> GeneralUtils.optimizePdfWithGhostscript(Mockito.any(byte[].class))) .thenReturn(optimizedBytes); - ResponseEntity expectedResponse = ResponseEntity.ok(optimizedBytes); - wr.when( - () -> - WebResponseUtils.bytesToWebResponse( - optimizedBytes, "ebook_convertedToPDF.pdf")) + ResponseEntity expectedResponse = streamingOk(optimizedBytes); + wr.when(() -> WebResponseUtils.pdfFileToWebResponse(any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.convertEbookToPdf(request); + ResponseEntity response = controller.convertEbookToPdf(request); assertSame(expectedResponse, response); gu.verify(() -> GeneralUtils.optimizePdfWithGhostscript(Mockito.any(byte[].class))); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDFTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDFTest.java index 2a7d138577..7f4857d830 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDFTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDFTest.java @@ -4,12 +4,18 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +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.charset.StandardCharsets; +import java.nio.file.Files; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -21,17 +27,29 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.api.converters.EmlToPdfRequest; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.CustomHtmlSanitizer; import stirling.software.common.util.EmlToPdf; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ConvertEmlToPDFTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private RuntimePathConfig runtimePathConfig; @@ -40,35 +58,51 @@ class ConvertEmlToPDFTest { @InjectMocks private ConvertEmlToPDF 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; + }); + } + @Test - void convertEmlToPdf_emptyFileReturnsBadRequest() { + void convertEmlToPdf_emptyFileReturnsBadRequest() throws java.io.IOException { MockMultipartFile emptyFile = new MockMultipartFile("fileInput", "test.eml", "message/rfc822", new byte[0]); EmlToPdfRequest request = new EmlToPdfRequest(); request.setFileInput(emptyFile); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); assertTrue( - new String(response.getBody(), StandardCharsets.UTF_8) + new String(drainBody(response), StandardCharsets.UTF_8) .contains("No file provided")); } @Test - void convertEmlToPdf_nullFilenameReturnsBadRequest() { + void convertEmlToPdf_nullFilenameReturnsBadRequest() throws java.io.IOException { MockMultipartFile file = new MockMultipartFile("fileInput", null, "message/rfc822", "content".getBytes()); EmlToPdfRequest request = new EmlToPdfRequest(); request.setFileInput(file); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); assertTrue( - new String(response.getBody(), StandardCharsets.UTF_8).contains("valid filename")); + new String(drainBody(response), StandardCharsets.UTF_8).contains("valid filename")); } @Test @@ -79,24 +113,24 @@ class ConvertEmlToPDFTest { EmlToPdfRequest request = new EmlToPdfRequest(); request.setFileInput(file); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); } @Test - void convertEmlToPdf_invalidFileTypeReturnsBadRequest() { + void convertEmlToPdf_invalidFileTypeReturnsBadRequest() throws java.io.IOException { MockMultipartFile file = new MockMultipartFile("fileInput", "test.txt", "text/plain", "content".getBytes()); EmlToPdfRequest request = new EmlToPdfRequest(); request.setFileInput(file); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); assertTrue( - new String(response.getBody(), StandardCharsets.UTF_8) + new String(drainBody(response), StandardCharsets.UTF_8) .contains("valid EML or MSG")); } @@ -112,7 +146,7 @@ class ConvertEmlToPDFTest { when(runtimePathConfig.getWeasyPrintPath()).thenReturn("/usr/bin/weasyprint"); - ResponseEntity expectedResponse = ResponseEntity.ok(pdfBytes); + ResponseEntity expectedResponse = streamingOk(pdfBytes); try (MockedStatic emlMock = Mockito.mockStatic(EmlToPdf.class); MockedStatic wrMock = @@ -132,14 +166,14 @@ class ConvertEmlToPDFTest { wrMock.when( () -> - WebResponseUtils.bytesToWebResponse( - pdfBytes, "test.eml.pdf", MediaType.APPLICATION_PDF)) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertArrayEquals(pdfBytes, response.getBody()); + assertArrayEquals(pdfBytes, drainBody(response)); } } @@ -154,8 +188,8 @@ class ConvertEmlToPDFTest { request.setFileInput(file); request.setDownloadHtml(true); - ResponseEntity expectedResponse = - ResponseEntity.ok(htmlContent.getBytes(StandardCharsets.UTF_8)); + ResponseEntity expectedResponse = + streamingOk(htmlContent.getBytes(StandardCharsets.UTF_8)); try (MockedStatic emlMock = Mockito.mockStatic(EmlToPdf.class); MockedStatic wrMock = @@ -171,13 +205,11 @@ class ConvertEmlToPDFTest { wrMock.when( () -> - WebResponseUtils.bytesToWebResponse( - htmlContent.getBytes(StandardCharsets.UTF_8), - "test.eml.html", - MediaType.TEXT_HTML)) + WebResponseUtils.fileToWebResponse( + any(TempFile.class), anyString(), any(MediaType.class))) .thenReturn(expectedResponse); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -203,11 +235,11 @@ class ConvertEmlToPDFTest { eq(customHtmlSanitizer))) .thenThrow(new IOException("Parse error")); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); assertTrue( - new String(response.getBody(), StandardCharsets.UTF_8) + new String(drainBody(response), StandardCharsets.UTF_8) .contains("HTML conversion failed")); } } @@ -231,11 +263,11 @@ class ConvertEmlToPDFTest { any(), any(), any(), any(), any(), any(), any())) .thenReturn(null); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); assertTrue( - new String(response.getBody(), StandardCharsets.UTF_8) + new String(drainBody(response), StandardCharsets.UTF_8) .contains("empty output")); } } @@ -255,7 +287,7 @@ class ConvertEmlToPDFTest { when(runtimePathConfig.getWeasyPrintPath()).thenReturn("/usr/bin/weasyprint"); - ResponseEntity expectedResponse = ResponseEntity.ok(pdfBytes); + ResponseEntity expectedResponse = streamingOk(pdfBytes); try (MockedStatic emlMock = Mockito.mockStatic(EmlToPdf.class); MockedStatic wrMock = @@ -269,13 +301,11 @@ class ConvertEmlToPDFTest { wrMock.when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), - any(String.class), - any(MediaType.class))) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -300,11 +330,12 @@ class ConvertEmlToPDFTest { any(), any(), any(), any(), any(), any(), any())) .thenThrow(new InterruptedException("interrupted")); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); assertTrue( - new String(response.getBody(), StandardCharsets.UTF_8).contains("interrupted")); + new String(drainBody(response), StandardCharsets.UTF_8) + .contains("interrupted")); } } } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDFTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDFTest.java index aa9f84b93f..466ab50650 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDFTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDFTest.java @@ -3,9 +3,16 @@ package stirling.software.SPDF.controller.api.converters; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.File; +import java.nio.file.Files; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -16,6 +23,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.api.converters.HTMLToPdfRequest; @@ -23,11 +31,22 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.CustomHtmlSanitizer; import stirling.software.common.util.FileToPdf; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ConvertHtmlToPDFTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private RuntimePathConfig runtimePathConfig; @@ -36,6 +55,22 @@ class ConvertHtmlToPDFTest { @InjectMocks private ConvertHtmlToPDF 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; + }); + } + @Test void htmlToPdf_nullFileInputThrows() { HTMLToPdfRequest request = new HTMLToPdfRequest(); @@ -69,7 +104,7 @@ class ConvertHtmlToPDFTest { when(pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes)) .thenReturn(processedPdf); - ResponseEntity expectedResponse = ResponseEntity.ok(processedPdf); + ResponseEntity expectedResponse = streamingOk(processedPdf); try (MockedStatic ftpMock = Mockito.mockStatic(FileToPdf.class); MockedStatic guMock = Mockito.mockStatic(GeneralUtils.class); @@ -90,10 +125,13 @@ class ConvertHtmlToPDFTest { guMock.when(() -> GeneralUtils.generateFilename("test.html", ".pdf")) .thenReturn("test.pdf"); - wrMock.when(() -> WebResponseUtils.bytesToWebResponse(processedPdf, "test.pdf")) + wrMock.when( + () -> + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.HtmlToPdf(request); + ResponseEntity response = controller.HtmlToPdf(request); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -114,7 +152,7 @@ class ConvertHtmlToPDFTest { when(pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes)) .thenReturn(processedPdf); - ResponseEntity expectedResponse = ResponseEntity.ok(processedPdf); + ResponseEntity expectedResponse = streamingOk(processedPdf); try (MockedStatic ftpMock = Mockito.mockStatic(FileToPdf.class); MockedStatic guMock = Mockito.mockStatic(GeneralUtils.class); @@ -135,10 +173,13 @@ class ConvertHtmlToPDFTest { guMock.when(() -> GeneralUtils.generateFilename("archive.zip", ".pdf")) .thenReturn("archive.pdf"); - wrMock.when(() -> WebResponseUtils.bytesToWebResponse(processedPdf, "archive.pdf")) + wrMock.when( + () -> + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.HtmlToPdf(request); + ResponseEntity response = controller.HtmlToPdf(request); assertEquals(HttpStatus.OK, response.getStatusCode()); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFControllerTest.java index 011a3fcb61..78ce0233b1 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFControllerTest.java @@ -14,6 +14,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest; @@ -25,6 +26,16 @@ import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ConvertImgPDFControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private TempFileManager tempFileManager; diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdfTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdfTest.java index 838c91ea86..00116172cd 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdfTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdfTest.java @@ -4,10 +4,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.File; +import java.nio.file.Files; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -18,6 +25,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.api.GeneralFile; @@ -25,11 +33,22 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.CustomHtmlSanitizer; import stirling.software.common.util.FileToPdf; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ConvertMarkdownToPdfTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private RuntimePathConfig runtimePathConfig; @@ -38,6 +57,22 @@ class ConvertMarkdownToPdfTest { @InjectMocks private ConvertMarkdownToPdf 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; + }); + } + @Test void markdownToPdf_nullFileInputThrows() { GeneralFile generalFile = new GeneralFile(); @@ -71,7 +106,7 @@ class ConvertMarkdownToPdfTest { when(pdfDocumentFactory.createNewBytesBasedOnOldDocument(any(byte[].class))) .thenReturn(processedPdf); - ResponseEntity expectedResponse = ResponseEntity.ok(processedPdf); + ResponseEntity expectedResponse = streamingOk(processedPdf); try (MockedStatic ftpMock = Mockito.mockStatic(FileToPdf.class); MockedStatic guMock = Mockito.mockStatic(GeneralUtils.class); @@ -92,10 +127,13 @@ class ConvertMarkdownToPdfTest { guMock.when(() -> GeneralUtils.generateFilename("readme.md", ".pdf")) .thenReturn("readme.pdf"); - wrMock.when(() -> WebResponseUtils.bytesToWebResponse(processedPdf, "readme.pdf")) + wrMock.when( + () -> + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.markdownToPdf(generalFile); + ResponseEntity response = controller.markdownToPdf(generalFile); assertEquals(HttpStatus.OK, response.getStatusCode()); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubControllerTest.java index 65cae373a4..a9d669c052 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubControllerTest.java @@ -3,12 +3,15 @@ package stirling.software.SPDF.controller.api.converters; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -18,6 +21,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -29,6 +33,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest; @@ -38,10 +43,21 @@ import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; import stirling.software.common.util.ProcessExecutor.Processes; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class ConvertPDFToEpubControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } private static final MediaType EPUB_MEDIA_TYPE = MediaType.valueOf("application/epub+zip"); @@ -50,6 +66,22 @@ class ConvertPDFToEpubControllerTest { @InjectMocks private ConvertPDFToEpubController 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; + }); + } + @Test void convertPdfToEpub_buildsGoldenCommandAndCleansUp() throws Exception { when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true); @@ -110,7 +142,7 @@ class ConvertPDFToEpubControllerTest { gu.when(() -> GeneralUtils.generateFilename("novel.pdf", "_convertedToEPUB.epub")) .thenReturn("novel_convertedToEPUB.epub"); - ResponseEntity response = controller.convertPdfToEpub(request); + ResponseEntity response = controller.convertPdfToEpub(request); List command = commandCaptor.getValue(); assertEquals(13, command.size()); @@ -134,7 +166,7 @@ class ConvertPDFToEpubControllerTest { assertEquals( "novel_convertedToEPUB.epub", response.getHeaders().getContentDisposition().getFilename()); - assertEquals("epub", new String(response.getBody(), StandardCharsets.UTF_8)); + assertEquals("epub", new String(drainBody(response), StandardCharsets.UTF_8)); verify(tempFileManager).deleteTempDirectory(workingDir); assertEquals(workingDir, deletedDir.get()); @@ -202,7 +234,7 @@ class ConvertPDFToEpubControllerTest { gu.when(() -> GeneralUtils.generateFilename("story.pdf", "_convertedToEPUB.epub")) .thenReturn("story_convertedToEPUB.epub"); - ResponseEntity response = controller.convertPdfToEpub(request); + ResponseEntity response = controller.convertPdfToEpub(request); List command = commandCaptor.getValue(); assertTrue(command.stream().noneMatch(arg -> "--chapter".equals(arg))); @@ -220,7 +252,7 @@ class ConvertPDFToEpubControllerTest { assertEquals( "story_convertedToEPUB.epub", response.getHeaders().getContentDisposition().getFilename()); - assertEquals("epub", new String(response.getBody(), StandardCharsets.UTF_8)); + assertEquals("epub", new String(drainBody(response), StandardCharsets.UTF_8)); } finally { deleteIfExists(workingDir); } @@ -287,7 +319,7 @@ class ConvertPDFToEpubControllerTest { gu.when(() -> GeneralUtils.generateFilename("book.pdf", "_convertedToAZW3.azw3")) .thenReturn("book_convertedToAZW3.azw3"); - ResponseEntity response = controller.convertPdfToEpub(request); + ResponseEntity response = controller.convertPdfToEpub(request); List command = commandCaptor.getValue(); assertEquals("ebook-convert", command.get(0)); @@ -308,7 +340,7 @@ class ConvertPDFToEpubControllerTest { assertEquals( "book_convertedToAZW3.azw3", response.getHeaders().getContentDisposition().getFilename()); - assertEquals("azw3", new String(response.getBody(), StandardCharsets.UTF_8)); + assertEquals("azw3", new String(drainBody(response), StandardCharsets.UTF_8)); verify(tempFileManager).deleteTempDirectory(workingDir); } finally { diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelControllerTest.java index 6b5bb1e7f5..ca7a7112fa 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelControllerTest.java @@ -2,11 +2,17 @@ package stirling.software.SPDF.controller.api.converters; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +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.nio.file.Files; import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -17,18 +23,38 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class ConvertPDFToExcelControllerTest { @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private ConvertPDFToExcelController 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; + }); + } + @Test void pdfToExcel_noTablesReturnsNoContent() throws Exception { MockMultipartFile pdfFile = @@ -55,7 +81,7 @@ class ConvertPDFToExcelControllerTest { Mockito.eq(true))) .thenReturn(List.of(1)); - ResponseEntity response = controller.pdfToExcel(request); + ResponseEntity response = controller.pdfToExcel(request); // tabula may or may not find tables in an empty page assertNotNull(response); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOfficeTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOfficeTest.java index 50991742b9..d768ce1b29 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOfficeTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOfficeTest.java @@ -4,10 +4,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +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.nio.file.Files; + import org.apache.pdfbox.pdmodel.PDDocument; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -18,6 +24,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest; import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest; @@ -27,11 +34,22 @@ import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PDFToFile; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ConvertPDFToOfficeTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private TempFileManager tempFileManager; @@ -39,6 +57,22 @@ class ConvertPDFToOfficeTest { @InjectMocks private ConvertPDFToOffice 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; + }); + } + private MockMultipartFile createPdfFile() { return new MockMultipartFile( "fileInput", "document.pdf", "application/pdf", "pdf-content".getBytes()); @@ -51,7 +85,8 @@ class ConvertPDFToOfficeTest { request.setFileInput(pdfFile); request.setOutputFormat("pptx"); - ResponseEntity expectedResponse = ResponseEntity.ok("pptx-content".getBytes()); + ResponseEntity expectedResponse = + streamingOk("pptx-content".getBytes()); try (MockedStatic mock = Mockito.mockStatic(PDFToFile.class, Mockito.CALLS_REAL_METHODS)) { @@ -80,7 +115,8 @@ class ConvertPDFToOfficeTest { realDoc.addPage(new org.apache.pdfbox.pdmodel.PDPage()); when(pdfDocumentFactory.load(pdfFile)).thenReturn(realDoc); - ResponseEntity expectedResponse = ResponseEntity.ok("text content".getBytes()); + ResponseEntity expectedResponse = + streamingOk("text content".getBytes()); try (MockedStatic guMock = Mockito.mockStatic(GeneralUtils.class); MockedStatic wrMock = @@ -91,13 +127,12 @@ class ConvertPDFToOfficeTest { wrMock.when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), - eq("document.txt"), - eq(MediaType.TEXT_PLAIN))) + WebResponseUtils.fileToWebResponse( + any(TempFile.class), anyString(), any(MediaType.class))) .thenReturn(expectedResponse); - ResponseEntity response = controller.processPdfToRTForTXT(request); + ResponseEntity response = + controller.processPdfToRTForTXT(request); assertSame(expectedResponse, response); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonControllerTest.java index d9d499b17d..882c24926a 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonControllerTest.java @@ -4,34 +4,67 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.service.PdfJsonConversionService; import stirling.software.common.model.api.GeneralFile; import stirling.software.common.model.api.PDFFile; -import stirling.software.common.util.WebResponseUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class ConvertPdfJsonControllerTest { @Mock private PdfJsonConversionService pdfJsonConversionService; + @Mock private TempFileManager tempFileManager; @InjectMocks private ConvertPdfJsonController 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; + }); + } + + private static byte[] drainBody(ResponseEntity response) + throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } + @Test void convertPdfToJson_nullFileInputThrows() { PDFFile request = new PDFFile(); @@ -51,19 +84,11 @@ class ConvertPdfJsonControllerTest { when(pdfJsonConversionService.convertPdfToJson(pdfFile, false)).thenReturn(jsonBytes); - ResponseEntity expectedResponse = ResponseEntity.ok(jsonBytes); + ResponseEntity response = + controller.convertPdfToJson(request, false); - try (MockedStatic wrMock = Mockito.mockStatic(WebResponseUtils.class)) { - wrMock.when( - () -> - WebResponseUtils.bytesToWebResponse( - jsonBytes, "doc.json", MediaType.APPLICATION_JSON)) - .thenReturn(expectedResponse); - - ResponseEntity response = controller.convertPdfToJson(request, false); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - } + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); } @Test @@ -77,20 +102,10 @@ class ConvertPdfJsonControllerTest { when(pdfJsonConversionService.convertPdfToJson(pdfFile, true)).thenReturn(jsonBytes); - ResponseEntity expectedResponse = ResponseEntity.ok(jsonBytes); + ResponseEntity response = controller.convertPdfToJson(request, true); - try (MockedStatic wrMock = Mockito.mockStatic(WebResponseUtils.class)) { - wrMock.when( - () -> - WebResponseUtils.bytesToWebResponse( - jsonBytes, "doc.json", MediaType.APPLICATION_JSON)) - .thenReturn(expectedResponse); - - ResponseEntity response = controller.convertPdfToJson(request, true); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - verify(pdfJsonConversionService).convertPdfToJson(pdfFile, true); - } + assertEquals(HttpStatus.OK, response.getStatusCode()); + verify(pdfJsonConversionService).convertPdfToJson(pdfFile, true); } @Test @@ -112,16 +127,10 @@ class ConvertPdfJsonControllerTest { when(pdfJsonConversionService.convertJsonToPdf(jsonFile)).thenReturn(pdfBytes); - ResponseEntity expectedResponse = ResponseEntity.ok(pdfBytes); + ResponseEntity response = controller.convertJsonToPdf(request); - try (MockedStatic wrMock = Mockito.mockStatic(WebResponseUtils.class)) { - wrMock.when(() -> WebResponseUtils.bytesToWebResponse(pdfBytes, "doc.pdf")) - .thenReturn(expectedResponse); - - ResponseEntity response = controller.convertJsonToPdf(request); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - } + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); } @Test @@ -144,7 +153,7 @@ class ConvertPdfJsonControllerTest { when(pdfJsonConversionService.extractDocumentMetadata(eq(pdfFile), any(String.class))) .thenReturn(jsonBytes); - ResponseEntity response = controller.extractPdfMetadata(request); + ResponseEntity response = controller.extractPdfMetadata(request); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(MediaType.APPLICATION_JSON, response.getHeaders().getContentType()); @@ -168,19 +177,10 @@ class ConvertPdfJsonControllerTest { when(pdfJsonConversionService.extractSinglePage(jobId, 1)).thenReturn(jsonBytes); - ResponseEntity expectedResponse = ResponseEntity.ok(jsonBytes); + ResponseEntity response = controller.extractSinglePage(jobId, 1); - try (MockedStatic wrMock = Mockito.mockStatic(WebResponseUtils.class)) { - wrMock.when( - () -> - WebResponseUtils.bytesToWebResponse( - jsonBytes, "page_1.json", MediaType.APPLICATION_JSON)) - .thenReturn(expectedResponse); - - ResponseEntity response = controller.extractSinglePage(jobId, 1); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - } + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); } @Test @@ -190,20 +190,9 @@ class ConvertPdfJsonControllerTest { when(pdfJsonConversionService.extractPageFonts(jobId, 1)).thenReturn(jsonBytes); - ResponseEntity expectedResponse = ResponseEntity.ok(jsonBytes); + ResponseEntity response = controller.extractPageFonts(jobId, 1); - try (MockedStatic wrMock = Mockito.mockStatic(WebResponseUtils.class)) { - wrMock.when( - () -> - WebResponseUtils.bytesToWebResponse( - jsonBytes, - "page_fonts_1.json", - MediaType.APPLICATION_JSON)) - .thenReturn(expectedResponse); - - ResponseEntity response = controller.extractPageFonts(jobId, 1); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - } + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); } } 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 deleted file mode 100644 index 0b8de27f9b..0000000000 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPdfToVideoControllerTest.java +++ /dev/null @@ -1,164 +0,0 @@ -package stirling.software.SPDF.controller.api.converters; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.mock.web.MockMultipartFile; - -import stirling.software.SPDF.model.api.converters.PdfToVideoRequest; -import stirling.software.common.service.CustomPDFDocumentFactory; -import stirling.software.common.util.CheckProgramInstall; -import stirling.software.common.util.TempFileManager; - -@ExtendWith(MockitoExtension.class) -class ConvertPdfToVideoControllerTest { - - @Mock private CustomPDFDocumentFactory pdfDocumentFactory; - @Mock private TempFileManager tempFileManager; - - @InjectMocks private ConvertPdfToVideoController controller; - - @Test - void convertPdfToVideo_ffmpegNotAvailableThrows() { - PdfToVideoRequest request = new PdfToVideoRequest(); - MockMultipartFile pdfFile = - new MockMultipartFile( - "fileInput", "doc.pdf", "application/pdf", "content".getBytes()); - request.setFileInput(pdfFile); - - try (MockedStatic mock = - Mockito.mockStatic(CheckProgramInstall.class)) { - mock.when(CheckProgramInstall::isFfmpegAvailable).thenReturn(false); - - assertThrows(Exception.class, () -> controller.convertPdfToVideo(request)); - } - } - - @Test - void convertPdfToVideo_nullFileThrows() { - PdfToVideoRequest request = new PdfToVideoRequest(); - request.setFileInput(null); - - try (MockedStatic mock = - Mockito.mockStatic(CheckProgramInstall.class)) { - mock.when(CheckProgramInstall::isFfmpegAvailable).thenReturn(true); - - assertThrows(Exception.class, () -> controller.convertPdfToVideo(request)); - } - } - - @Test - void convertPdfToVideo_emptyFileThrows() { - PdfToVideoRequest request = new PdfToVideoRequest(); - MockMultipartFile emptyFile = - new MockMultipartFile("fileInput", "doc.pdf", "application/pdf", new byte[0]); - request.setFileInput(emptyFile); - - try (MockedStatic mock = - Mockito.mockStatic(CheckProgramInstall.class)) { - mock.when(CheckProgramInstall::isFfmpegAvailable).thenReturn(true); - - assertThrows(Exception.class, () -> controller.convertPdfToVideo(request)); - } - } - - @Test - void convertPdfToVideo_nonPdfContentTypeReturnsBadRequest() throws Exception { - PdfToVideoRequest request = new PdfToVideoRequest(); - MockMultipartFile txtFile = - new MockMultipartFile("fileInput", "doc.txt", "text/plain", "content".getBytes()); - request.setFileInput(txtFile); - - try (MockedStatic mock = - Mockito.mockStatic(CheckProgramInstall.class)) { - mock.when(CheckProgramInstall::isFfmpegAvailable).thenReturn(true); - - ResponseEntity response = controller.convertPdfToVideo(request); - - assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - } - } - - @Test - void convertPdfToVideo_invalidOpacityThrows() { - PdfToVideoRequest request = new PdfToVideoRequest(); - MockMultipartFile pdfFile = - new MockMultipartFile( - "fileInput", "doc.pdf", "application/pdf", "content".getBytes()); - request.setFileInput(pdfFile); - request.setOpacity(1.5f); - - try (MockedStatic mock = - Mockito.mockStatic(CheckProgramInstall.class)) { - mock.when(CheckProgramInstall::isFfmpegAvailable).thenReturn(true); - - assertThrows(Exception.class, () -> controller.convertPdfToVideo(request)); - } - } - - @Test - void convertPdfToVideo_negativeOpacityThrows() { - PdfToVideoRequest request = new PdfToVideoRequest(); - MockMultipartFile pdfFile = - new MockMultipartFile( - "fileInput", "doc.pdf", "application/pdf", "content".getBytes()); - request.setFileInput(pdfFile); - request.setOpacity(-0.1f); - - try (MockedStatic mock = - Mockito.mockStatic(CheckProgramInstall.class)) { - mock.when(CheckProgramInstall::isFfmpegAvailable).thenReturn(true); - - assertThrows(Exception.class, () -> controller.convertPdfToVideo(request)); - } - } - - @Test - void convertPdfToVideo_negativeSecondsPerPageThrows() { - PdfToVideoRequest request = new PdfToVideoRequest(); - MockMultipartFile pdfFile = - new MockMultipartFile( - "fileInput", "doc.pdf", "application/pdf", "content".getBytes()); - request.setFileInput(pdfFile); - request.setSecondsPerPage(-1); - - try (MockedStatic mock = - Mockito.mockStatic(CheckProgramInstall.class)) { - mock.when(CheckProgramInstall::isFfmpegAvailable).thenReturn(true); - - assertThrows(Exception.class, () -> controller.convertPdfToVideo(request)); - } - } - - @Test - void convertPdfToVideo_zeroSecondsPerPageThrows() { - PdfToVideoRequest request = new PdfToVideoRequest(); - MockMultipartFile pdfFile = - new MockMultipartFile( - "fileInput", "doc.pdf", "application/pdf", "content".getBytes()); - request.setFileInput(pdfFile); - request.setSecondsPerPage(0); - - try (MockedStatic mock = - Mockito.mockStatic(CheckProgramInstall.class)) { - mock.when(CheckProgramInstall::isFfmpegAvailable).thenReturn(true); - - assertThrows(Exception.class, () -> controller.convertPdfToVideo(request)); - } - } - - @Test - void controllerIsConstructed() { - assertNotNull(controller); - } -} diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDFTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDFTest.java index 642f16f3b8..7d727cec22 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDFTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDFTest.java @@ -3,11 +3,17 @@ package stirling.software.SPDF.controller.api.converters; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; 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.File; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -16,20 +22,31 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.converters.SvgToPdfRequest; import stirling.software.SPDF.utils.SvgToPdf; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.SvgSanitizer; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ConvertSvgToPDFTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private SvgSanitizer svgSanitizer; @@ -37,16 +54,32 @@ class ConvertSvgToPDFTest { @InjectMocks private ConvertSvgToPDF 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; + }); + } + @Test - void convertSvgToPdf_nullFilesReturnsBadRequest() { + void convertSvgToPdf_nullFilesReturnsBadRequest() throws java.io.IOException { SvgToPdfRequest request = new SvgToPdfRequest(); request.setFileInput(null); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); assertTrue( - new String(response.getBody(), StandardCharsets.UTF_8) + new String(drainBody(response), StandardCharsets.UTF_8) .contains("No files provided")); } @@ -55,7 +88,7 @@ class ConvertSvgToPDFTest { SvgToPdfRequest request = new SvgToPdfRequest(); request.setFileInput(new MockMultipartFile[0]); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); } @@ -69,10 +102,11 @@ class ConvertSvgToPDFTest { request.setFileInput(new MockMultipartFile[] {txtFile}); request.setCombineIntoSinglePdf(false); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - assertTrue(new String(response.getBody(), StandardCharsets.UTF_8).contains("No valid SVG")); + assertTrue( + new String(drainBody(response), StandardCharsets.UTF_8).contains("No valid SVG")); } @Test @@ -84,7 +118,7 @@ class ConvertSvgToPDFTest { request.setFileInput(new MockMultipartFile[] {emptyFile}); request.setCombineIntoSinglePdf(false); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); } @@ -107,7 +141,7 @@ class ConvertSvgToPDFTest { when(pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes)) .thenReturn(processedPdf); - ResponseEntity expectedResponse = ResponseEntity.ok(processedPdf); + ResponseEntity expectedResponse = streamingOk(processedPdf); try (MockedStatic svgMock = Mockito.mockStatic(SvgToPdf.class); MockedStatic guMock = Mockito.mockStatic(GeneralUtils.class); @@ -121,11 +155,11 @@ class ConvertSvgToPDFTest { wrMock.when( () -> - WebResponseUtils.bytesToWebResponse( - processedPdf, "drawing.pdf", MediaType.APPLICATION_PDF)) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -154,7 +188,7 @@ class ConvertSvgToPDFTest { when(pdfDocumentFactory.createNewBytesBasedOnOldDocument(combinedPdf)) .thenReturn(processedPdf); - ResponseEntity expectedResponse = ResponseEntity.ok(processedPdf); + ResponseEntity expectedResponse = streamingOk(processedPdf); try (MockedStatic svgMock = Mockito.mockStatic(SvgToPdf.class); MockedStatic guMock = Mockito.mockStatic(GeneralUtils.class); @@ -168,13 +202,11 @@ class ConvertSvgToPDFTest { wrMock.when( () -> - WebResponseUtils.bytesToWebResponse( - processedPdf, - "a_combined.pdf", - MediaType.APPLICATION_PDF)) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -189,7 +221,7 @@ class ConvertSvgToPDFTest { request.setFileInput(new MockMultipartFile[] {nullNameFile}); request.setCombineIntoSinglePdf(false); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); } @@ -206,7 +238,7 @@ class ConvertSvgToPDFTest { when(svgSanitizer.sanitize(svgContent)).thenThrow(new IOException("sanitization error")); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java index a85e6a6666..c722b83c9d 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java @@ -4,9 +4,10 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.lang.reflect.Method; @@ -34,6 +35,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.converters.UrlToPdfRequest; import stirling.software.common.configuration.RuntimePathConfig; @@ -43,13 +45,25 @@ import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; import stirling.software.common.util.ProcessExecutor.Processes; -import stirling.software.common.util.WebResponseUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; public class ConvertWebsiteToPdfTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } private static final Pattern PDF_FILENAME_PATTERN = Pattern.compile("[A-Za-z0-9_]+\\.pdf"); @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private RuntimePathConfig runtimePathConfig; + @Mock private TempFileManager tempFileManager; private ApplicationProperties applicationProperties; private ConvertWebsiteToPDF sut; @@ -58,6 +72,18 @@ public class ConvertWebsiteToPdfTest { @BeforeEach void setUp() throws Exception { mocks = MockitoAnnotations.openMocks(this); + 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; + }); // Enable feature (adjust structure for your project if necessary) applicationProperties = new ApplicationProperties(); @@ -68,7 +94,12 @@ public class ConvertWebsiteToPdfTest { when(pdfDocumentFactory.load(any(File.class))).thenReturn(new PDDocument()); // Build SUT - sut = new ConvertWebsiteToPDF(pdfDocumentFactory, runtimePathConfig, applicationProperties); + sut = + new ConvertWebsiteToPDF( + pdfDocumentFactory, + runtimePathConfig, + applicationProperties, + tempFileManager); // Provide RequestContext for ServletUriComponentsBuilder MockHttpServletRequest req = new MockHttpServletRequest(); @@ -172,38 +203,29 @@ public class ConvertWebsiteToPdfTest { request.setUrlInput("https://example.com"); try (MockedStatic pe = Mockito.mockStatic(ProcessExecutor.class); - MockedStatic wr = Mockito.mockStatic(WebResponseUtils.class); MockedStatic gu = Mockito.mockStatic(GeneralUtils.class); MockedStatic httpClient = mockHttpClientReturning("")) { - // Force URL checks to be positive gu.when(() -> GeneralUtils.isValidURL("https://example.com")).thenReturn(true); gu.when(() -> GeneralUtils.isURLReachable("https://example.com")).thenReturn(true); + gu.when(() -> GeneralUtils.convertToFileName(anyString())).thenReturn("example_com"); + gu.when(() -> GeneralUtils.generateFilename(anyString(), anyString())) + .thenAnswer(inv -> inv.getArgument(0) + inv.getArgument(1)); - // correct ProcessExecutor! ProcessExecutor mockExec = Mockito.mock(ProcessExecutor.class); pe.when(() -> ProcessExecutor.getInstance(Processes.WEASYPRINT)).thenReturn(mockExec); @SuppressWarnings("unchecked") ArgumentCaptor> cmdCaptor = ArgumentCaptor.forClass(List.class); - // Return value of correct type ProcessExecutorResult dummyResult = Mockito.mock(ProcessExecutorResult.class); when(mockExec.runCommandWithOutputHandling(cmdCaptor.capture())) .thenReturn(dummyResult); - ResponseEntity fakeResponse = ResponseEntity.ok(new byte[0]); - - wr.when( - () -> - WebResponseUtils.baosToWebResponse( - any(ByteArrayOutputStream.class), any())) - .thenReturn(fakeResponse); - - // Act ResponseEntity resp = sut.urlToPdf(request); - // Assert – Response OK + // Assert + assertNotNull(resp); assertEquals(HttpStatus.OK, resp.getStatusCode()); // Assert – WeasyPrint command correct @@ -236,17 +258,19 @@ public class ConvertWebsiteToPdfTest { try (MockedStatic gu = Mockito.mockStatic(GeneralUtils.class); MockedStatic pe = Mockito.mockStatic(ProcessExecutor.class); - MockedStatic wr = Mockito.mockStatic(WebResponseUtils.class); MockedStatic files = Mockito.mockStatic(Files.class); MockedStatic httpClient = mockHttpClientReturning("")) { - // Force URL checks to be positive gu.when(() -> GeneralUtils.isValidURL("https://example.com")).thenReturn(true); gu.when(() -> GeneralUtils.isURLReachable("https://example.com")).thenReturn(true); + gu.when(() -> GeneralUtils.convertToFileName(anyString())).thenReturn("example_com"); + gu.when(() -> GeneralUtils.generateFilename(anyString(), anyString())) + .thenAnswer(inv -> inv.getArgument(0) + inv.getArgument(1)); - // Force temp files + provoke delete error files.when(() -> Files.createTempFile("url_input_", ".html")).thenReturn(htmlTemp); files.when(() -> Files.createTempFile("output_", ".pdf")).thenReturn(preCreatedTemp); + files.when(() -> Files.createTempFile(eq("test"), anyString())) + .thenReturn(preCreatedTemp); files.when( () -> Files.writeString( @@ -257,26 +281,20 @@ public class ConvertWebsiteToPdfTest { files.when(() -> Files.deleteIfExists(htmlTemp)).thenReturn(true); files.when(() -> Files.deleteIfExists(preCreatedTemp)) .thenThrow(new IOException("fail delete")); - files.when(() -> Files.exists(preCreatedTemp)).thenReturn(true); // for the assert + files.when(() -> Files.exists(preCreatedTemp)).thenReturn(true); + files.when(() -> Files.size(any(Path.class))).thenReturn(100L); + files.when(() -> Files.copy(any(Path.class), any(java.io.OutputStream.class))) + .thenReturn(0L); + files.when(() -> Files.newOutputStream(any(Path.class))) + .thenAnswer(inv -> new java.io.ByteArrayOutputStream()); - // ProcessExecutor ProcessExecutor mockExec = Mockito.mock(ProcessExecutor.class); pe.when(() -> ProcessExecutor.getInstance(Processes.WEASYPRINT)).thenReturn(mockExec); ProcessExecutorResult dummy = Mockito.mock(ProcessExecutorResult.class); when(mockExec.runCommandWithOutputHandling(Mockito.any())).thenReturn(dummy); - // WebResponseUtils - ResponseEntity fakeResponse = ResponseEntity.ok(new byte[0]); - wr.when( - () -> - WebResponseUtils.baosToWebResponse( - any(ByteArrayOutputStream.class), any())) - .thenReturn(fakeResponse); - - // Act: should not throw and should return a Response ResponseEntity resp = assertDoesNotThrow(() -> sut.urlToPdf(request)); - // Assert assertNotNull(resp, "Response should not be null"); assertEquals(HttpStatus.OK, resp.getStatusCode()); assertTrue( diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportControllerTest.java index ad63b21039..20f881e5ac 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportControllerTest.java @@ -3,11 +3,13 @@ package stirling.software.SPDF.controller.api.converters; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; 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.reset; import static org.mockito.Mockito.when; +import java.io.File; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Path; @@ -25,11 +27,13 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.model.api.converters.PdfVectorExportRequest; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) @@ -44,6 +48,18 @@ class PdfVectorExportControllerTest { @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; + }); when(tempFileManager.createTempFile(any())) .thenAnswer( invocation -> { @@ -106,7 +122,8 @@ class PdfVectorExportControllerTest { PdfVectorExportRequest request = new PdfVectorExportRequest(); request.setFileInput(file); - ResponseEntity response = controller.convertGhostscriptInputsToPdf(request); + ResponseEntity response = + controller.convertGhostscriptInputsToPdf(request); assertThat(response.getStatusCode()).isEqualTo(org.springframework.http.HttpStatus.OK); assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PDF); @@ -123,11 +140,14 @@ class PdfVectorExportControllerTest { PdfVectorExportRequest request = new PdfVectorExportRequest(); request.setFileInput(file); - ResponseEntity response = controller.convertGhostscriptInputsToPdf(request); + ResponseEntity response = + controller.convertGhostscriptInputsToPdf(request); assertThat(response.getStatusCode()).isEqualTo(org.springframework.http.HttpStatus.OK); assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PDF); - assertThat(response.getBody()).contains(content); + java.io.ByteArrayOutputStream baosVerify = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baosVerify); + assertThat(baosVerify.toByteArray()).contains(content); } @Test diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/filters/FilterControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/filters/FilterControllerTest.java index 0b73d3f7f9..df988f52fd 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/filters/FilterControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/filters/FilterControllerTest.java @@ -17,6 +17,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.PDFComparisonAndCount; import stirling.software.SPDF.model.api.PDFWithPageNums; @@ -26,12 +27,24 @@ import stirling.software.SPDF.model.api.filter.PageRotationRequest; import stirling.software.SPDF.model.api.filter.PageSizeRequest; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.PdfUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class FilterControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private FilterController filterController; @@ -59,19 +72,22 @@ class FilterControllerTest { PDDocument mockDoc = mock(PDDocument.class); when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDoc); - ResponseEntity expectedResponse = ResponseEntity.ok(new byte[] {1, 2, 3}); + ResponseEntity expectedResponse = streamingOk(new byte[] {1, 2, 3}); try (MockedStatic pdfUtilsMock = mockStatic(PdfUtils.class); MockedStatic webMock = mockStatic(WebResponseUtils.class)) { pdfUtilsMock.when(() -> PdfUtils.hasText(mockDoc, "all", "hello")).thenReturn(true); - webMock.when(() -> WebResponseUtils.pdfDocToWebResponse(mockDoc, "test.pdf")) + webMock.when( + () -> + WebResponseUtils.pdfDocToWebResponse( + mockDoc, "test.pdf", tempFileManager)) .thenReturn(expectedResponse); - ResponseEntity result = filterController.containsText(request); + ResponseEntity result = filterController.containsText(request); assertEquals(HttpStatus.OK, result.getStatusCode()); - assertArrayEquals(new byte[] {1, 2, 3}, result.getBody()); + assertArrayEquals(new byte[] {1, 2, 3}, drainBody(result)); } } @@ -88,7 +104,7 @@ class FilterControllerTest { try (MockedStatic pdfUtilsMock = mockStatic(PdfUtils.class)) { pdfUtilsMock.when(() -> PdfUtils.hasText(mockDoc, "all", "missing")).thenReturn(false); - ResponseEntity result = filterController.containsText(request); + ResponseEntity result = filterController.containsText(request); assertEquals(HttpStatus.NO_CONTENT, result.getStatusCode()); assertNull(result.getBody()); @@ -106,19 +122,22 @@ class FilterControllerTest { PDDocument mockDoc = mock(PDDocument.class); when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDoc); - ResponseEntity expectedResponse = ResponseEntity.ok(new byte[] {4, 5, 6}); + ResponseEntity expectedResponse = streamingOk(new byte[] {4, 5, 6}); try (MockedStatic pdfUtilsMock = mockStatic(PdfUtils.class); MockedStatic webMock = mockStatic(WebResponseUtils.class)) { pdfUtilsMock.when(() -> PdfUtils.hasImages(mockDoc, "all")).thenReturn(true); - webMock.when(() -> WebResponseUtils.pdfDocToWebResponse(mockDoc, "test.pdf")) + webMock.when( + () -> + WebResponseUtils.pdfDocToWebResponse( + mockDoc, "test.pdf", tempFileManager)) .thenReturn(expectedResponse); - ResponseEntity result = filterController.containsImage(request); + ResponseEntity result = filterController.containsImage(request); assertEquals(HttpStatus.OK, result.getStatusCode()); - assertArrayEquals(new byte[] {4, 5, 6}, result.getBody()); + assertArrayEquals(new byte[] {4, 5, 6}, drainBody(result)); } } @@ -134,7 +153,7 @@ class FilterControllerTest { try (MockedStatic pdfUtilsMock = mockStatic(PdfUtils.class)) { pdfUtilsMock.when(() -> PdfUtils.hasImages(mockDoc, "1")).thenReturn(false); - ResponseEntity result = filterController.containsImage(request); + ResponseEntity result = filterController.containsImage(request); assertEquals(HttpStatus.NO_CONTENT, result.getStatusCode()); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/form/FormFillControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/form/FormFillControllerTest.java index 539fa250e7..0db7f60514 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/form/FormFillControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/form/FormFillControllerTest.java @@ -2,10 +2,13 @@ package stirling.software.SPDF.controller.api.form; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; @@ -22,8 +25,11 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; @@ -31,8 +37,19 @@ import tools.jackson.databind.json.JsonMapper; @ExtendWith(MockitoExtension.class) @DisplayName("FormFillController Tests") class FormFillControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; private ObjectMapper realObjectMapper; @@ -40,6 +57,18 @@ class FormFillControllerTest { @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; + }); realObjectMapper = JsonMapper.builder().build(); // Inject real ObjectMapper via reflection since @InjectMocks uses the mock var field = FormFillController.class.getDeclaredField("objectMapper"); @@ -203,7 +232,8 @@ class FormFillControllerTest { when(pdfDocumentFactory.load(eq(file))).thenReturn(doc); byte[] payload = "{\"field1\":\"value1\"}".getBytes(); - ResponseEntity response = controller.fillForm(file, payload, false); + ResponseEntity response = + controller.fillForm(file, payload, false); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isNotNull(); @@ -216,7 +246,7 @@ class FormFillControllerTest { PDDocument doc = createMinimalPdf(); when(pdfDocumentFactory.load(eq(file))).thenReturn(doc); - ResponseEntity response = controller.fillForm(file, null, false); + ResponseEntity response = controller.fillForm(file, null, false); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -257,7 +287,7 @@ class FormFillControllerTest { when(pdfDocumentFactory.load(eq(file))).thenReturn(doc); byte[] payload = "[\"field1\"]".getBytes(); - ResponseEntity response = controller.deleteFields(file, payload); + ResponseEntity response = controller.deleteFields(file, payload); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -293,7 +323,8 @@ class FormFillControllerTest { String json = "[{\"targetName\":\"f1\",\"name\":null,\"label\":null,\"type\":null," + "\"required\":null,\"multiSelect\":null,\"options\":null,\"defaultValue\":\"newVal\",\"tooltip\":null}]"; - ResponseEntity response = controller.modifyFields(file, json.getBytes()); + ResponseEntity response = + controller.modifyFields(file, json.getBytes()); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AttachmentControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AttachmentControllerTest.java index fe0e2ca2d6..b3e4690cc7 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AttachmentControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AttachmentControllerTest.java @@ -1,9 +1,12 @@ package stirling.software.SPDF.controller.api.misc; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; @@ -19,18 +22,32 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.misc.AddAttachmentRequest; import stirling.software.SPDF.service.AttachmentServiceInterface; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class AttachmentControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private AttachmentServiceInterface pdfAttachmentService; + @Mock private TempFileManager tempFileManager; @InjectMocks private AttachmentController attachmentController; @@ -42,7 +59,19 @@ class AttachmentControllerTest { private PDDocument modifiedMockDocument; @BeforeEach - void setUp() { + 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; + }); pdfFile = new MockMultipartFile( "fileInput", @@ -71,8 +100,8 @@ class AttachmentControllerTest { List attachments = List.of(attachment1, attachment2); request.setAttachments(attachments); request.setFileInput(pdfFile); - ResponseEntity expectedResponse = - ResponseEntity.ok("modified PDF content".getBytes()); + ResponseEntity expectedResponse = + streamingOk("modified PDF content".getBytes()); when(pdfDocumentFactory.load(request, false)).thenReturn(mockDocument); when(pdfAttachmentService.addAttachment(mockDocument, attachments)) @@ -84,10 +113,13 @@ class AttachmentControllerTest { .when( () -> WebResponseUtils.pdfDocToWebResponse( - eq(mockDocument), eq("test_with_attachments.pdf"))) + any(PDDocument.class), + anyString(), + any(TempFileManager.class))) .thenReturn(expectedResponse); - ResponseEntity response = attachmentController.addAttachments(request); + ResponseEntity response = + attachmentController.addAttachments(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -102,8 +134,8 @@ class AttachmentControllerTest { List attachments = List.of(attachment1); request.setAttachments(attachments); request.setFileInput(pdfFile); - ResponseEntity expectedResponse = - ResponseEntity.ok("modified PDF content".getBytes()); + ResponseEntity expectedResponse = + streamingOk("modified PDF content".getBytes()); when(pdfDocumentFactory.load(request, false)).thenReturn(mockDocument); when(pdfAttachmentService.addAttachment(mockDocument, attachments)) @@ -115,10 +147,13 @@ class AttachmentControllerTest { .when( () -> WebResponseUtils.pdfDocToWebResponse( - eq(mockDocument), eq("test_with_attachments.pdf"))) + any(PDDocument.class), + anyString(), + any(TempFileManager.class))) .thenReturn(expectedResponse); - ResponseEntity response = attachmentController.addAttachments(request); + ResponseEntity response = + attachmentController.addAttachments(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AutoRenameControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AutoRenameControllerTest.java index ce82c8c7ec..3cdf0a3a84 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AutoRenameControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AutoRenameControllerTest.java @@ -1,8 +1,10 @@ package stirling.software.SPDF.controller.api.misc; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -14,6 +16,7 @@ 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.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -24,17 +27,47 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class AutoRenameControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @TempDir Path tempDir; @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private AutoRenameController 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; + }); + } + private MockMultipartFile createPdfWithText(String text, float fontSize) throws IOException { Path path = tempDir.resolve("test.pdf"); try (PDDocument doc = new PDDocument()) { @@ -68,10 +101,10 @@ class AutoRenameControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.extractHeader(request); + ResponseEntity response = controller.extractHeader(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); + assertThat(drainBody(response)).isNotEmpty(); String contentDisposition = response.getHeaders().getFirst("Content-Disposition"); assertThat(contentDisposition).contains(".pdf"); } @@ -94,7 +127,7 @@ class AutoRenameControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.extractHeader(request); + ResponseEntity response = controller.extractHeader(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -107,7 +140,7 @@ class AutoRenameControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.extractHeader(request); + ResponseEntity response = controller.extractHeader(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -155,7 +188,7 @@ class AutoRenameControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.extractHeader(request); + ResponseEntity response = controller.extractHeader(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); // The largest font text should be used as title (URL-encoded in Content-Disposition) @@ -173,7 +206,7 @@ class AutoRenameControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.extractHeader(request); + ResponseEntity response = controller.extractHeader(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); // Should fallback to original filename since header is too long @@ -189,7 +222,7 @@ class AutoRenameControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.extractHeader(request); + ResponseEntity response = controller.extractHeader(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); // Special characters should be sanitized @@ -215,7 +248,7 @@ class AutoRenameControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.extractHeader(request); + ResponseEntity response = controller.extractHeader(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/DecompressPdfControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/DecompressPdfControllerTest.java index 1be522272d..dd7541e635 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/DecompressPdfControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/DecompressPdfControllerTest.java @@ -1,8 +1,10 @@ package stirling.software.SPDF.controller.api.misc; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -14,6 +16,7 @@ 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.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -24,17 +27,47 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class DecompressPdfControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @TempDir Path tempDir; @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private DecompressPdfController 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; + }); + } + private MockMultipartFile createRealPdf(String content) throws IOException { Path path = tempDir.resolve("test.pdf"); try (PDDocument doc = new PDDocument()) { @@ -64,12 +97,12 @@ class DecompressPdfControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.decompressPdf(request); + ResponseEntity response = controller.decompressPdf(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); + assertThat(drainBody(response)).isNotEmpty(); // Verify the result is a valid PDF - try (PDDocument result = Loader.loadPDF(response.getBody())) { + try (PDDocument result = Loader.loadPDF(drainBody(response))) { assertThat(result.getNumberOfPages()).isEqualTo(1); } } @@ -83,10 +116,10 @@ class DecompressPdfControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.decompressPdf(request); + ResponseEntity response = controller.decompressPdf(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); + assertThat(drainBody(response)).isNotEmpty(); } @Test @@ -114,7 +147,7 @@ class DecompressPdfControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.decompressPdf(request); + ResponseEntity response = controller.decompressPdf(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); String contentDisposition = response.getHeaders().getFirst("Content-Disposition"); @@ -150,10 +183,10 @@ class DecompressPdfControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.decompressPdf(request); + ResponseEntity response = controller.decompressPdf(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - try (PDDocument result = Loader.loadPDF(response.getBody())) { + try (PDDocument result = Loader.loadPDF(drainBody(response))) { assertThat(result.getNumberOfPages()).isEqualTo(3); } } @@ -167,11 +200,11 @@ class DecompressPdfControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.decompressPdf(request); + ResponseEntity response = controller.decompressPdf(request); assertThat(response.getBody()).isNotNull(); // Decompressed PDF should generally be larger or equal to compressed - assertThat(response.getBody().length).isGreaterThan(0); + assertThat(drainBody(response).length).isGreaterThan(0); } @Test @@ -183,7 +216,7 @@ class DecompressPdfControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.decompressPdf(request); + ResponseEntity response = controller.decompressPdf(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/FlattenControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/FlattenControllerTest.java index aaee18ee8b..d55314c21b 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/FlattenControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/FlattenControllerTest.java @@ -1,8 +1,10 @@ package stirling.software.SPDF.controller.api.misc; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -16,6 +18,7 @@ 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.pdmodel.interactive.form.PDAcroForm; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -26,17 +29,47 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.misc.FlattenRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class FlattenControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @TempDir Path tempDir; @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private FlattenController 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; + }); + } + private MockMultipartFile createPdf() throws IOException { Path path = tempDir.resolve("test.pdf"); try (PDDocument doc = new PDDocument()) { @@ -65,10 +98,10 @@ class FlattenControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.flatten(request); + ResponseEntity response = controller.flatten(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); + assertThat(drainBody(response)).isNotEmpty(); } @Test @@ -85,7 +118,7 @@ class FlattenControllerTest { when(doc.getDocumentCatalog()).thenReturn(catalog); when(catalog.getAcroForm()).thenReturn(null); - ResponseEntity response = controller.flatten(request); + ResponseEntity response = controller.flatten(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); verify(doc).close(); @@ -105,7 +138,7 @@ class FlattenControllerTest { when(doc.getDocumentCatalog()).thenReturn(catalog); when(catalog.getAcroForm()).thenReturn(form); - ResponseEntity response = controller.flatten(request); + ResponseEntity response = controller.flatten(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); verify(form).flatten(); @@ -137,10 +170,10 @@ class FlattenControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(doc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(doc)).thenReturn(newDoc); - ResponseEntity response = controller.flatten(request); + ResponseEntity response = controller.flatten(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); + assertThat(drainBody(response)).isNotEmpty(); } @Test @@ -155,7 +188,7 @@ class FlattenControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(doc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(doc)).thenReturn(newDoc); - ResponseEntity response = controller.flatten(request); + ResponseEntity response = controller.flatten(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -173,7 +206,7 @@ class FlattenControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(doc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(doc)).thenReturn(newDoc); - ResponseEntity response = controller.flatten(request); + ResponseEntity response = controller.flatten(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -191,7 +224,7 @@ class FlattenControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(doc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(doc)).thenReturn(newDoc); - ResponseEntity response = controller.flatten(request); + ResponseEntity response = controller.flatten(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/OverlayImageControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/OverlayImageControllerTest.java index 45bf0dd8c7..29324bbca1 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/OverlayImageControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/OverlayImageControllerTest.java @@ -2,11 +2,14 @@ package stirling.software.SPDF.controller.api.misc; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import javax.imageio.ImageIO; @@ -24,15 +27,29 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.misc.OverlayImageRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class OverlayImageControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private OverlayImageController controller; @@ -41,6 +58,18 @@ class OverlayImageControllerTest { @BeforeEach void setUp() throws IOException { + 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; + }); pdfFile = new MockMultipartFile( "fileInput", @@ -79,12 +108,16 @@ class OverlayImageControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("result".getBytes()); + ResponseEntity expectedResponse = + streamingOk("result".getBytes()); mockedWebResponse - .when(() -> WebResponseUtils.bytesToWebResponse(any(byte[].class), anyString())) + .when( + () -> + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.overlayImage(request); + ResponseEntity response = controller.overlayImage(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -103,7 +136,7 @@ class OverlayImageControllerTest { when(pdfDocumentFactory.load(any(byte[].class))).thenThrow(new IOException("bad PDF")); - ResponseEntity response = controller.overlayImage(request); + ResponseEntity response = controller.overlayImage(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); } @@ -124,12 +157,16 @@ class OverlayImageControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("result".getBytes()); + ResponseEntity expectedResponse = + streamingOk("result".getBytes()); mockedWebResponse - .when(() -> WebResponseUtils.bytesToWebResponse(any(byte[].class), anyString())) + .when( + () -> + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.overlayImage(request); + ResponseEntity response = controller.overlayImage(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -152,12 +189,16 @@ class OverlayImageControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("result".getBytes()); + ResponseEntity expectedResponse = + streamingOk("result".getBytes()); mockedWebResponse - .when(() -> WebResponseUtils.bytesToWebResponse(any(byte[].class), anyString())) + .when( + () -> + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.overlayImage(request); + ResponseEntity response = controller.overlayImage(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -180,13 +221,17 @@ class OverlayImageControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("result".getBytes()); + ResponseEntity expectedResponse = + streamingOk("result".getBytes()); mockedWebResponse - .when(() -> WebResponseUtils.bytesToWebResponse(any(byte[].class), anyString())) + .when( + () -> + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); // Should not throw - coordinates are passed to contentStream.drawImage - ResponseEntity response = controller.overlayImage(request); + ResponseEntity response = controller.overlayImage(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorControllerTest.java index eb126af067..a526f91726 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorControllerTest.java @@ -2,10 +2,13 @@ package stirling.software.SPDF.controller.api.misc; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -19,17 +22,31 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.misc.ReplaceAndInvertColorRequest; import stirling.software.SPDF.service.misc.ReplaceAndInvertColorService; import stirling.software.common.model.api.misc.HighContrastColorCombination; import stirling.software.common.model.api.misc.ReplaceAndInvert; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ReplaceAndInvertColorControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private ReplaceAndInvertColorService replaceAndInvertColorService; + @Mock private TempFileManager tempFileManager; @InjectMocks private ReplaceAndInvertColorController controller; @@ -37,7 +54,19 @@ class ReplaceAndInvertColorControllerTest { private ReplaceAndInvertColorRequest request; @BeforeEach - void setUp() { + 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; + }); pdfFile = new MockMultipartFile( "fileInput", @@ -67,17 +96,16 @@ class ReplaceAndInvertColorControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok(resultBytes); + ResponseEntity expectedResponse = streamingOk(resultBytes); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), - anyString(), - eq(MediaType.APPLICATION_PDF))) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.replaceAndInvertColor(request); + ResponseEntity response = + controller.replaceAndInvertColor(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -104,17 +132,16 @@ class ReplaceAndInvertColorControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok(resultBytes); + ResponseEntity expectedResponse = streamingOk(resultBytes); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), - anyString(), - eq(MediaType.APPLICATION_PDF))) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.replaceAndInvertColor(request); + ResponseEntity response = + controller.replaceAndInvertColor(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -139,17 +166,16 @@ class ReplaceAndInvertColorControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok(resultBytes); + ResponseEntity expectedResponse = streamingOk(resultBytes); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), - anyString(), - eq(MediaType.APPLICATION_PDF))) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.replaceAndInvertColor(request); + ResponseEntity response = + controller.replaceAndInvertColor(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -179,24 +205,18 @@ class ReplaceAndInvertColorControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok(resultBytes); + ResponseEntity expectedResponse = streamingOk(resultBytes); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), - contains("_inverted.pdf"), - eq(MediaType.APPLICATION_PDF))) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); controller.replaceAndInvertColor(request); mockedWebResponse.verify( - () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), - contains("_inverted.pdf"), - eq(MediaType.APPLICATION_PDF))); + () -> WebResponseUtils.pdfFileToWebResponse(any(TempFile.class), anyString())); } } } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ShowJavascriptTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ShowJavascriptTest.java index bc7e66f30c..abeffb7f4a 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ShowJavascriptTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ShowJavascriptTest.java @@ -1,9 +1,11 @@ package stirling.software.SPDF.controller.api.misc; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; -import java.nio.charset.StandardCharsets; +import java.io.File; +import java.nio.file.Files; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; @@ -21,15 +23,29 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ShowJavascriptTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private ShowJavascript showJavascript; @@ -37,7 +53,19 @@ class ShowJavascriptTest { private PDFFile request; @BeforeEach - void setUp() { + 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; + }); pdfFile = new MockMultipartFile( "fileInput", @@ -58,31 +86,25 @@ class ShowJavascriptTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("no js".getBytes()); + ResponseEntity expectedResponse = + streamingOk("no js".getBytes()); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), + WebResponseUtils.fileToWebResponse( + any(TempFile.class), eq("test.pdf.js"), eq(MediaType.TEXT_PLAIN))) .thenReturn(expectedResponse); - ResponseEntity response = showJavascript.extractHeader(request); + ResponseEntity response = showJavascript.extractHeader(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); - // Verify the bytes passed contain the "does not contain" message mockedWebResponse.verify( () -> - WebResponseUtils.bytesToWebResponse( - argThat( - bytes -> { - String content = - new String(bytes, StandardCharsets.UTF_8); - return content.contains( - "does not contain Javascript"); - }), + WebResponseUtils.fileToWebResponse( + any(TempFile.class), eq("test.pdf.js"), eq(MediaType.TEXT_PLAIN))); } @@ -107,30 +129,24 @@ class ShowJavascriptTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("js content".getBytes()); + ResponseEntity expectedResponse = + streamingOk("js content".getBytes()); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), + WebResponseUtils.fileToWebResponse( + any(TempFile.class), eq("test.pdf.js"), eq(MediaType.TEXT_PLAIN))) .thenReturn(expectedResponse); - ResponseEntity response = showJavascript.extractHeader(request); + ResponseEntity response = showJavascript.extractHeader(request); assertNotNull(response); - // Verify the bytes passed contain the script content mockedWebResponse.verify( () -> - WebResponseUtils.bytesToWebResponse( - argThat( - bytes -> { - String content = - new String(bytes, StandardCharsets.UTF_8); - return content.contains("alert('hello');") - && content.contains("Script1"); - }), + WebResponseUtils.fileToWebResponse( + any(TempFile.class), eq("test.pdf.js"), eq(MediaType.TEXT_PLAIN))); } @@ -144,31 +160,24 @@ class ShowJavascriptTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("no js".getBytes()); + ResponseEntity expectedResponse = + streamingOk("no js".getBytes()); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), + WebResponseUtils.fileToWebResponse( + any(TempFile.class), anyString(), eq(MediaType.TEXT_PLAIN))) .thenReturn(expectedResponse); - ResponseEntity response = showJavascript.extractHeader(request); + ResponseEntity response = showJavascript.extractHeader(request); assertNotNull(response); mockedWebResponse.verify( () -> - WebResponseUtils.bytesToWebResponse( - argThat( - bytes -> { - String content = - new String(bytes, StandardCharsets.UTF_8); - return content.contains( - "does not contain Javascript"); - }), - anyString(), - eq(MediaType.TEXT_PLAIN))); + WebResponseUtils.fileToWebResponse( + any(TempFile.class), anyString(), eq(MediaType.TEXT_PLAIN))); } } @@ -191,31 +200,24 @@ class ShowJavascriptTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("no js".getBytes()); + ResponseEntity expectedResponse = + streamingOk("no js".getBytes()); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), + WebResponseUtils.fileToWebResponse( + any(TempFile.class), anyString(), eq(MediaType.TEXT_PLAIN))) .thenReturn(expectedResponse); - ResponseEntity response = showJavascript.extractHeader(request); + ResponseEntity response = showJavascript.extractHeader(request); assertNotNull(response); mockedWebResponse.verify( () -> - WebResponseUtils.bytesToWebResponse( - argThat( - bytes -> { - String content = - new String(bytes, StandardCharsets.UTF_8); - return content.contains( - "does not contain Javascript"); - }), - anyString(), - eq(MediaType.TEXT_PLAIN))); + WebResponseUtils.fileToWebResponse( + any(TempFile.class), anyString(), eq(MediaType.TEXT_PLAIN))); } } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/StampControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/StampControllerTest.java index 158abf486e..2a8b548ffd 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/StampControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/StampControllerTest.java @@ -9,6 +9,7 @@ import java.util.regex.Pattern; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentInformation; +import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -46,6 +47,7 @@ class StampControllerTest { private Method processStampTextMethod; private Method processCustomDateFormatMethod; + private Method calculateImagePositionYMethod; @BeforeEach void setUp() throws NoSuchMethodException { @@ -63,6 +65,26 @@ class StampControllerTest { StampController.class.getDeclaredMethod( "processCustomDateFormat", String.class, LocalDateTime.class); processCustomDateFormatMethod.setAccessible(true); + + calculateImagePositionYMethod = + StampController.class.getDeclaredMethod( + "calculateImagePositionY", + PDRectangle.class, + int.class, + float.class, + float.class); + calculateImagePositionYMethod.setAccessible(true); + } + + private float invokeCalculateImagePositionY( + PDRectangle pageSize, int position, float imageHeight, float margin) throws Exception { + try { + return (float) + calculateImagePositionYMethod.invoke( + stampController, pageSize, position, imageHeight, margin); + } catch (InvocationTargetException e) { + throw (Exception) e.getCause(); + } } private String invokeProcessStampText( @@ -86,6 +108,45 @@ class StampControllerTest { } } + @Nested + @DisplayName("Image stamp position (lower-left anchor)") + class ImagePositionYTests { + + @Test + @DisplayName("Top row: upper edge of image sits below top margin") + void topRowUsesUpperRightMinusMarginMinusHeight() throws Exception { + PDRectangle page = new PDRectangle(0, 0, 600, 800); + float y = invokeCalculateImagePositionY(page, 3, 100f, 10f); + assertEquals(690f, y, 0.001f); + } + + @Test + @DisplayName("Middle row: image is vertically centred on page") + void middleRowCentresImage() throws Exception { + PDRectangle page = new PDRectangle(0, 0, 600, 800); + float y = invokeCalculateImagePositionY(page, 5, 100f, 10f); + assertEquals(350f, y, 0.001f); + } + + @Test + @DisplayName("Bottom row: lower edge of image sits above bottom margin") + void bottomRowUsesLowerLeftPlusMargin() throws Exception { + PDRectangle page = new PDRectangle(0, 0, 600, 800); + float y = invokeCalculateImagePositionY(page, 7, 100f, 10f); + assertEquals(10f, y, 0.001f); + } + + @Test + @DisplayName("Honours non-zero media box origin") + void respectsLowerLeftOrigin() throws Exception { + PDRectangle page = new PDRectangle(50f, 100f, 400f, 300f); + float yMid = invokeCalculateImagePositionY(page, 5, 20f, 5f); + assertEquals(240f, yMid, 0.001f); + float yTop = invokeCalculateImagePositionY(page, 3, 20f, 5f); + assertEquals(375f, yTop, 0.001f); + } + } + @Nested @DisplayName("Basic Variable Substitution Tests") class BasicVariableTests { diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsControllerTest.java index b660519355..fb34b5318c 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsControllerTest.java @@ -2,8 +2,12 @@ package stirling.software.SPDF.controller.api.misc; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; +import java.nio.file.Files; + import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; @@ -14,22 +18,38 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class UnlockPDFFormsControllerTest { @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; private UnlockPDFFormsController controller; private MockMultipartFile mockPdfFile; @BeforeEach - void setUp() { - controller = new UnlockPDFFormsController(pdfDocumentFactory); + 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; + }); + controller = new UnlockPDFFormsController(pdfDocumentFactory, tempFileManager); mockPdfFile = new MockMultipartFile( "fileInput", @@ -47,7 +67,7 @@ class UnlockPDFFormsControllerTest { PDFFile file = new PDFFile(); file.setFileInput(mockPdfFile); - ResponseEntity response = controller.unlockPDFForms(file); + ResponseEntity response = controller.unlockPDFForms(file); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -65,7 +85,7 @@ class UnlockPDFFormsControllerTest { PDFFile file = new PDFFile(); file.setFileInput(mockPdfFile); - ResponseEntity response = controller.unlockPDFForms(file); + ResponseEntity response = controller.unlockPDFForms(file); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -79,7 +99,7 @@ class UnlockPDFFormsControllerTest { PDFFile file = new PDFFile(); file.setFileInput(mockPdfFile); - ResponseEntity response = controller.unlockPDFForms(file); + ResponseEntity response = controller.unlockPDFForms(file); // Controller catches exceptions and returns null assertNull(response); @@ -94,7 +114,7 @@ class UnlockPDFFormsControllerTest { PDFFile file = new PDFFile(); file.setFileInput(mockPdfFile); - ResponseEntity response = controller.unlockPDFForms(file); + ResponseEntity response = controller.unlockPDFForms(file); assertNotNull(response); String contentDisposition = response.getHeaders().getFirst("Content-Disposition"); @@ -113,7 +133,7 @@ class UnlockPDFFormsControllerTest { PDFFile file = new PDFFile(); file.setFileInput(mockPdfFile); - ResponseEntity response = controller.unlockPDFForms(file); + ResponseEntity response = controller.unlockPDFForms(file); assertNotNull(response); assertTrue(acroForm.getNeedAppearances()); 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 d58770f45d..e87d08a863 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 @@ -4,8 +4,6 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -15,24 +13,18 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockedConstruction; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import jakarta.servlet.ServletContext; import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineResult; import stirling.software.SPDF.service.ApiDocService; -import stirling.software.common.service.UserServiceInterface; +import stirling.software.common.service.InternalApiClient; import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) @@ -40,20 +32,16 @@ class PipelineProcessorTest { @Mock ApiDocService apiDocService; - @Mock UserServiceInterface userService; - - @Mock ServletContext servletContext; + @Mock InternalApiClient internalApiClient; @Mock TempFileManager tempFileManager; PipelineProcessor pipelineProcessor; @BeforeEach - void setUp() { + void setUp() throws Exception { pipelineProcessor = - spy( - new PipelineProcessor( - apiDocService, userService, servletContext, tempFileManager)); + new PipelineProcessor(apiDocService, internalApiClient, tempFileManager); } @Test @@ -65,7 +53,6 @@ class PipelineProcessorTest { config.setOperations(List.of(op)); Resource file = new MyFileByteArrayResource(); - List files = List.of(file); when(apiDocService.isMultiInput("/api/v1/filter/filter-page-count")).thenReturn(false); @@ -74,13 +61,11 @@ class PipelineProcessorTest { when(apiDocService.isValidOperation(eq("/api/v1/filter/filter-page-count"), anyMap())) .thenReturn(true); - // Use a FileSystemResource backed by a temp file to avoid FileNotFoundException Path emptyTemp = Files.createTempFile("empty", ".tmp"); Resource emptyResource = new FileSystemResource(emptyTemp.toFile()); - doReturn(new ResponseEntity<>(emptyResource, HttpStatus.OK)) - .when(pipelineProcessor) - .sendWebRequest(anyString(), any()); + when(internalApiClient.post(anyString(), any())) + .thenReturn(new ResponseEntity<>(emptyResource, HttpStatus.OK)); PipelineResult result = pipelineProcessor.runPipelineAgainstFiles(files, config); @@ -118,104 +103,18 @@ class PipelineProcessorTest { when(apiDocService.getExtensionTypes(anyBoolean(), anyString())).thenReturn(List.of("pdf")); when(apiDocService.isValidOperation(anyString(), anyMap())).thenReturn(true); - doReturn(new ResponseEntity<>(outputResource, HttpStatus.OK)) - .when(pipelineProcessor) - .sendWebRequest(anyString(), any()); + when(internalApiClient.post(anyString(), any())) + .thenReturn(new ResponseEntity<>(outputResource, HttpStatus.OK)); PipelineResult result = pipelineProcessor.runPipelineAgainstFiles(files, config); - verify(pipelineProcessor).sendWebRequest(anyString(), any()); + verify(internalApiClient).post(anyString(), any()); assertFalse(result.isHasErrors()); - // Clean up Files.deleteIfExists(tempPath); } - @Test - void sendWebRequestDoesNotForceContentType() throws Exception { - MultiValueMap body = new LinkedMultiValueMap<>(); - body.add( - "fileInput", - new ByteArrayResource("data".getBytes(StandardCharsets.UTF_8)) { - @Override - public String getFilename() { - return "input.pdf"; - } - }); - - Path tempPath = Files.createTempFile("pipeline-test", ".tmp"); - var tempFile = mock(stirling.software.common.util.TempFile.class); - when(tempFile.getPath()).thenReturn(tempPath); - when(tempFile.getFile()).thenReturn(tempPath.toFile()); - when(tempFileManager.createManagedTempFile("pipeline")).thenReturn(tempFile); - - var capturedHeaders = new org.springframework.http.HttpHeaders[1]; - - try (MockedConstruction ignored = - mockConstruction( - org.springframework.web.client.RestTemplate.class, - (mock, context) -> { - when(mock.httpEntityCallback(any(), eq(Resource.class))) - .thenAnswer( - invocation -> { - var entity = invocation.getArgument(0); - capturedHeaders[0] = - ((org.springframework.http.HttpEntity) - entity) - .getHeaders(); - return (org.springframework.web.client - .RequestCallback) - request -> {}; - }); - - when(mock.execute( - anyString(), - eq(org.springframework.http.HttpMethod.POST), - any(), - any())) - .thenAnswer( - invocation -> { - @SuppressWarnings("unchecked") - var extractor = - (org.springframework.web.client - .ResponseExtractor< - ResponseEntity>) - invocation.getArgument(3); - ClientHttpResponse response = - mock(ClientHttpResponse.class); - when(response.getBody()) - .thenReturn( - new ByteArrayInputStream( - "ok" - .getBytes( - StandardCharsets - .UTF_8))); - var headers = - new org.springframework.http.HttpHeaders(); - headers.add( - org.springframework.http.HttpHeaders - .CONTENT_DISPOSITION, - "attachment; filename=\"out.pdf\""); - when(response.getHeaders()).thenReturn(headers); - lenient() - .when(response.getStatusCode()) - .thenReturn(HttpStatus.OK); - return extractor.extractData(response); - }); - })) { - ResponseEntity response = - pipelineProcessor.sendWebRequest("http://localhost/api", body); - - assertNotNull(response); - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertNull(capturedHeaders[0].getContentType()); - } finally { - Files.deleteIfExists(tempPath); - } - } - private static class MyFileByteArrayResource extends ByteArrayResource { public MyFileByteArrayResource() { super("data".getBytes()); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/CertSignControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/CertSignControllerTest.java index 5a5eff1f12..e5d1a5255c 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/CertSignControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/CertSignControllerTest.java @@ -4,10 +4,14 @@ 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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.InputStream; +import java.nio.file.Files; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; @@ -23,14 +27,28 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class CertSignControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private CertSignController certSignController; @@ -47,6 +65,18 @@ class CertSignControllerTest { @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; + }); try (PDDocument doc = new PDDocument()) { doc.addPage(new PDPage()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -137,10 +167,11 @@ class CertSignControllerTest { request.setPageNumber(1); request.setShowLogo(false); - ResponseEntity response = certSignController.signPDFWithCert(request); + ResponseEntity response = + certSignController.signPDFWithCert(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -163,10 +194,11 @@ class CertSignControllerTest { request.setPageNumber(1); request.setShowLogo(false); - ResponseEntity response = certSignController.signPDFWithCert(request); + ResponseEntity response = + certSignController.signPDFWithCert(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -215,10 +247,11 @@ class CertSignControllerTest { request.setPageNumber(1); request.setShowLogo(false); - ResponseEntity response = certSignController.signPDFWithCert(request); + ResponseEntity response = + certSignController.signPDFWithCert(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -246,10 +279,11 @@ class CertSignControllerTest { request.setPageNumber(1); request.setShowLogo(false); - ResponseEntity response = certSignController.signPDFWithCert(request); + ResponseEntity response = + certSignController.signPDFWithCert(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -277,10 +311,11 @@ class CertSignControllerTest { request.setPageNumber(1); request.setShowLogo(false); - ResponseEntity response = certSignController.signPDFWithCert(request); + ResponseEntity response = + certSignController.signPDFWithCert(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -308,10 +343,11 @@ class CertSignControllerTest { request.setPageNumber(1); request.setShowLogo(false); - ResponseEntity response = certSignController.signPDFWithCert(request); + ResponseEntity response = + certSignController.signPDFWithCert(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -339,9 +375,10 @@ class CertSignControllerTest { request.setPageNumber(1); request.setShowLogo(false); - ResponseEntity response = certSignController.signPDFWithCert(request); + ResponseEntity response = + certSignController.signPDFWithCert(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/PasswordControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/PasswordControllerTest.java index 71ce1b7f4f..d0d1b45fad 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/PasswordControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/PasswordControllerTest.java @@ -3,10 +3,14 @@ package stirling.software.SPDF.controller.api.security; import static org.junit.jupiter.api.Assertions.*; 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.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; @@ -28,17 +32,31 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.security.AddPasswordRequest; import stirling.software.SPDF.model.api.security.PDFPasswordRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @DisplayName("PasswordController Tests") @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class PasswordControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private PasswordController passwordController; @@ -46,6 +64,18 @@ class PasswordControllerTest { @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; + }); try (PDDocument doc = new PDDocument()) { doc.addPage(new PDPage()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -90,10 +120,11 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyString())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.removePassword(request); + ResponseEntity response = + passwordController.removePassword(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -114,7 +145,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyString())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.removePassword(request); + ResponseEntity response = + passwordController.removePassword(request); assertNotNull(response); assertNotNull(response.getBody()); @@ -177,7 +209,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyString())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.removePassword(request); + ResponseEntity response = + passwordController.removePassword(request); assertNotNull(response.getBody()); } @@ -195,7 +228,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyString())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.removePassword(request); + ResponseEntity response = + passwordController.removePassword(request); assertNotNull(response.getBody()); } } @@ -223,10 +257,11 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -248,7 +283,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); } @@ -274,7 +310,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); } @@ -297,7 +334,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); } @@ -328,9 +366,10 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -353,7 +392,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); } @@ -376,7 +416,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); } @@ -399,7 +440,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); } } 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 75f1b8d01d..caa03b3cf3 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 @@ -1,13 +1,16 @@ package stirling.software.SPDF.controller.api.security; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import java.awt.Color; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -44,20 +47,34 @@ import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.security.ManualRedactPdfRequest; import stirling.software.SPDF.model.api.security.RedactPdfRequest; import stirling.software.common.model.api.security.RedactionArea; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @DisplayName("PDF Redaction Controller tests") @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class RedactControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } private static final Logger log = LoggerFactory.getLogger(RedactControllerTest.class); @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private RedactController redactController; @@ -112,6 +129,18 @@ class RedactControllerTest { @BeforeEach void setUp() throws IOException { + 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; + }); mockPdfFile = new MockMultipartFile( "fileInput", @@ -159,14 +188,15 @@ class RedactControllerTest { when(mockCOSStream.createOutputStream()).thenReturn(mockOutputStream); when(mockCOSStream.createOutputStream(any())).thenReturn(mockOutputStream); - doAnswer( - invocation -> { - ByteArrayOutputStream baos = invocation.getArgument(0); - baos.write("Mock PDF Content".getBytes()); + lenient() + .doAnswer( + inv -> { + File f = inv.getArgument(0); + java.nio.file.Files.write(f.toPath(), "mock pdf".getBytes()); return null; }) .when(mockDocument) - .save(any(ByteArrayOutputStream.class)); + .save(any(File.class)); doNothing().when(mockDocument).close(); // Initialize a real document for unit tests @@ -298,12 +328,12 @@ class RedactControllerTest { mock(org.apache.pdfbox.pdmodel.PDDocumentInformation.class); when(mockDocument.getDocumentInformation()).thenReturn(mockInfo); - ResponseEntity response = redactController.redactPdf(request); + ResponseEntity response = redactController.redactPdf(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); - verify(mockDocument).save(any(ByteArrayOutputStream.class)); + verify(mockDocument).save(any(File.class)); verify(mockDocument).close(); } } @@ -702,14 +732,14 @@ class RedactControllerTest { request.setConvertPDFToImage(convertToImage); try { - ResponseEntity response = redactController.redactPdf(request); + ResponseEntity response = redactController.redactPdf(request); if (expectSuccess && response != null) { assertNotNull(response); assertEquals(200, response.getStatusCode().value()); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); - verify(mockDocument, times(1)).save(any(ByteArrayOutputStream.class)); + assertTrue(drainBody(response).length > 0); + verify(mockDocument, times(1)).save(any(File.class)); verify(mockDocument, times(1)).close(); } } catch (Exception e) { @@ -727,12 +757,12 @@ class RedactControllerTest { request.setConvertPDFToImage(convertToImage); try { - ResponseEntity response = redactController.redactPDF(request); + ResponseEntity response = redactController.redactPDF(request); if (response != null) { assertNotNull(response); assertEquals(200, response.getStatusCode().value()); - verify(mockDocument, times(1)).save(any(ByteArrayOutputStream.class)); + verify(mockDocument, times(1)).save(any(File.class)); } } catch (Exception e) { log.info("Manual redaction test completed with graceful handling: {}", e.getMessage()); @@ -918,7 +948,7 @@ class RedactControllerTest { request.setListOfText("test"); request.setRedactColor(null); - ResponseEntity response = redactController.redactPdf(request); + ResponseEntity response = redactController.redactPdf(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -942,7 +972,7 @@ class RedactControllerTest { ManualRedactPdfRequest request = createManualRedactPdfRequest(); request.setRedactions(null); - ResponseEntity response = redactController.redactPDF(request); + ResponseEntity response = redactController.redactPDF(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -954,7 +984,7 @@ class RedactControllerTest { ManualRedactPdfRequest request = createManualRedactPdfRequest(); request.setPageNumbers("100-200"); - ResponseEntity response = redactController.redactPDF(request); + ResponseEntity response = redactController.redactPDF(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -1419,12 +1449,12 @@ class RedactControllerTest { request.setUseRegex(false); request.setWholeWordSearch(false); - ResponseEntity response = redactController.redactPdf(request); + ResponseEntity response = redactController.redactPdf(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } } } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RemoveCertSignControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RemoveCertSignControllerTest.java index cf8b03ec9b..1cf95e7e23 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RemoveCertSignControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RemoveCertSignControllerTest.java @@ -2,10 +2,15 @@ package stirling.software.SPDF.controller.api.security; import static org.junit.jupiter.api.Assertions.*; 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.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; @@ -27,16 +32,30 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @DisplayName("RemoveCertSignController Tests") @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class RemoveCertSignControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private RemoveCertSignController removeCertSignController; @@ -44,6 +63,18 @@ class RemoveCertSignControllerTest { @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; + }); try (PDDocument doc = new PDDocument()) { doc.addPage(new PDPage()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -72,10 +103,11 @@ class RemoveCertSignControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = removeCertSignController.removeCertSignPDF(request); + ResponseEntity response = + removeCertSignController.removeCertSignPDF(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -95,7 +127,8 @@ class RemoveCertSignControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = removeCertSignController.removeCertSignPDF(request); + ResponseEntity response = + removeCertSignController.removeCertSignPDF(request); assertNotNull(response.getBody()); } @@ -126,7 +159,8 @@ class RemoveCertSignControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(pdfWithAcroForm)); - ResponseEntity response = removeCertSignController.removeCertSignPDF(request); + ResponseEntity response = + removeCertSignController.removeCertSignPDF(request); assertNotNull(response.getBody()); } @@ -156,7 +190,8 @@ class RemoveCertSignControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(pdfWithSig)); - ResponseEntity response = removeCertSignController.removeCertSignPDF(request); + ResponseEntity response = + removeCertSignController.removeCertSignPDF(request); assertNotNull(response.getBody()); } @@ -176,7 +211,8 @@ class RemoveCertSignControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = removeCertSignController.removeCertSignPDF(request); + ResponseEntity response = + removeCertSignController.removeCertSignPDF(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -194,7 +230,8 @@ class RemoveCertSignControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = removeCertSignController.removeCertSignPDF(request); + ResponseEntity response = + removeCertSignController.removeCertSignPDF(request); assertNotNull(response.getBody()); } @@ -224,7 +261,8 @@ class RemoveCertSignControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(multiPagePdf)); - ResponseEntity response = removeCertSignController.removeCertSignPDF(request); + ResponseEntity response = + removeCertSignController.removeCertSignPDF(request); assertNotNull(response.getBody()); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/SanitizeControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/SanitizeControllerTest.java index 19b77c17f1..a806c1e291 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/SanitizeControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/SanitizeControllerTest.java @@ -3,10 +3,15 @@ package stirling.software.SPDF.controller.api.security; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +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.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; @@ -35,16 +40,30 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.security.SanitizePdfRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @DisplayName("SanitizeController Tests") @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class SanitizeControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private SanitizeController sanitizeController; @@ -52,6 +71,18 @@ class SanitizeControllerTest { @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; + }); try (PDDocument doc = new PDDocument()) { PDPage page = new PDPage(PDRectangle.A4); doc.addPage(page); @@ -139,7 +170,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(jsBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -167,7 +199,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); } } @@ -196,9 +229,10 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(linkBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } } @@ -226,7 +260,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(metaBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); } @@ -252,7 +287,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); } } @@ -283,7 +319,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); } } @@ -312,9 +349,10 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(jsBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -339,7 +377,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); } @@ -360,7 +399,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); } @@ -386,7 +426,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); } @@ -412,7 +453,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/WatermarkControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/WatermarkControllerTest.java index d504ad2606..3081bb8da4 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/WatermarkControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/WatermarkControllerTest.java @@ -2,9 +2,14 @@ package stirling.software.SPDF.controller.api.security; import static org.junit.jupiter.api.Assertions.*; 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.ByteArrayOutputStream; +import java.io.File; +import java.nio.file.Files; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; @@ -28,16 +33,30 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.security.AddWatermarkRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @DisplayName("WatermarkController Tests") @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class WatermarkControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private WatermarkController watermarkController; @@ -45,6 +64,18 @@ class WatermarkControllerTest { @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; + }); try (PDDocument doc = new PDDocument()) { PDPage page = new PDPage(PDRectangle.A4); doc.addPage(page); @@ -91,10 +122,11 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -124,7 +156,8 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); } @@ -154,7 +187,8 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); } @@ -184,7 +218,8 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); } @@ -214,7 +249,8 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); } } @@ -350,9 +386,10 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(multiPagePdf)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } } @@ -391,7 +428,8 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); } @@ -418,7 +456,8 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); } @@ -448,7 +487,8 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); } } diff --git a/app/core/src/test/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdownTest.java b/app/core/src/test/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdownTest.java index ca24ff46f0..b3edb4e451 100644 --- a/app/core/src/test/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdownTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdownTest.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.model.api.converters; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -16,10 +17,12 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; 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 org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.util.PDFToFile; @@ -34,10 +37,11 @@ class ConvertPDFToMarkdownTest { @RestControllerAdvice static class GlobalErrorHandler { @ExceptionHandler(Exception.class) - ResponseEntity handle(Exception ex) { + ResponseEntity handle(Exception ex) { String message = ex.getMessage(); byte[] body = message != null ? message.getBytes(StandardCharsets.UTF_8) : new byte[0]; - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body); + StreamingResponseBody stream = out -> out.write(body); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(stream); } } @@ -49,12 +53,13 @@ class ConvertPDFToMarkdownTest { Mockito.mockConstruction( PDFToFile.class, (mock, ctx) -> { + StreamingResponseBody stream = out -> out.write(md); when(mock.processPdfToMarkdown(any(MultipartFile.class))) .thenAnswer( inv -> ResponseEntity.ok() .header("Content-Type", "text/markdown") - .body(md)); + .body(stream)); })) { MockMvc mvc = mockMvc(); @@ -66,7 +71,12 @@ class ConvertPDFToMarkdownTest { "application/pdf", new byte[] {1, 2, 3}); - mvc.perform(multipart("/api/v1/convert/pdf/markdown").file(file)) + MvcResult asyncResult = + mvc.perform(multipart("/api/v1/convert/pdf/markdown").file(file)) + .andExpect(request().asyncStarted()) + .andReturn(); + + mvc.perform(asyncDispatch(asyncResult)) .andExpect(status().isOk()) .andExpect(header().string("Content-Type", "text/markdown")) .andExpect(content().bytes(md)); 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 302cb8fd81..42b56a9cfa 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 @@ -9,6 +9,8 @@ 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; @@ -348,6 +350,123 @@ class ApiDocServiceTest { 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); diff --git a/app/proprietary/build.gradle b/app/proprietary/build.gradle index e0c294e09a..1c03a435f9 100644 --- a/app/proprietary/build.gradle +++ b/app/proprietary/build.gradle @@ -16,6 +16,8 @@ spotless { target 'src/**/java/**/*.java' targetExclude 'src/main/java/org/apache/**' googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false) + // google-java-format 1.28.0 bundles Guava 32.x which crashes Spotless lint on JDK 24/25 + suppressLintsFor { setStep('google-java-format') } importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling") trimTrailingWhitespace() @@ -37,7 +39,7 @@ spotless { } dependencies { implementation project(':common') - api 'com.google.guava:guava:33.4.8-jre' + api 'com.google.guava:guava:33.5.0-jre' api 'org.springframework:spring-jdbc' api 'org.springframework:spring-webmvc' @@ -51,18 +53,26 @@ dependencies { api 'org.springframework.boot:spring-boot-starter-mail' api 'org.springframework.boot:spring-boot-starter-cache' api 'com.github.ben-manes.caffeine:caffeine' - api 'io.swagger.core.v3:swagger-core-jakarta:2.2.43' - implementation 'com.bucket4j:bucket4j_jdk17-core:8.16.1' + api 'io.swagger.core.v3:swagger-core-jakarta:2.2.46' + implementation 'com.bucket4j:bucket4j_jdk17-core:8.17.0' // https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17 implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" + // Tabula table extraction — used by MathAuditorOrchestrator + implementation ('technology.tabula:tabula:1.0.5') { + exclude group: 'org.slf4j', module: 'slf4j-simple' + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'com.google.code.gson', module: 'gson' + } + implementation 'com.google.code.gson:gson:2.13.2' + api 'io.micrometer:micrometer-registry-prometheus' api "io.jsonwebtoken:jjwt-api:$jwtVersion" runtimeOnly "io.jsonwebtoken:jjwt-impl:$jwtVersion" runtimeOnly "io.jsonwebtoken:jjwt-jackson:$jwtVersion" - runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database + 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.10' implementation('com.coveo:saml-client:5.0.0') { exclude group: 'org.opensaml', module: 'opensaml-core' 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 aa79f9b05e..a45714ba65 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 @@ -10,6 +10,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.support.TaskExecutorAdapter; import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.security.concurrent.DelegatingSecurityContextExecutor; @Configuration @EnableAsync @@ -48,4 +49,19 @@ public class AsyncConfig { adapter.setTaskDecorator(new MDCContextTaskDecorator()); return adapter; } + + /** + * AI orchestration runs on a background executor, so the incoming request's {@code + * SecurityContext} must be propagated for downstream calls to see the authenticated user. + * Without this, {@code JobOwnershipService} scopes job keys without a user prefix and + * authenticated downloads fail with 403; {@code InternalApiClient} also falls back to the + * internal-API-user key instead of the caller's. + */ + @Bean(name = "aiStreamExecutor") + public Executor aiStreamExecutor() { + TaskExecutorAdapter adapter = + new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor()); + adapter.setTaskDecorator(new MDCContextTaskDecorator()); + return new DelegatingSecurityContextExecutor(adapter); + } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AiEngineController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AiEngineController.java new file mode 100644 index 0000000000..279f408815 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AiEngineController.java @@ -0,0 +1,213 @@ +package stirling.software.proprietary.controller.api; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +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 org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +import jakarta.validation.Valid; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.model.job.ResultFile; +import stirling.software.common.service.JobOwnershipService; +import stirling.software.common.service.TaskManager; +import stirling.software.proprietary.model.api.ai.AiWorkflowRequest; +import stirling.software.proprietary.model.api.ai.AiWorkflowResponse; +import stirling.software.proprietary.model.api.ai.AiWorkflowResultFile; +import stirling.software.proprietary.service.AiEngineClient; +import stirling.software.proprietary.service.AiWorkflowService; + +import tools.jackson.core.JacksonException; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; + +@Slf4j +@RestController +@RequestMapping("/api/v1/ai") +@Hidden +@Tag(name = "AI Engine", description = "Endpoints for AI-powered PDF workflows") +public class AiEngineController { + + private final AiEngineClient aiEngineClient; + private final AiWorkflowService aiWorkflowService; + private final ObjectMapper objectMapper; + private final Executor aiStreamExecutor; + private final TaskManager taskManager; + private final JobOwnershipService jobOwnershipService; + + /** + * SSE emitter timeout. Long enough to accommodate multi-gigabyte PDF workflows (OCR on a + * 1000-page scan, splitting a huge PDF, etc.) without the emitter completing out from under the + * executor. Configurable via {@code stirling.ai.streamTimeoutMs}. + */ + @Value("${stirling.ai.streamTimeoutMs:1800000}") + private long streamTimeoutMs; + + public AiEngineController( + AiEngineClient aiEngineClient, + AiWorkflowService aiWorkflowService, + ObjectMapper objectMapper, + @Qualifier("aiStreamExecutor") Executor aiStreamExecutor, + TaskManager taskManager, + JobOwnershipService jobOwnershipService) { + this.aiEngineClient = aiEngineClient; + this.aiWorkflowService = aiWorkflowService; + this.objectMapper = objectMapper; + this.aiStreamExecutor = aiStreamExecutor; + this.taskManager = taskManager; + this.jobOwnershipService = jobOwnershipService; + } + + @GetMapping("/health") + @Operation( + summary = "AI engine health check", + description = "Returns the health status of the AI engine including configured models") + public ResponseEntity health() throws IOException { + String response = aiEngineClient.get("/health"); + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(response); + } + + @PostMapping(value = "/orchestrate", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation( + summary = "Run an AI workflow against a PDF", + description = + "Accepts PDF uploads and a user message and returns an AI workflow result." + + " When the workflow produces files, they are registered with the job" + + " system and downloadable via GET /api/v1/general/files/{fileId}.") + public AiWorkflowResponse orchestrate(@Valid @ModelAttribute AiWorkflowRequest request) + throws IOException { + AiWorkflowResponse result = aiWorkflowService.orchestrate(request); + registerFileResultAsJob(result); + return result; + } + + @PostMapping(value = "/orchestrate/stream", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation( + summary = "Run an AI workflow with streaming progress", + description = + "Accepts a PDF upload and a user message, returns SSE events with progress" + + " updates followed by the final AI workflow result") + public SseEmitter orchestrateStream(@Valid @ModelAttribute AiWorkflowRequest request) { + SseEmitter emitter = new SseEmitter(streamTimeoutMs); + + emitter.onTimeout( + () -> { + // Emit an explicit error frame so the frontend reports a timeout rather than + // silently seeing the stream end without a result. + log.warn( + "SSE emitter timed out for AI orchestration stream after {} ms", + streamTimeoutMs); + sendEvent( + emitter, + "error", + Map.of( + "message", + "AI workflow timed out after " + + (streamTimeoutMs / 1000) + + " seconds")); + emitter.complete(); + }); + emitter.onError(e -> log.warn("SSE emitter error for AI orchestration stream", e)); + + aiStreamExecutor.execute(() -> runOrchestrationStream(request, emitter)); + + return emitter; + } + + private void runOrchestrationStream(AiWorkflowRequest request, SseEmitter emitter) { + try { + AiWorkflowResponse result = + aiWorkflowService.orchestrate( + request, progress -> sendEvent(emitter, "progress", progress)); + registerFileResultAsJob(result); + sendEvent(emitter, "result", result); + emitter.complete(); + } catch (Exception e) { + log.error("AI orchestration stream failed", e); + // Emit an error frame for the frontend and then complete normally. Using + // completeWithError here as well would double-complete the emitter - the error + // frame already conveys the failure to the client. + sendEvent(emitter, "error", Map.of("message", e.getMessage())); + emitter.complete(); + } + } + + /** + * Register any file results produced by the workflow with {@link TaskManager} so they are + * downloadable via {@code GET /api/v1/general/files/{fileId}}. Uses {@code + * setMultipleFileResults} so the fileIds we registered earlier are not mangled by TaskManager's + * ZIP auto-extract path. + */ + private void registerFileResultAsJob(AiWorkflowResponse result) { + List files = result.getResultFiles(); + if (files == null || files.isEmpty()) { + return; + } + // Scope the job key to the current user so the download endpoint's ownership check + // passes when security is enabled. NoOpJobOwnershipService returns the UUID unchanged + // when security is off. + String jobKey = + jobOwnershipService.createScopedJobKey(java.util.UUID.randomUUID().toString()); + taskManager.createTask(jobKey); + List jobFiles = + files.stream() + .map( + f -> + ResultFile.builder() + .fileId(f.getFileId()) + .fileName(f.getFileName()) + .contentType(f.getContentType()) + .build()) + .toList(); + taskManager.setMultipleFileResults(jobKey, jobFiles); + taskManager.setComplete(jobKey); + } + + private void sendEvent(SseEmitter emitter, String name, Object data) { + try { + emitter.send(SseEmitter.event().name(name).data(data, MediaType.APPLICATION_JSON)); + } catch (IOException e) { + log.debug("Failed to send SSE event (client may have disconnected)", e); + } + } + + @PostMapping(value = "/pdf/edit", consumes = MediaType.APPLICATION_JSON_VALUE) + @Operation( + summary = "Generate a PDF edit plan", + description = + "Sends a user message to the PDF edit agent which returns a structured plan" + + " of tool operations to perform") + public ResponseEntity pdfEdit(@RequestBody String requestBody) throws IOException { + validateJson(requestBody); + String response = aiEngineClient.post("/api/v1/pdf/edit", requestBody); + return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(response); + } + + private void validateJson(String body) { + try { + objectMapper.readValue(body, JsonNode.class); + } catch (JacksonException e) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, "Request body is not valid JSON"); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java index 4a2f2df00f..4dff0c99a0 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java @@ -1,5 +1,6 @@ package stirling.software.proprietary.controller.api; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; @@ -239,7 +240,7 @@ public class AuditDashboardController { csv.append(escapeCSV(event.getData())).append("\n"); } - byte[] csvBytes = csv.toString().getBytes(); + byte[] csvBytes = csv.toString().getBytes(StandardCharsets.UTF_8); // Set up HTTP headers for download HttpHeaders headers = new HttpHeaders(); 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 new file mode 100644 index 0000000000..3691266b79 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/MathAuditorAgentController.java @@ -0,0 +1,97 @@ +package stirling.software.proprietary.controller.api; + +import java.io.IOException; +import java.math.BigDecimal; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +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 org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.proprietary.model.api.ai.Verdict; +import stirling.software.proprietary.service.MathAuditorOrchestrator; + +/** + * Public entry point for the Math Auditor Agent (mathAuditorAgent). + * + *

Accepts a PDF from the client, hands it to the {@link MathAuditorOrchestrator} which runs the + * multi-round Java-Python negotiation, and returns the Auditor's {@link Verdict}. + * + *

The raw PDF never leaves Java. Python receives only structured text and CSV data. + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/ai") +@RequiredArgsConstructor +@Tag(name = "AI Engine", description = "AI-powered document analysis endpoints.") +public class MathAuditorAgentController { + + private final MathAuditorOrchestrator orchestrator; + + @PostMapping(value = "/math-auditor-agent", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation( + summary = "Validate mathematical calculations in a PDF", + description = + """ + Analyses a PDF document for mathematical errors using the Math Auditor Agent. + + The auditor checks: + - Table row and column totals (tally errors) + - Inline arithmetic expressions (e.g. "100 + 200 = 300") + - Cross-page figure consistency (same figure cited differently on different pages) + - Prose claims about percentages, growth rates, and comparisons + + The PDF is processed entirely on the Java side; only extracted text and table data + are sent to the AI engine. + + Input: PDF Output: JSON Type: SISO + """) + public ResponseEntity mathAuditorAgent( + @Parameter(description = "The PDF document to audit", required = true) + @RequestParam("fileInput") + MultipartFile fileInput, + @Parameter( + description = + "Arithmetic tolerance — differences smaller than this are" + + " ignored (default: 0.01)") + @RequestParam(value = "tolerance", defaultValue = "0.01") + BigDecimal tolerance) { + + String contentType = fileInput.getContentType(); + if (contentType == null || !contentType.equals("application/pdf")) { + return ResponseEntity.badRequest().build(); + } + + if (tolerance.compareTo(BigDecimal.ZERO) < 0) { + return ResponseEntity.badRequest().build(); + } + + String safeName = + fileInput.getOriginalFilename() != null + ? fileInput.getOriginalFilename().replaceAll("[\\r\\n]", "_") + : ""; + log.info("[math-auditor-agent] request file={} tolerance={}", safeName, tolerance); + + try { + Verdict verdict = orchestrator.audit(fileInput, tolerance); + return ResponseEntity.ok(verdict); + } catch (IOException e) { + log.error("[math-auditor-agent] IO error during audit", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } catch (Exception e) { + log.error("[math-auditor-agent] unexpected error during audit", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java index 37d8f3999b..3295e62abb 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java @@ -19,6 +19,8 @@ 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.tags.Tag; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -37,6 +39,9 @@ import stirling.software.proprietary.service.SignatureService; @RestController @RequestMapping("/api/v1/proprietary/signatures") @RequiredArgsConstructor +@Tag( + name = "Saved Signatures", + description = "Manage saved signature templates for authenticated users") public class SignatureController { private final SignatureService signatureService; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiConversationMessage.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiConversationMessage.java new file mode 100644 index 0000000000..32a811302c --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiConversationMessage.java @@ -0,0 +1,22 @@ +package stirling.software.proprietary.model.api.ai; + +import io.swagger.v3.oas.annotations.media.Schema; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import lombok.Data; + +@Data +@Schema(description = "A prior message in the chat conversation") +public class AiConversationMessage { + + @NotNull + @NotBlank + @Schema(description = "The role of the message sender", example = "user") + private String role; + + @NotNull + @Schema(description = "The content of the message") + private String content; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiPdfContentType.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiPdfContentType.java new file mode 100644 index 0000000000..d7c38a723c --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiPdfContentType.java @@ -0,0 +1,57 @@ +package stirling.software.proprietary.model.api.ai; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Types of content that can be extracted from a PDF and sent to the AI. + * + *

Values MUST match {@code PdfContentType} in {@code engine/src/stirling/contracts/common.py}. + */ +public enum AiPdfContentType { + // Document-level structured data + PAGE_LAYOUT("page_layout"), + DOCUMENT_METADATA("document_metadata"), + ENCRYPTION_INFO("encryption_info"), + BOOKMARKS("bookmarks"), + LAYERS("layers"), + EMBEDDED_FILES("embedded_files"), + JAVASCRIPT("javascript"), + LINKS("links"), + IMAGE_INFO("image_info"), + FONTS("fonts"), + + // Text and content + PAGE_TEXT("page_text"), + FULL_TEXT("full_text"), + FORM_FIELDS("form_fields"), + ANNOTATIONS("annotations"), + SIGNATURES("signatures"), + STRUCTURE_TREE("structure_tree"), + XMP_METADATA("xmp_metadata"), + + // Heavy content + COMPLIANCE("compliance"), + IMAGES("images"); + + private final String value; + + AiPdfContentType(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static AiPdfContentType fromValue(String value) { + for (AiPdfContentType type : values()) { + if (type.value.equals(value)) { + return type; + } + } + throw new IllegalArgumentException("Unknown PDF content type: " + value); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowFileInput.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowFileInput.java new file mode 100644 index 0000000000..c83fa55698 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowFileInput.java @@ -0,0 +1,22 @@ +package stirling.software.proprietary.model.api.ai; + +import org.springframework.http.MediaType; +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.media.Schema; + +import jakarta.validation.constraints.NotNull; + +import lombok.Data; + +@Data +@Schema(description = "A single PDF file input") +public class AiWorkflowFileInput { + + @NotNull + @Schema( + description = "The input PDF file", + contentMediaType = MediaType.APPLICATION_PDF_VALUE, + format = "binary") + private MultipartFile fileInput; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowFileRequest.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowFileRequest.java new file mode 100644 index 0000000000..f238670287 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowFileRequest.java @@ -0,0 +1,22 @@ +package stirling.software.proprietary.model.api.ai; + +import java.util.ArrayList; +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +@Data +@Schema(description = "Per-file content extraction request from the AI engine") +public class AiWorkflowFileRequest { + + @Schema(description = "Original filename of the requested file", example = "contract.pdf") + private String fileName; + + @Schema(description = "Specific 1-based page numbers to extract from this file") + private List pageNumbers = new ArrayList<>(); + + @Schema(description = "Content types to extract from this file") + private List contentTypes = new ArrayList<>(); +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowOutcome.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowOutcome.java new file mode 100644 index 0000000000..577f24e5fd --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowOutcome.java @@ -0,0 +1,44 @@ +package stirling.software.proprietary.model.api.ai; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Discriminator values for AI workflow responses. + * + *

Values MUST match {@code WorkflowOutcome} in {@code engine/src/stirling/contracts/common.py}. + */ +public enum AiWorkflowOutcome { + ANSWER("answer"), + NOT_FOUND("not_found"), + NEED_CONTENT("need_content"), + PLAN("plan"), + NEED_CLARIFICATION("need_clarification"), + CANNOT_DO("cannot_do"), + DRAFT("draft"), + TOOL_CALL("tool_call"), + COMPLETED("completed"), + UNSUPPORTED_CAPABILITY("unsupported_capability"), + CANNOT_CONTINUE("cannot_continue"); + + private final String value; + + AiWorkflowOutcome(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static AiWorkflowOutcome fromValue(String value) { + for (AiWorkflowOutcome outcome : values()) { + if (outcome.value.equals(value)) { + return outcome; + } + } + throw new IllegalArgumentException("Unknown AI workflow outcome: " + value); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowPhase.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowPhase.java new file mode 100644 index 0000000000..b1ab9fff23 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowPhase.java @@ -0,0 +1,34 @@ +package stirling.software.proprietary.model.api.ai; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** Progress phases emitted during AI workflow orchestration. */ +public enum AiWorkflowPhase { + ANALYZING("analyzing"), + CALLING_ENGINE("calling_engine"), + EXTRACTING_CONTENT("extracting_content"), + EXECUTING_TOOL("executing_tool"), + PROCESSING("processing"); + + private final String value; + + AiWorkflowPhase(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static AiWorkflowPhase fromValue(String value) { + for (AiWorkflowPhase phase : values()) { + if (phase.value.equals(value)) { + return phase; + } + } + throw new IllegalArgumentException("Unknown AI workflow phase: " + value); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowProgressEvent.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowProgressEvent.java new file mode 100644 index 0000000000..92c15a0004 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowProgressEvent.java @@ -0,0 +1,38 @@ +package stirling.software.proprietary.model.api.ai; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AiWorkflowProgressEvent { + private AiWorkflowPhase phase; + private long timestamp; + + /** The tool endpoint path being executed, for {@link AiWorkflowPhase#EXECUTING_TOOL} events. */ + private String tool; + + /** + * 1-based index of the current plan step, for {@link AiWorkflowPhase#EXECUTING_TOOL} events. + */ + private Integer stepIndex; + + /** Total number of plan steps, for {@link AiWorkflowPhase#EXECUTING_TOOL} events. */ + private Integer stepCount; + + public static AiWorkflowProgressEvent of(AiWorkflowPhase phase) { + return new AiWorkflowProgressEvent(phase, System.currentTimeMillis(), null, null, null); + } + + public static AiWorkflowProgressEvent executingTool(String tool, int stepIndex, int stepCount) { + return new AiWorkflowProgressEvent( + AiWorkflowPhase.EXECUTING_TOOL, + System.currentTimeMillis(), + tool, + stepIndex, + stepCount); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowRequest.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowRequest.java new file mode 100644 index 0000000000..da327177c4 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowRequest.java @@ -0,0 +1,30 @@ +package stirling.software.proprietary.model.api.ai; + +import java.util.ArrayList; +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import lombok.Data; + +@Data +@Schema(description = "Run an AI workflow against one or more PDF files") +public class AiWorkflowRequest { + + @NotNull + @Schema(description = "The input PDF files") + private List fileInputs; + + @NotBlank + @Schema(description = "The user message to orchestrate", example = "Summarise these documents") + private String userMessage; + + @Schema( + description = + "Prior chat messages exchanged between the user and the assistant, ordered" + + " oldest-first. Excludes the current userMessage.") + private List conversationHistory = new ArrayList<>(); +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowResponse.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowResponse.java new file mode 100644 index 0000000000..ff28b2e9eb --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowResponse.java @@ -0,0 +1,82 @@ +package stirling.software.proprietary.model.api.ai; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +@Data +@Schema(description = "Structured AI workflow result") +public class AiWorkflowResponse { + + @Schema(description = "Workflow outcome") + private AiWorkflowOutcome outcome; + + @Schema(description = "Answer returned by the AI workflow when applicable") + private String answer; + + @Schema(description = "Summary returned by the AI workflow when applicable") + private String summary; + + @Schema(description = "Rationale returned by the AI workflow when applicable") + private String rationale; + + @Schema(description = "Reason when the AI workflow cannot proceed") + private String reason; + + @Schema(description = "Clarification question for the user when more input is required") + private String question; + + @Schema( + description = + "Unsupported capability identifier when the workflow cannot route the request") + private String capability; + + @Schema(description = "Message returned for unsupported capability outcomes") + private String message; + + @Schema(description = "Supporting evidence snippets from extracted PDF text") + private List evidence = new ArrayList<>(); + + @Schema(description = "Structured tool steps when the workflow returns a plan") + private List> steps = new ArrayList<>(); + + @Schema( + description = + "Tool endpoint path for tool_call outcomes (e.g. /api/v1/misc/compress-pdf)") + private String tool; + + @Schema(description = "Tool parameters for tool_call outcomes") + private Map parameters; + + @Schema(description = "Result file ID after tool execution completes (single-file result)") + private String fileId; + + @Schema(description = "Result filename after tool execution completes (single-file result)") + private String fileName; + + @Schema(description = "Result MIME type after tool execution completes (single-file result)") + private String contentType; + + @Schema( + description = + "Result files produced by the workflow. Always populated on completed outcomes" + + " with at least one entry; for single-file results this mirrors" + + " fileId/fileName/contentType.") + private List resultFiles = new ArrayList<>(); + + @Schema(description = "Per-file text extraction requests from the AI engine") + private List files = new ArrayList<>(); + + @Schema(description = "Maximum number of pages the AI engine wants text extracted from") + private Integer maxPages; + + @Schema(description = "Maximum number of characters the AI engine wants extracted") + private Integer maxCharacters; + + @Schema(description = "AI engine capability to resume with on the next turn") + private String resumeWith; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowResultFile.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowResultFile.java new file mode 100644 index 0000000000..57207d2bc9 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowResultFile.java @@ -0,0 +1,24 @@ +package stirling.software.proprietary.model.api.ai; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** A single file produced by a completed AI workflow. */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Descriptor for a file produced by an AI workflow") +public class AiWorkflowResultFile { + + @Schema(description = "Stirling file ID — download with GET /api/v1/general/files/{fileId}") + private String fileId; + + @Schema(description = "Original filename for the file") + private String fileName; + + @Schema(description = "MIME type of the file", example = "application/pdf") + private String contentType; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowTextSelection.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowTextSelection.java new file mode 100644 index 0000000000..265d989dab --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowTextSelection.java @@ -0,0 +1,16 @@ +package stirling.software.proprietary.model.api.ai; + +import io.swagger.v3.oas.annotations.media.Schema; + +import lombok.Data; + +@Data +@Schema(description = "Page-scoped extracted text selection") +public class AiWorkflowTextSelection { + + @Schema(description = "1-based page number", example = "2") + private Integer pageNumber; + + @Schema(description = "Extracted text or evidence snippet") + private String text; +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AuditDiscrepancy.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AuditDiscrepancy.java new file mode 100644 index 0000000000..ba7cd596d7 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AuditDiscrepancy.java @@ -0,0 +1,21 @@ +package stirling.software.proprietary.model.api.ai; + +/** + * A single mathematical error found by the Python Auditor. + * + * @param page 0-indexed page number where the discrepancy appears. + * @param kind Category of the discrepancy. + * @param severity Whether this is a definite mistake or a possible ambiguity. + * @param description Human-readable explanation of the error. + * @param stated The value as it appears in the document. + * @param expected The value the Auditor calculated. + * @param context Surrounding text or table fragment for traceability. + */ +public record AuditDiscrepancy( + int page, + DiscrepancyKind kind, + AuditSeverity severity, + String description, + String stated, + String expected, + String context) {} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AuditSeverity.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AuditSeverity.java new file mode 100644 index 0000000000..1bef46a443 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AuditSeverity.java @@ -0,0 +1,17 @@ +package stirling.software.proprietary.model.api.ai; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Severity of a mathematical discrepancy. Mirrors the Python {@code Severity} enum in {@code + * contracts/ledger.py}. + */ +public enum AuditSeverity { + ERROR, + WARNING; + + @JsonValue + public String toJson() { + return name().toLowerCase(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/DiscrepancyKind.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/DiscrepancyKind.java new file mode 100644 index 0000000000..54c7ac66f5 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/DiscrepancyKind.java @@ -0,0 +1,19 @@ +package stirling.software.proprietary.model.api.ai; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Category of a mathematical discrepancy found by the auditor. Mirrors the Python {@code + * DiscrepancyKind} enum in {@code contracts/ledger.py}. + */ +public enum DiscrepancyKind { + TALLY, + ARITHMETIC, + CONSISTENCY, + STATEMENT; + + @JsonValue + public String toJson() { + return name().toLowerCase(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/Evidence.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/Evidence.java new file mode 100644 index 0000000000..98b3a09454 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/Evidence.java @@ -0,0 +1,24 @@ +package stirling.software.proprietary.model.api.ai; + +import java.util.List; + +/** + * Java's fulfilment package: the extracted content the Python Auditor asked for. + * + *

Sent after Java has fulfilled a {@link Requisition}. When {@code finalRound} is {@code true}, + * the Auditor must return a {@link Verdict} — Java will not honour further Requisitions. + * + * @param sessionId Matches the session opened by the original client request. + * @param folios The extracted page content for each page in the Requisition. + * @param round Which negotiation round this Evidence belongs to (1–3). + * @param finalRound When {@code true}, the Auditor must commit to a Verdict this round. + * @param unauditablePages Pages that were requested but could not be fulfilled — e.g. OCR was asked + * for but is not yet wired. The Auditor echoes these into {@link Verdict#unauditablePages()} so + * the client knows coverage is incomplete. + */ +public record Evidence( + String sessionId, + List folios, + int round, + boolean finalRound, + List unauditablePages) {} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/Folio.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/Folio.java new file mode 100644 index 0000000000..7126e59354 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/Folio.java @@ -0,0 +1,17 @@ +package stirling.software.proprietary.model.api.ai; + +import java.util.List; + +/** + * One page's worth of extracted content, assembled by Java in response to a {@link Requisition}. + * + *

Only the fields explicitly requested will be populated; unused fields are {@code null}. + * + * @param page 0-indexed page number. + * @param text PDFBox plain-text extraction result (null if not requested). + * @param tables Tabula CSV strings, one per table found on the page (null if not requested). + * @param ocrText OCRmyPDF output text (null if not requested or OCR not available). + * @param ocrConfidence Mean character confidence from OCRmyPDF, 0.0–1.0 (null if OCR not run). + */ +public record Folio( + int page, String text, List tables, String ocrText, Double ocrConfidence) {} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/FolioManifest.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/FolioManifest.java new file mode 100644 index 0000000000..49ba60facb --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/FolioManifest.java @@ -0,0 +1,18 @@ +package stirling.software.proprietary.model.api.ai; + +import java.util.List; + +/** + * Java's opening move in the audit negotiation. + * + *

Built from a cheap PDFBox scan (character count + image detection) with no OCR or Tabula + * involved. Sent to the Python Examiner, which replies with a {@link Requisition}. + * + * @param sessionId Opaque handle Java uses to locate the PDF on disk during this audit session. + * @param pageCount Total number of pages in the document. + * @param folioTypes One {@link FolioType} per page (0-indexed). {@code folioTypes.size() == + * pageCount}. + * @param round Which negotiation round this manifest belongs to (1–3). + */ +public record FolioManifest( + String sessionId, int pageCount, List folioTypes, int round) {} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/FolioType.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/FolioType.java new file mode 100644 index 0000000000..a10625deb4 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/FolioType.java @@ -0,0 +1,21 @@ +package stirling.software.proprietary.model.api.ai; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Java's classification of a single PDF page after a cheap PDFBox character-count scan. Mirrors the + * Python {@code FolioType} enum in {@code ledger/models.py}. + */ +public enum FolioType { + /** Selectable text layer is present — PDFBox can extract text directly. */ + TEXT, + /** Image-only page — OCRmyPDF is required before any text is available. */ + IMAGE, + /** Partial text layer plus embedded images — both PDFBox and OCRmyPDF may be useful. */ + MIXED; + + @JsonValue + public String toJson() { + return name().toLowerCase(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/Requisition.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/Requisition.java new file mode 100644 index 0000000000..05de6df97e --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/Requisition.java @@ -0,0 +1,30 @@ +package stirling.software.proprietary.model.api.ai; + +import java.util.List; + +/** + * The Python Examiner's shopping list: which pages Java must extract before the Auditor can form an + * opinion. + * + *

Java parses this from the Examiner's response, fulfils it (text / tables / OCR), and sends the + * results back as an {@link Evidence} payload. + * + * @param type Discriminator — always {@code "requisition"}. + * @param needText 0-indexed page numbers requiring PDFBox plain-text extraction. + * @param needTables 0-indexed page numbers requiring Tabula CSV extraction. + * @param needOcr 0-indexed page numbers requiring OCRmyPDF. + * @param rationale Human-readable reason logged for observability. + */ +public record Requisition( + String type, + List needText, + List needTables, + List needOcr, + String rationale) { + + public boolean isEmpty() { + return (needText == null || needText.isEmpty()) + && (needTables == null || needTables.isEmpty()) + && (needOcr == null || needOcr.isEmpty()); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/Verdict.java b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/Verdict.java new file mode 100644 index 0000000000..4d859f76c2 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/Verdict.java @@ -0,0 +1,43 @@ +package stirling.software.proprietary.model.api.ai; + +import java.util.List; + +/** + * The Auditor's final opinion on the document's mathematical integrity. + * + *

This is the terminal message in the audit negotiation; Java returns it to the client once + * received from Python. + * + * @param type Discriminator — always {@code "verdict"}. + * @param sessionId Matches the session opened by the original client request. + * @param discrepancies Every mathematical error found, sorted by page. + * @param pagesExamined 0-indexed page numbers the Auditor actually inspected. + * @param roundsTaken How many negotiation rounds were needed (1–3). + * @param summary One or two sentences suitable for the end user. + * @param clean {@code true} iff no errors were found (warnings are tolerated). + * @param unauditablePages Pages that could not be audited — typically image-only pages for which + * OCR was requested but is not yet wired. The client should indicate that these pages were not + * checked. + */ +public record Verdict( + String type, + String sessionId, + List discrepancies, + List pagesExamined, + int roundsTaken, + String summary, + boolean clean, + List unauditablePages) { + + public long errorCount() { + return discrepancies == null + ? 0 + : discrepancies.stream().filter(d -> d.severity() == AuditSeverity.ERROR).count(); + } + + public long warningCount() { + return discrepancies == null + ? 0 + : discrepancies.stream().filter(d -> d.severity() == AuditSeverity.WARNING).count(); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/pdf/FlexibleCSVWriter.java b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/FlexibleCSVWriter.java new file mode 100644 index 0000000000..a22357a7ec --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/pdf/FlexibleCSVWriter.java @@ -0,0 +1,17 @@ +package stirling.software.proprietary.pdf; + +import org.apache.commons.csv.CSVFormat; + +import technology.tabula.writers.CSVWriter; + +/** Exposes Tabula's protected {@link CSVWriter#CSVWriter(CSVFormat)} constructor. */ +public class FlexibleCSVWriter extends CSVWriter { + + public FlexibleCSVWriter() { + super(); + } + + public FlexibleCSVWriter(CSVFormat csvFormat) { + super(csvFormat); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java index 956f91b7a2..456c5a7c34 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java @@ -159,10 +159,12 @@ public class SecurityConfiguration { firewall.setAllowedHeaderValues( headerValue -> headerValue != null && allowedChars.matcher(headerValue).matches()); - // Apply the same rules to parameter values for consistency. + // Allow non-ASCII characters and newlines in parameter values. + Pattern allowedParamChars = Pattern.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]\\r\\n]*"); firewall.setAllowedParameterValues( parameterValue -> - parameterValue != null && allowedChars.matcher(parameterValue).matches()); + parameterValue != null + && allowedParamChars.matcher(parameterValue).matches()); return firewall; } @@ -291,7 +293,12 @@ public class SecurityConfiguration { http.addFilterBefore( userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(rateLimitingFilter, UsernamePasswordAuthenticationFilter.class) + // TODO: IPRateLimitingFilter disabled — limit is 1M (no-op) and raw Filter + // impl causes Spring Security async dispatch bug (response already committed + // errors on StreamingResponseBody endpoints). Re-enable once converted to + // OncePerRequestFilter with proper config-driven limits. + // .addFilterBefore(rateLimitingFilter, + // UsernamePasswordAuthenticationFilter.class) .addFilterBefore(jwtAuthenticationFilter, UserAuthenticationFilter.class); http.sessionManagement( diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java index ac2051196b..1653bca0ef 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java @@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -35,6 +36,7 @@ import tools.jackson.databind.ObjectMapper; @RestController @RequestMapping("/api/v1/ui-data") @RequiredArgsConstructor +@Tag(name = "UI Data") public class UIDataTessdataController { private static final Pattern INVALID_LANG_CHARS_PATTERN = Pattern.compile("[^A-Za-z0-9_+\\-]"); 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 ea960207dd..e18c101e1e 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 @@ -198,6 +198,15 @@ public class UserService implements UserServiceInterface { return user.getApiKey(); } + @Override + public String getCurrentUserApiKey() { + String username = getCurrentUsername(); + if (username == null || username.isEmpty()) { + throw new IllegalStateException("Cannot determine calling user for API key lookup"); + } + return getApiKeyForUser(username); + } + public boolean isValidApiKey(String apiKey) { return userRepository.findByApiKey(apiKey).isPresent(); } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/AiEngineClient.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/AiEngineClient.java new file mode 100644 index 0000000000..753331b124 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/AiEngineClient.java @@ -0,0 +1,108 @@ +package stirling.software.proprietary.service; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.model.ApplicationProperties; + +@Slf4j +@Service +public class AiEngineClient { + + private final ApplicationProperties applicationProperties; + private final HttpClient httpClient; + + public AiEngineClient(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + this.httpClient = + HttpClient.newBuilder() + .connectTimeout( + Duration.ofSeconds( + applicationProperties.getAiEngine().getTimeoutSeconds())) + .build(); + } + + public String post(String path, String jsonBody) throws IOException { + ApplicationProperties.AiEngine config = applicationProperties.getAiEngine(); + if (!config.isEnabled()) { + throw new ResponseStatusException( + HttpStatus.SERVICE_UNAVAILABLE, "AI engine is not enabled"); + } + + String url = config.getUrl().stripTrailing() + path; + log.debug("Proxying AI engine request to {}", url); + + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .timeout(Duration.ofSeconds(config.getTimeoutSeconds())) + .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) + .build(); + + HttpResponse response = sendRequest(request); + + log.debug("AI engine responded with status {}", response.statusCode()); + checkResponseStatus(response); + return response.body(); + } + + public String get(String path) throws IOException { + ApplicationProperties.AiEngine config = applicationProperties.getAiEngine(); + if (!config.isEnabled()) { + throw new ResponseStatusException( + HttpStatus.SERVICE_UNAVAILABLE, "AI engine is not enabled"); + } + + String url = config.getUrl().stripTrailing() + path; + log.debug("Proxying AI engine GET request to {}", url); + + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Accept", "application/json") + .timeout(Duration.ofSeconds(config.getTimeoutSeconds())) + .GET() + .build(); + + HttpResponse response = sendRequest(request); + + log.debug("AI engine responded with status {}", response.statusCode()); + checkResponseStatus(response); + return response.body(); + } + + private HttpResponse sendRequest(HttpRequest request) throws IOException { + try { + return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new ResponseStatusException( + HttpStatus.SERVICE_UNAVAILABLE, "AI engine request was interrupted"); + } + } + + private void checkResponseStatus(HttpResponse response) { + int status = response.statusCode(); + if (status >= 500) { + throw new ResponseStatusException( + HttpStatus.BAD_GATEWAY, "AI engine returned error: " + status); + } + if (status >= 400) { + throw new ResponseStatusException( + HttpStatus.valueOf(status), + "AI engine returned client error: " + response.body()); + } + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/AiWorkflowService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/AiWorkflowService.java new file mode 100644 index 0000000000..14bc56ed98 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/AiWorkflowService.java @@ -0,0 +1,428 @@ +package stirling.software.proprietary.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.io.FilenameUtils; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.MediaTypeFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.multipart.MultipartFile; + +import io.github.pixee.security.Filenames; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.service.FileStorage; +import stirling.software.common.service.InternalApiClient; +import stirling.software.common.service.ToolMetadataService; +import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; +import stirling.software.common.util.ZipExtractionUtils; +import stirling.software.proprietary.model.api.ai.AiConversationMessage; +import stirling.software.proprietary.model.api.ai.AiWorkflowFileInput; +import stirling.software.proprietary.model.api.ai.AiWorkflowFileRequest; +import stirling.software.proprietary.model.api.ai.AiWorkflowOutcome; +import stirling.software.proprietary.model.api.ai.AiWorkflowPhase; +import stirling.software.proprietary.model.api.ai.AiWorkflowProgressEvent; +import stirling.software.proprietary.model.api.ai.AiWorkflowRequest; +import stirling.software.proprietary.model.api.ai.AiWorkflowResponse; +import stirling.software.proprietary.model.api.ai.AiWorkflowResultFile; +import stirling.software.proprietary.service.PdfContentExtractor.LoadedFile; +import stirling.software.proprietary.service.PdfContentExtractor.PdfContentResult; +import stirling.software.proprietary.service.PdfContentExtractor.WorkflowArtifact; + +import tools.jackson.databind.ObjectMapper; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AiWorkflowService { + + private final CustomPDFDocumentFactory pdfDocumentFactory; + private final AiEngineClient aiEngineClient; + private final PdfContentExtractor pdfContentExtractor; + private final ObjectMapper objectMapper; + private final InternalApiClient internalApiClient; + private final FileStorage fileStorage; + private final ToolMetadataService toolMetadataService; + private final TempFileManager tempFileManager; + + @FunctionalInterface + public interface ProgressListener { + void onProgress(AiWorkflowProgressEvent event); + } + + private static final ProgressListener NOOP_LISTENER = event -> {}; + + private sealed interface WorkflowState { + record Pending(WorkflowTurnRequest request) implements WorkflowState {} + + record Terminal(AiWorkflowResponse response) implements WorkflowState {} + } + + public AiWorkflowResponse orchestrate(AiWorkflowRequest request) throws IOException { + return orchestrate(request, NOOP_LISTENER); + } + + public AiWorkflowResponse orchestrate(AiWorkflowRequest request, ProgressListener listener) + throws IOException { + validateRequest(request); + + Map filesByName = new LinkedHashMap<>(); + for (AiWorkflowFileInput fileInput : request.getFileInputs()) { + filesByName.put( + fileInput.getFileInput().getOriginalFilename(), fileInput.getFileInput()); + } + + WorkflowTurnRequest initialRequest = new WorkflowTurnRequest(); + initialRequest.setUserMessage(request.getUserMessage().trim()); + initialRequest.setFileNames(new ArrayList<>(filesByName.keySet())); + initialRequest.setConversationHistory( + request.getConversationHistory() == null + ? new ArrayList<>() + : new ArrayList<>(request.getConversationHistory())); + + listener.onProgress(AiWorkflowProgressEvent.of(AiWorkflowPhase.ANALYZING)); + + WorkflowState state = new WorkflowState.Pending(initialRequest); + while (state instanceof WorkflowState.Pending pending) { + state = advance(pending.request(), filesByName, listener); + } + return ((WorkflowState.Terminal) state).response(); + } + + private WorkflowState advance( + WorkflowTurnRequest request, + Map filesByName, + ProgressListener listener) + throws IOException { + listener.onProgress(AiWorkflowProgressEvent.of(AiWorkflowPhase.CALLING_ENGINE)); + AiWorkflowResponse response = invokeOrchestrator(request); + return switch (response.getOutcome()) { + case NEED_CONTENT -> onNeedContent(response, filesByName, request, listener); + case TOOL_CALL -> onToolCall(response, filesByName, listener); + case PLAN -> onPlan(response, filesByName, listener); + case ANSWER, + NOT_FOUND, + NEED_CLARIFICATION, + CANNOT_DO, + DRAFT, + COMPLETED, + UNSUPPORTED_CAPABILITY, + CANNOT_CONTINUE -> + new WorkflowState.Terminal(response); + }; + } + + private WorkflowState onNeedContent( + AiWorkflowResponse response, + Map filesByName, + WorkflowTurnRequest request, + ProgressListener listener) + throws IOException { + if (!request.getArtifacts().isEmpty()) { + return new WorkflowState.Terminal( + cannotContinue("AI engine requested content extraction more than once.")); + } + + List requestedFiles = response.getFiles(); + + // Validate requested file names before loading anything + if (requestedFiles != null && !requestedFiles.isEmpty()) { + for (AiWorkflowFileRequest fileReq : requestedFiles) { + if (!filesByName.containsKey(fileReq.getFileName())) { + return new WorkflowState.Terminal( + cannotContinue( + "AI engine requested unknown file: " + fileReq.getFileName())); + } + } + } + + List fileNamesToLoad = + (requestedFiles == null || requestedFiles.isEmpty()) + ? new ArrayList<>(filesByName.keySet()) + : requestedFiles.stream().map(AiWorkflowFileRequest::getFileName).toList(); + + Map requestedByName = + requestedFiles == null || requestedFiles.isEmpty() + ? Map.of() + : requestedFiles.stream() + .collect( + Collectors.toMap( + AiWorkflowFileRequest::getFileName, r -> r)); + + listener.onProgress(AiWorkflowProgressEvent.of(AiWorkflowPhase.EXTRACTING_CONTENT)); + + List loadedFiles = new ArrayList<>(); + try { + for (String fileName : fileNamesToLoad) { + PDDocument doc = pdfDocumentFactory.load(filesByName.get(fileName), true); + loadedFiles.add(new LoadedFile(fileName, doc)); + } + + List contentResults = + pdfContentExtractor.extractContent( + loadedFiles, + requestedByName, + response.getMaxPages(), + response.getMaxCharacters()); + + listener.onProgress(AiWorkflowProgressEvent.of(AiWorkflowPhase.PROCESSING)); + + WorkflowTurnRequest nextRequest = new WorkflowTurnRequest(); + nextRequest.setUserMessage(request.getUserMessage()); + nextRequest.setFileNames(request.getFileNames()); + nextRequest.setConversationHistory(request.getConversationHistory()); + nextRequest.setArtifacts(pdfContentExtractor.buildArtifacts(contentResults)); + nextRequest.setResumeWith(response.getResumeWith()); + return new WorkflowState.Pending(nextRequest); + } finally { + for (LoadedFile lf : loadedFiles) { + try { + lf.document().close(); + } catch (IOException e) { + log.warn("Failed to close PDF document: {}", lf.fileName(), e); + } + } + } + } + + @SuppressWarnings("unchecked") + private WorkflowState onToolCall( + AiWorkflowResponse response, + Map filesByName, + ProgressListener listener) { + String endpointPath = response.getTool(); + Map parameters = response.getParameters(); + if (endpointPath == null || endpointPath.isBlank()) { + return new WorkflowState.Terminal( + cannotContinue("AI engine returned tool_call without a tool endpoint.")); + } + if (parameters == null) { + parameters = Map.of(); + } + + try { + List inputFiles = toResources(filesByName); + listener.onProgress(AiWorkflowProgressEvent.executingTool(endpointPath, 1, 1)); + List results = executeStep(endpointPath, parameters, inputFiles); + return new WorkflowState.Terminal( + buildCompletedResponse( + response.getRationale(), + results, + new ArrayList<>(filesByName.keySet()))); + } catch (Exception e) { + log.error("Failed to execute tool {}: {}", endpointPath, e.getMessage(), e); + return new WorkflowState.Terminal( + cannotContinue("Tool execution failed: " + e.getMessage())); + } + } + + @SuppressWarnings("unchecked") + private WorkflowState onPlan( + AiWorkflowResponse response, + Map filesByName, + ProgressListener listener) { + List> steps = response.getSteps(); + if (steps == null || steps.isEmpty()) { + return new WorkflowState.Terminal( + cannotContinue("AI engine returned a plan with no steps.")); + } + + try { + List currentFiles = toResources(filesByName); + + for (int i = 0; i < steps.size(); i++) { + Map step = steps.get(i); + String endpointPath = (String) step.get("tool"); + Map parameters = + step.containsKey("parameters") + ? (Map) step.get("parameters") + : Map.of(); + + if (endpointPath == null || endpointPath.isBlank()) { + return new WorkflowState.Terminal( + cannotContinue("Plan step " + (i + 1) + " has no tool endpoint.")); + } + + listener.onProgress( + AiWorkflowProgressEvent.executingTool(endpointPath, i + 1, steps.size())); + currentFiles = executeStep(endpointPath, parameters, currentFiles); + } + + return new WorkflowState.Terminal( + buildCompletedResponse( + response.getSummary(), + currentFiles, + new ArrayList<>(filesByName.keySet()))); + } catch (Exception e) { + log.error("Failed to execute plan: {}", e.getMessage(), e); + return new WorkflowState.Terminal( + cannotContinue("Plan execution failed: " + e.getMessage())); + } + } + + /** + * Execute a single tool step. If the endpoint accepts multiple files, all files are sent in one + * call. Otherwise, the endpoint is called once per file. ZIP responses are unpacked so each + * inner file is treated as its own result (e.g. split outputs a ZIP of pages). + */ + private List executeStep( + String endpointPath, Map parameters, List inputFiles) + throws IOException { + List results = new ArrayList<>(); + if (toolMetadataService.isMultiInput(endpointPath)) { + results.addAll(callEndpoint(endpointPath, parameters, inputFiles)); + } else { + for (Resource file : inputFiles) { + results.addAll(callEndpoint(endpointPath, parameters, List.of(file))); + } + } + return results; + } + + /** + * Call an endpoint and return the response body. Endpoints that are declared as ZIP-returning + * in the API spec (multi-output, or {@code Output:ZIP-*}) are unpacked into their individual + * entries so callers always see a flat list of result files. + */ + private List callEndpoint( + String endpointPath, Map parameters, List files) + throws IOException { + MultiValueMap body = new LinkedMultiValueMap<>(); + for (Resource file : files) { + body.add("fileInput", file); + } + for (Map.Entry entry : parameters.entrySet()) { + if (entry.getValue() instanceof List list) { + for (Object item : list) { + body.add(entry.getKey(), item); + } + } else { + body.add(entry.getKey(), entry.getValue()); + } + } + ResponseEntity response = internalApiClient.post(endpointPath, body); + if (!HttpStatus.OK.equals(response.getStatusCode()) || response.getBody() == null) { + throw new IOException( + "Tool returned HTTP " + response.getStatusCode() + " for " + endpointPath); + } + Resource resource = response.getBody(); + if (toolMetadataService.shouldUnpackZipResponse(endpointPath)) { + return ZipExtractionUtils.extractZip(resource, tempFileManager); + } + return List.of(resource); + } + + private List toResources(Map filesByName) throws IOException { + List resources = new ArrayList<>(); + for (MultipartFile file : filesByName.values()) { + TempFile tempFile = tempFileManager.createManagedTempFile("ai-workflow"); + file.transferTo(tempFile.getPath()); + final String originalName = Filenames.toSimpleFileName(file.getOriginalFilename()); + resources.add( + new FileSystemResource(tempFile.getFile()) { + @Override + public String getFilename() { + return originalName; + } + }); + } + return resources; + } + + private AiWorkflowResponse buildCompletedResponse( + String summary, List resultFiles, List inputFileNames) + throws IOException { + // Store every output file individually so each gets its own Stirling file ID and the + // frontend can add them as independent variants without going through a zip. + boolean preserveInputNames = inputFileNames.size() == resultFiles.size(); + List descriptors = new ArrayList<>(); + for (int i = 0; i < resultFiles.size(); i++) { + Resource resource = resultFiles.get(i); + String responseName = resource.getFilename(); + String inputName = preserveInputNames ? inputFileNames.get(i) : null; + // Prefer the input name only for 1:1 operations where the output keeps the same + // extension (rotate, compress, etc.). For converters and other extension-changing + // tools, the response filename from Content-Disposition is authoritative. + String name; + if (inputName != null + && FilenameUtils.getExtension(inputName) + .equalsIgnoreCase(FilenameUtils.getExtension(responseName))) { + name = inputName; + } else if (responseName != null) { + name = responseName; + } else { + name = "result-" + (i + 1); + } + String contentType = + MediaTypeFactory.getMediaType(name) + .orElse(MediaType.APPLICATION_OCTET_STREAM) + .toString(); + String fileId; + try (java.io.InputStream is = resource.getInputStream()) { + fileId = fileStorage.storeInputStream(is, name).fileId(); + } + descriptors.add(new AiWorkflowResultFile(fileId, name, contentType)); + } + + AiWorkflowResponse completed = new AiWorkflowResponse(); + completed.setOutcome(AiWorkflowOutcome.COMPLETED); + completed.setSummary(summary); + completed.setResultFiles(descriptors); + // Mirror the first file into the legacy single-file fields so existing clients still work. + if (!descriptors.isEmpty()) { + AiWorkflowResultFile first = descriptors.getFirst(); + completed.setFileId(first.getFileId()); + completed.setFileName(first.getFileName()); + completed.setContentType(first.getContentType()); + } + return completed; + } + + private void validateRequest(AiWorkflowRequest request) { + for (AiWorkflowFileInput fileInput : request.getFileInputs()) { + if (fileInput.getFileInput().isEmpty()) { + throw ExceptionUtils.createFileNullOrEmptyException(); + } + } + } + + private AiWorkflowResponse cannotContinue(String reason) { + AiWorkflowResponse response = new AiWorkflowResponse(); + response.setOutcome(AiWorkflowOutcome.CANNOT_CONTINUE); + response.setReason(reason); + return response; + } + + private AiWorkflowResponse invokeOrchestrator(WorkflowTurnRequest request) throws IOException { + String requestBody = objectMapper.writeValueAsString(request); + String responseBody = aiEngineClient.post("/api/v1/orchestrator", requestBody); + return objectMapper.readValue(responseBody, AiWorkflowResponse.class); + } + + @Data + private static class WorkflowTurnRequest { + private String userMessage; + private List fileNames = new ArrayList<>(); + private List conversationHistory = new ArrayList<>(); + private List artifacts = new ArrayList<>(); + private String resumeWith; + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/MathAuditorOrchestrator.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/MathAuditorOrchestrator.java new file mode 100644 index 0000000000..92c595b43a --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/MathAuditorOrchestrator.java @@ -0,0 +1,226 @@ +package stirling.software.proprietary.service; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.proprietary.model.api.ai.Evidence; +import stirling.software.proprietary.model.api.ai.Folio; +import stirling.software.proprietary.model.api.ai.FolioManifest; +import stirling.software.proprietary.model.api.ai.FolioType; +import stirling.software.proprietary.model.api.ai.Requisition; +import stirling.software.proprietary.model.api.ai.Verdict; + +import tools.jackson.databind.ObjectMapper; + +/** + * Orchestrator for the Math Auditor Agent (mathAuditorAgent). + * + *

Manages a four-step Java-Python protocol: + * + *

    + *
  1. Classify all pages cheaply with PDFBox (no OCR or Tabula yet). + *
  2. Send the {@link FolioManifest} to the Python Examiner; receive a {@link Requisition}. + *
  3. Fulfil the Requisition (text / tables / OCR) for only the requested pages. + *
  4. Send the {@link Evidence} to the Python Auditor; receive a {@link Verdict}. + *
+ * + *

The raw PDF never leaves Java. Python only receives structured text and CSV data. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class MathAuditorOrchestrator { + + private static final String EXAMINE_PATH = "/api/v1/ai/math-auditor-agent/examine"; + private static final String DELIBERATE_PATH = "/api/v1/ai/math-auditor-agent/deliberate"; + + private final AiEngineClient aiEngineClient; + private final CustomPDFDocumentFactory pdfDocumentFactory; + private final PdfContentExtractor pdfContentExtractor; + private final ObjectMapper objectMapper; + + /** + * Run a full math audit against the supplied PDF file. + * + * @param pdfFile The uploaded PDF to audit. + * @param tolerance Arithmetic tolerance — differences smaller than this are ignored. + * @return The Auditor's final Verdict. + */ + public Verdict audit(MultipartFile pdfFile, BigDecimal tolerance) throws IOException { + String sessionId = UUID.randomUUID().toString(); + log.info( + "[math-auditor-agent] audit started session={} file={} tolerance={}", + sessionId, + pdfFile.getOriginalFilename(), + tolerance); + + try (PDDocument document = pdfDocumentFactory.load(pdfFile)) { + // Round 1: classify pages cheaply; send manifest; get requisition + List folioTypes = classifyPages(document); + FolioManifest manifest = + new FolioManifest(sessionId, document.getNumberOfPages(), folioTypes, 1); + + Requisition requisition = callExamine(manifest); + log.info( + "[math-auditor-agent] session={} requisition received: {}", + sessionId, + requisition.rationale()); + + // Round 2: fulfil the requisition and get verdict + Evidence evidence = fulfil(document, sessionId, requisition, 2, true); + Verdict verdict = callDeliberate(evidence, tolerance); + + if (verdict == null) { + log.error( + "[math-auditor-agent] session={} null Verdict from deliberate", sessionId); + throw new IllegalStateException("Math Auditor Agent returned null Verdict"); + } + + log.info( + "[math-auditor-agent] session={} verdict: {} errors, {} warnings," + + " clean={}", + sessionId, + verdict.errorCount(), + verdict.warningCount(), + verdict.clean()); + return verdict; + } + } + + // ----------------------------------------------------------------------- + // Python engine calls + // ----------------------------------------------------------------------- + + private Requisition callExamine(FolioManifest manifest) throws IOException { + String requestBody = objectMapper.writeValueAsString(manifest); + log.info( + "[math-auditor-agent] POST {} session={} round={}", + EXAMINE_PATH, + manifest.sessionId(), + manifest.round()); + String responseBody = aiEngineClient.post(EXAMINE_PATH, requestBody); + return objectMapper.readValue(responseBody, Requisition.class); + } + + private Verdict callDeliberate(Evidence evidence, BigDecimal tolerance) throws IOException { + String path = DELIBERATE_PATH + "?tolerance=" + tolerance.toPlainString(); + String requestBody = objectMapper.writeValueAsString(evidence); + log.info( + "[math-auditor-agent] POST {} session={} round={} final={}", + path, + evidence.sessionId(), + evidence.round(), + evidence.finalRound()); + String responseBody = aiEngineClient.post(path, requestBody); + return objectMapper.readValue(responseBody, Verdict.class); + } + + // ----------------------------------------------------------------------- + // Page classification + // ----------------------------------------------------------------------- + + private List classifyPages(PDDocument document) throws IOException { + List types = new ArrayList<>(); + for (int page = 1; page <= document.getNumberOfPages(); page++) { + types.add(pdfContentExtractor.classifyPage(document, page)); + } + return types; + } + + // ----------------------------------------------------------------------- + // Requisition fulfilment + // ----------------------------------------------------------------------- + + private Evidence fulfil( + PDDocument document, + String sessionId, + Requisition requisition, + int round, + boolean finalRound) + throws IOException { + + List allPages = + union(requisition.needText(), requisition.needTables(), requisition.needOcr()); + int totalPages = document.getNumberOfPages(); + allPages.removeIf(page -> page < 0 || page >= totalPages); + if (allPages.isEmpty()) { + log.warn( + "[math-auditor-agent] session={} all requested pages are out of bounds", + sessionId); + } + List folios = new ArrayList<>(); + List unauditablePages = new ArrayList<>(); + + for (int page : allPages) { + // Page indices from Python are 0-based; PdfContentExtractor uses 1-based + int pageNumber = page + 1; + String text = null; + List tables = null; + String ocrText = null; + + if (contains(requisition.needText(), page)) { + text = pdfContentExtractor.extractPageTextRaw(document, pageNumber); + } + if (contains(requisition.needTables(), page)) { + tables = pdfContentExtractor.extractTablesAsCsv(document, pageNumber); + } + if (contains(requisition.needOcr(), page)) { + log.warn( + "[math-auditor-agent] session={} OCR requested for page {} but not yet" + + " wired - marking unauditable", + sessionId, + page); + unauditablePages.add(page); + } + + if (text != null || tables != null) { + folios.add(new Folio(page, text, tables, ocrText, null)); + } + } + + log.info( + "[math-auditor-agent] session={} fulfilled round {} with {} folios, {}" + + " unauditable pages", + sessionId, + round, + folios.size(), + unauditablePages.size()); + return new Evidence(sessionId, folios, round, finalRound, unauditablePages); + } + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + @SafeVarargs + private static List union(List... lists) { + List result = new ArrayList<>(); + for (List list : lists) { + if (list != null) { + for (int page : list) { + if (!result.contains(page)) { + result.add(page); + } + } + } + } + Collections.sort(result); + return result; + } + + private static boolean contains(List list, int value) { + return list != null && list.contains(value); + } +} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/PdfContentExtractor.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/PdfContentExtractor.java new file mode 100644 index 0000000000..0716b2ca88 --- /dev/null +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/PdfContentExtractor.java @@ -0,0 +1,367 @@ +package stirling.software.proprietary.service; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.QuoteMode; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.text.PDFTextStripper; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonValue; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.PdfUtils; +import stirling.software.proprietary.model.api.ai.AiPdfContentType; +import stirling.software.proprietary.model.api.ai.AiWorkflowFileRequest; +import stirling.software.proprietary.model.api.ai.AiWorkflowTextSelection; +import stirling.software.proprietary.model.api.ai.FolioType; +import stirling.software.proprietary.pdf.FlexibleCSVWriter; + +import technology.tabula.ObjectExtractor; +import technology.tabula.Page; +import technology.tabula.Table; +import technology.tabula.extractors.SpreadsheetExtractionAlgorithm; + +@Slf4j +@Service +public class PdfContentExtractor { + + private static final int MAX_CHARACTERS_PER_PAGE = 4_000; + + private static final int TEXT_PRESENCE_THRESHOLD = 20; + + record LoadedFile(String fileName, PDDocument document) {} + + // ----------------------------------------------------------------------- + // Low-level extraction methods (usable by any agent) + // ----------------------------------------------------------------------- + + /** + * Classify a single page as TEXT, IMAGE, or MIXED. + * + * @param document the open PDF + * @param pageNumber 1-based page number + */ + public FolioType classifyPage(PDDocument document, int pageNumber) throws IOException { + PDFTextStripper stripper = new PDFTextStripper(); + stripper.setStartPage(pageNumber); + stripper.setEndPage(pageNumber); + String text = stripper.getText(document).trim(); + + boolean hasText = text.length() > TEXT_PRESENCE_THRESHOLD; + boolean hasImages = PdfUtils.hasImagesOnPage(document.getPage(pageNumber - 1)); + + if (hasText && hasImages) { + return FolioType.MIXED; + } else if (hasText) { + return FolioType.TEXT; + } else { + return FolioType.IMAGE; + } + } + + /** + * Extract plain text from a single page, clipped to {@link #MAX_CHARACTERS_PER_PAGE}. + * + * @param document the open PDF + * @param pageNumber 1-based page number + * @return trimmed text, or empty string if the page has no extractable text + */ + public String extractPageTextRaw(PDDocument document, int pageNumber) throws IOException { + PDFTextStripper stripper = new PDFTextStripper(); + stripper.setStartPage(pageNumber); + stripper.setEndPage(pageNumber); + String text = stripper.getText(document).trim(); + return clip(text, MAX_CHARACTERS_PER_PAGE); + } + + /** + * Extract all tables from a single page as CSV strings. + * + * @param document the open PDF + * @param pageNumber 1-based page number + * @return list of CSV strings (one per table), empty if no tables found + */ + public List extractTablesAsCsv(PDDocument document, int pageNumber) throws IOException { + SpreadsheetExtractionAlgorithm sea = new SpreadsheetExtractionAlgorithm(); + CSVFormat format = + CSVFormat.EXCEL.builder().setEscape('"').setQuoteMode(QuoteMode.ALL).build(); + List csvStrings = new ArrayList<>(); + + try (ObjectExtractor extractor = new ObjectExtractor(document)) { + Page tabulaPage = extractor.extract(pageNumber); + List tables = sea.extract(tabulaPage); + + for (Table table : tables) { + StringWriter sw = new StringWriter(); + FlexibleCSVWriter csvWriter = new FlexibleCSVWriter(format); + csvWriter.write(sw, Collections.singletonList(table)); + csvStrings.add(sw.toString()); + } + } + return csvStrings; + } + + // ----------------------------------------------------------------------- + // Workflow extraction (used by AiWorkflowService) + // ----------------------------------------------------------------------- + + /** + * Extracts content from the loaded files according to the requested content types and budget + * constraints. + */ + List extractContent( + List loadedFiles, + Map requestedByName, + int maxPages, + int maxCharacters) + throws IOException { + List contentResults = new ArrayList<>(); + int remainingPages = maxPages; + int remainingCharacters = maxCharacters; + + for (LoadedFile lf : loadedFiles) { + if (remainingPages <= 0 || remainingCharacters <= 0) break; + AiWorkflowFileRequest fileReq = requestedByName.get(lf.fileName()); + List contentTypes = + fileReq != null && !fileReq.getContentTypes().isEmpty() + ? fileReq.getContentTypes() + : List.of(AiPdfContentType.PAGE_TEXT); + + for (AiPdfContentType contentType : contentTypes) { + Optional result = + dispatchContentType( + contentType, lf, fileReq, remainingPages, remainingCharacters); + if (result.isPresent()) { + PdfContentResult content = result.get(); + contentResults.add(content); + remainingPages -= content.pagesConsumed(); + remainingCharacters -= content.charactersConsumed(); + } + } + } + return contentResults; + } + + /** Groups content results by artifact kind and builds the corresponding workflow artifacts. */ + List buildArtifacts(List results) { + List artifacts = new ArrayList<>(); + Map> byKind = + results.stream().collect(Collectors.groupingBy(PdfContentResult::getArtifactKind)); + for (var entry : byKind.entrySet()) { + artifacts.add(buildArtifact(entry.getKey(), entry.getValue())); + } + return artifacts; + } + + private Optional dispatchContentType( + AiPdfContentType contentType, + LoadedFile lf, + AiWorkflowFileRequest fileReq, + int remainingPages, + int remainingCharacters) + throws IOException { + return switch (contentType) { + case PAGE_TEXT, FULL_TEXT -> + Optional.ofNullable( + extractText(lf, fileReq, remainingPages, remainingCharacters)); + default -> { + log.warn( + "Content type {} not yet implemented, skipping for {}", + contentType, + lf.fileName()); + yield Optional.empty(); + } + }; + } + + private ExtractedFileText extractText( + LoadedFile lf, + AiWorkflowFileRequest fileReq, + int remainingPages, + int remainingCharacters) + throws IOException { + List requestedPages = fileReq != null ? fileReq.getPageNumbers() : null; + List pages = + selectPages(lf.document().getNumberOfPages(), requestedPages, remainingPages); + List extracted = + extractPageText(lf.document(), pages, remainingCharacters); + return extracted.isEmpty() ? null : buildExtractedFileText(lf.fileName(), extracted); + } + + private WorkflowArtifact buildArtifact(ArtifactKind kind, List results) { + return switch (kind) { + case EXTRACTED_TEXT -> { + ExtractedTextArtifact artifact = new ExtractedTextArtifact(); + artifact.setFiles(results.stream().map(ExtractedFileText.class::cast).toList()); + yield artifact; + } + }; + } + + private List selectPages( + int totalPages, List requestedPageNumbers, int maxPages) { + if (totalPages <= 0) { + throw ExceptionUtils.createPdfNoPages(); + } + + List pages = new ArrayList<>(); + + if (requestedPageNumbers == null || requestedPageNumbers.isEmpty()) { + for (int p = 1; p <= totalPages && pages.size() < maxPages; p++) { + pages.add(p); + } + return pages; + } + + Set deduplicatedPages = new LinkedHashSet<>(requestedPageNumbers); + for (Integer pageNumber : deduplicatedPages) { + if (pageNumber == null || pageNumber < 1 || pageNumber > totalPages) { + throw ExceptionUtils.createIllegalArgumentException( + "error.invalidPageNumber", + "Requested page number %s is outside the PDF page range.", + pageNumber); + } + pages.add(pageNumber); + if (pages.size() >= maxPages) { + break; + } + } + return pages; + } + + private List extractPageText( + PDDocument document, List selectedPages, int maxCharacters) + throws IOException { + PDFTextStripper textStripper = new PDFTextStripper(); + List pages = new ArrayList<>(); + int remainingCharacters = maxCharacters; + + for (Integer pageNumber : selectedPages) { + if (remainingCharacters <= 0) { + break; + } + + textStripper.setStartPage(pageNumber); + textStripper.setEndPage(pageNumber); + + String pageText = textStripper.getText(document).trim(); + if (pageText.isBlank()) { + continue; + } + + int allowedCharacters = Math.min(remainingCharacters, MAX_CHARACTERS_PER_PAGE); + String clippedText = clip(pageText, allowedCharacters); + if (clippedText.isBlank()) { + continue; + } + + AiWorkflowTextSelection selection = new AiWorkflowTextSelection(); + selection.setPageNumber(pageNumber); + selection.setText(clippedText); + pages.add(selection); + remainingCharacters -= clippedText.length(); + } + return pages; + } + + private ExtractedFileText buildExtractedFileText( + String fileName, List pages) { + ExtractedFileText fileText = new ExtractedFileText(); + fileText.setFileName(fileName); + fileText.setPages(pages); + return fileText; + } + + private String clip(String text, int maxLength) { + if (text.length() <= maxLength) { + return text; + } + // Avoid splitting a surrogate pair at the boundary + int end = maxLength; + if (Character.isHighSurrogate(text.charAt(end - 1))) { + end--; + } + return text.substring(0, end); + } + + // --- Types shared with AiWorkflowService (package-private) --- + + interface PdfContentResult { + @JsonIgnore + ArtifactKind getArtifactKind(); + + @JsonIgnore + default int pagesConsumed() { + return 0; + } + + @JsonIgnore + default int charactersConsumed() { + return 0; + } + } + + /** + * Values MUST match {@code ArtifactKind} in {@code engine/src/stirling/contracts/common.py}. + */ + enum ArtifactKind { + EXTRACTED_TEXT("extracted_text"); + + private final String value; + + ArtifactKind(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + } + + interface WorkflowArtifact { + ArtifactKind getKind(); + } + + @Data + static class ExtractedFileText implements PdfContentResult { + private String fileName; + private List pages = new ArrayList<>(); + + @Override + public ArtifactKind getArtifactKind() { + return ArtifactKind.EXTRACTED_TEXT; + } + + @Override + public int pagesConsumed() { + return pages.size(); + } + + @Override + public int charactersConsumed() { + return pages.stream().mapToInt(p -> p.getText().length()).sum(); + } + } + + @Data + static final class ExtractedTextArtifact implements WorkflowArtifact { + private final ArtifactKind kind = ArtifactKind.EXTRACTED_TEXT; + private List files = new ArrayList<>(); + } +} 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 8429135204..6027721a28 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 @@ -22,6 +22,8 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; +import io.swagger.v3.oas.annotations.tags.Tag; + import lombok.RequiredArgsConstructor; import stirling.software.proprietary.security.model.User; @@ -38,6 +40,9 @@ import stirling.software.proprietary.storage.service.FileStorageService; @RestController @RequestMapping("/api/v1/storage") @RequiredArgsConstructor +@Tag( + name = "File Storage", + description = "Stored file management, sharing, and share link operations") public class FileStorageController { private final FileStorageService fileStorageService; 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 3269388868..756fba1fe0 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 @@ -47,7 +47,9 @@ import stirling.software.proprietary.workflow.service.WorkflowSessionService; @Slf4j @RestController @RequestMapping("/api/v1/security") -@Tag(name = "Security", description = "Security APIs - Signing Workflows") +@Tag( + name = "Signing Sessions", + description = "Signing session lifecycle and participant management") @RequiredArgsConstructor public class SigningSessionController { diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/AiWorkflowServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/AiWorkflowServiceTest.java new file mode 100644 index 0000000000..2872e0db91 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/service/AiWorkflowServiceTest.java @@ -0,0 +1,335 @@ +package stirling.software.proprietary.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.junit.jupiter.api.BeforeEach; +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.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.util.MultiValueMap; + +import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.service.FileStorage; +import stirling.software.common.service.FileStorage.StoredFile; +import stirling.software.common.service.InternalApiClient; +import stirling.software.common.service.ToolMetadataService; +import stirling.software.common.util.TempFileManager; +import stirling.software.common.util.TempFileRegistry; +import stirling.software.proprietary.model.api.ai.AiWorkflowFileInput; +import stirling.software.proprietary.model.api.ai.AiWorkflowOutcome; +import stirling.software.proprietary.model.api.ai.AiWorkflowRequest; +import stirling.software.proprietary.model.api.ai.AiWorkflowResponse; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +/** + * Smoke tests for {@link AiWorkflowService}. Covers the TOOL_CALL and PLAN execution paths, + * ZIP-response unpacking (split endpoints), multi-input dispatch (merge endpoints), and the 1:1 + * input-to-output filename preservation rule. + * + *

External collaborators (engine client, internal API client, tool metadata, file storage) are + * mocked. {@link TempFileManager} is constructed with a real in-test registry so the service's + * temp-file handling exercises real code. + */ +@ExtendWith(MockitoExtension.class) +class AiWorkflowServiceTest { + + private static final String ROTATE_ENDPOINT = "/api/v1/general/rotate-pdf"; + private static final String SPLIT_ENDPOINT = "/api/v1/general/split-pages"; + private static final String MERGE_ENDPOINT = "/api/v1/general/merge-pdfs"; + private static final String COMPRESS_ENDPOINT = "/api/v1/misc/compress-pdf"; + + @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private AiEngineClient aiEngineClient; + @Mock private PdfContentExtractor pdfContentExtractor; + @Mock private InternalApiClient internalApiClient; + @Mock private FileStorage fileStorage; + @Mock private ToolMetadataService toolMetadataService; + + @TempDir Path tempDir; + + private TempFileManager tempFileManager; + private ObjectMapper objectMapper; + private AiWorkflowService service; + + @BeforeEach + void setUp() { + ApplicationProperties props = new ApplicationProperties(); + props.getSystem().getTempFileManagement().setBaseTmpDir(tempDir.toString()); + props.getSystem().getTempFileManagement().setPrefix("ai-test-"); + tempFileManager = new TempFileManager(new TempFileRegistry(), props); + objectMapper = JsonMapper.builder().build(); + + service = + new AiWorkflowService( + pdfDocumentFactory, + aiEngineClient, + pdfContentExtractor, + objectMapper, + internalApiClient, + fileStorage, + toolMetadataService, + tempFileManager); + } + + @Test + void toolCallSingleFilePreservesInputFilename() throws IOException { + MockMultipartFile input = pdf("input.pdf", "original-pdf-bytes"); + stubOrchestrator( + """ + {"outcome":"tool_call","tool":"%s","parameters":{"angle":90},"rationale":"Rotating"} + """ + .formatted(ROTATE_ENDPOINT)); + when(toolMetadataService.isMultiInput(ROTATE_ENDPOINT)).thenReturn(false); + when(toolMetadataService.shouldUnpackZipResponse(ROTATE_ENDPOINT)).thenReturn(false); + stubEndpoint(ROTATE_ENDPOINT, pdfResource("rotated-bytes", "rotated.pdf")); + AtomicInteger ids = stubFileStorage(); + + AiWorkflowResponse result = service.orchestrate(requestFor(input, "rotate 90")); + + assertEquals(AiWorkflowOutcome.COMPLETED, result.getOutcome()); + assertEquals(1, result.getResultFiles().size()); + // 1:1 mapping — the single output should inherit the single input's filename. + assertEquals("input.pdf", result.getResultFiles().get(0).getFileName()); + assertEquals("file-1", result.getResultFiles().get(0).getFileId()); + assertEquals(1, ids.get()); + verify(internalApiClient, times(1)).post(eq(ROTATE_ENDPOINT), any()); + } + + @Test + void toolCallZipResponseUnpacksIntoMultipleResults() throws IOException { + MockMultipartFile input = pdf("doc.pdf", "original"); + stubOrchestrator( + """ + {"outcome":"tool_call","tool":"%s","parameters":{},"rationale":"Splitting"} + """ + .formatted(SPLIT_ENDPOINT)); + when(toolMetadataService.isMultiInput(SPLIT_ENDPOINT)).thenReturn(false); + when(toolMetadataService.shouldUnpackZipResponse(SPLIT_ENDPOINT)).thenReturn(true); + stubEndpoint( + SPLIT_ENDPOINT, + zipResource( + "doc.zip", + List.of( + new ZipEntryBytes("page-1.pdf", "page-one"), + new ZipEntryBytes("page-2.pdf", "page-two"), + new ZipEntryBytes("page-3.pdf", "page-three")))); + stubFileStorage(); + + AiWorkflowResponse result = service.orchestrate(requestFor(input, "split")); + + assertEquals(AiWorkflowOutcome.COMPLETED, result.getOutcome()); + assertEquals(3, result.getResultFiles().size()); + // Input count (1) != output count (3) so the per-entry filename is kept. + assertEquals("page-1.pdf", result.getResultFiles().get(0).getFileName()); + assertEquals("page-2.pdf", result.getResultFiles().get(1).getFileName()); + assertEquals("page-3.pdf", result.getResultFiles().get(2).getFileName()); + } + + @Test + void multiInputEndpointIsCalledOnceWithAllFiles() throws IOException { + MockMultipartFile a = pdf("a.pdf", "a-bytes"); + MockMultipartFile b = pdf("b.pdf", "b-bytes"); + stubOrchestrator( + """ + {"outcome":"tool_call","tool":"%s","parameters":{},"rationale":"Merging"} + """ + .formatted(MERGE_ENDPOINT)); + when(toolMetadataService.isMultiInput(MERGE_ENDPOINT)).thenReturn(true); + when(toolMetadataService.shouldUnpackZipResponse(MERGE_ENDPOINT)).thenReturn(false); + stubEndpoint(MERGE_ENDPOINT, pdfResource("merged-bytes", "merged.pdf")); + stubFileStorage(); + + AiWorkflowResponse result = + service.orchestrate(requestFor(new MockMultipartFile[] {a, b}, "merge these")); + + assertEquals(AiWorkflowOutcome.COMPLETED, result.getOutcome()); + assertEquals(1, result.getResultFiles().size()); + // Two inputs but only one output → filename is not preserved from either input. + assertEquals("merged.pdf", result.getResultFiles().get(0).getFileName()); + verify(internalApiClient, times(1)).post(eq(MERGE_ENDPOINT), any()); + } + + @Test + void singleInputEndpointIsCalledOncePerFile() throws IOException { + MockMultipartFile a = pdf("a.pdf", "a-bytes"); + MockMultipartFile b = pdf("b.pdf", "b-bytes"); + stubOrchestrator( + """ + {"outcome":"tool_call","tool":"%s","parameters":{"angle":90},"rationale":"Rotating"} + """ + .formatted(ROTATE_ENDPOINT)); + when(toolMetadataService.isMultiInput(ROTATE_ENDPOINT)).thenReturn(false); + when(toolMetadataService.shouldUnpackZipResponse(ROTATE_ENDPOINT)).thenReturn(false); + stubEndpoint(ROTATE_ENDPOINT, pdfResource("rotated", "rotated.pdf")); + stubFileStorage(); + + AiWorkflowResponse result = + service.orchestrate(requestFor(new MockMultipartFile[] {a, b}, "rotate both")); + + assertEquals(AiWorkflowOutcome.COMPLETED, result.getOutcome()); + assertEquals(2, result.getResultFiles().size()); + // Per-file loop dispatches one call per input file. + verify(internalApiClient, times(2)).post(eq(ROTATE_ENDPOINT), any()); + // 1:1 mapping preserves each input's filename. + assertEquals("a.pdf", result.getResultFiles().get(0).getFileName()); + assertEquals("b.pdf", result.getResultFiles().get(1).getFileName()); + } + + @Test + void planExecutesStepsSequentially() throws IOException { + MockMultipartFile input = pdf("input.pdf", "bytes"); + stubOrchestrator( + """ + { + "outcome":"plan", + "summary":"Rotate then compress", + "steps":[ + {"tool":"%s","parameters":{"angle":90}}, + {"tool":"%s","parameters":{}} + ] + } + """ + .formatted(ROTATE_ENDPOINT, COMPRESS_ENDPOINT)); + when(toolMetadataService.isMultiInput(anyString())).thenReturn(false); + when(toolMetadataService.shouldUnpackZipResponse(anyString())).thenReturn(false); + stubEndpoint(ROTATE_ENDPOINT, pdfResource("rotated", "rotated.pdf")); + stubEndpoint(COMPRESS_ENDPOINT, pdfResource("compressed", "compressed.pdf")); + stubFileStorage(); + + AiWorkflowResponse result = service.orchestrate(requestFor(input, "rotate and compress")); + + assertEquals(AiWorkflowOutcome.COMPLETED, result.getOutcome()); + assertEquals(1, result.getResultFiles().size()); + // 1:1 input → output mapping at the plan level preserves the input's filename. + assertEquals("input.pdf", result.getResultFiles().get(0).getFileName()); + verify(internalApiClient, times(1)).post(eq(ROTATE_ENDPOINT), any()); + verify(internalApiClient, times(1)).post(eq(COMPRESS_ENDPOINT), any()); + } + + @Test + void toolCallWithoutEndpointFallsBackToCannotContinue() throws IOException { + MockMultipartFile input = pdf("input.pdf", "bytes"); + stubOrchestrator("{\"outcome\":\"tool_call\",\"parameters\":{}}"); + + AiWorkflowResponse result = service.orchestrate(requestFor(input, "do something")); + + assertEquals(AiWorkflowOutcome.CANNOT_CONTINUE, result.getOutcome()); + assertNotNull(result.getReason()); + verify(internalApiClient, never()).post(anyString(), any()); + } + + // --- helpers --- + + private void stubOrchestrator(String responseJson) throws IOException { + when(aiEngineClient.post(eq("/api/v1/orchestrator"), anyString())).thenReturn(responseJson); + } + + private void stubEndpoint(String endpoint, Resource body) { + when(internalApiClient.post(eq(endpoint), any(MultiValueMap.class))) + .thenReturn(ResponseEntity.ok(body)); + } + + /** + * Stub {@link FileStorage#storeInputStream} with sequential file IDs and an accurate byte + * count. Returns the counter so tests can assert how many stores happened. + */ + private AtomicInteger stubFileStorage() throws IOException { + AtomicInteger counter = new AtomicInteger(); + when(fileStorage.storeInputStream(any(InputStream.class), anyString())) + .thenAnswer( + inv -> { + InputStream is = inv.getArgument(0); + long size = is.readAllBytes().length; + return new StoredFile("file-" + counter.incrementAndGet(), size); + }); + return counter; + } + + private static MockMultipartFile pdf(String filename, String content) { + return new MockMultipartFile("fileInput", filename, "application/pdf", content.getBytes()); + } + + private static AiWorkflowRequest requestFor(MockMultipartFile file, String message) { + return requestFor(new MockMultipartFile[] {file}, message); + } + + private static AiWorkflowRequest requestFor(MockMultipartFile[] files, String message) { + AiWorkflowRequest request = new AiWorkflowRequest(); + List inputs = new ArrayList<>(); + for (MockMultipartFile file : files) { + AiWorkflowFileInput fileInput = new AiWorkflowFileInput(); + fileInput.setFileInput(file); + inputs.add(fileInput); + } + request.setFileInputs(inputs); + request.setUserMessage(message); + return request; + } + + private static ByteArrayResource pdfResource(String content, String filename) { + return new ByteArrayResource(content.getBytes()) { + @Override + public String getFilename() { + return filename; + } + }; + } + + private static ByteArrayResource zipResource(String filename, List entries) + throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(baos)) { + for (ZipEntryBytes entry : entries) { + zos.putNextEntry(new ZipEntry(entry.name())); + zos.write(entry.bytes()); + zos.closeEntry(); + } + } + byte[] zipBytes = baos.toByteArray(); + return new ByteArrayResource(zipBytes) { + @Override + public String getFilename() { + return filename; + } + + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(zipBytes); + } + }; + } + + private record ZipEntryBytes(String name, byte[] bytes) { + ZipEntryBytes(String name, String content) { + this(name, content.getBytes()); + } + } +} diff --git a/build.gradle b/build.gradle index b41c54e3b8..0721c37baa 100644 --- a/build.gradle +++ b/build.gradle @@ -2,13 +2,13 @@ plugins { id "java" id "jacoco" id "io.spring.dependency-management" version "1.1.7" - id "org.springframework.boot" version "4.0.3" + id "org.springframework.boot" version "4.0.5" id "org.springdoc.openapi-gradle-plugin" version "1.9.0" id "io.swagger.swaggerhub" version "1.3.2" - id "com.diffplug.spotless" version "8.1.0" - id "com.github.jk1.dependency-license-report" version "3.0.1" + id "com.diffplug.spotless" version "8.4.0" + id "com.github.jk1.dependency-license-report" version "3.1.1" //id "nebula.lint" version "19.0.3" - id "org.sonarqube" version "7.2.2.6593" + id "org.sonarqube" version "7.2.3.7755" } import com.github.jk1.license.render.* @@ -20,17 +20,17 @@ import org.gradle.api.tasks.testing.Test import org.gradle.jvm.toolchain.JavaLanguageVersion ext { - springBootVersion = "4.0.3" + springBootVersion = "4.0.5" pdfboxVersion = "3.0.7" imageioVersion = "3.13.1" - lombokVersion = "1.18.42" + lombokVersion = "1.18.44" bouncycastleVersion = "1.83" - springSecuritySamlVersion = "7.0.2" + springSecuritySamlVersion = "7.0.4" openSamlVersion = "5.2.1" - commonmarkVersion = "0.27.1" - googleJavaFormatVersion = "1.35.0" + commonmarkVersion = "0.28.0" + googleJavaFormatVersion = "1.28.0" logback = "1.5.32" - junitPlatformVersion = "1.12.2" + // junit-platform-launcher version managed by Spring Boot BOM modernJavaVersion = 21 } @@ -78,7 +78,7 @@ springBoot { allprojects { group = 'stirling.software' - version = '2.8.0' + version = '2.9.2' configurations.configureEach { exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat" @@ -110,11 +110,11 @@ tasks.register('syncAppVersion') { [new File(sim1Path), new File(sim2Path)].each { f -> if (f.exists()) { def content = f.getText('UTF-8') - def matcher = (content =~ /(appVersion:\s*')([^']*)(')/) + def matcher = (content =~ /(appVersion:\s*(['"]))(.*?)(\2)/) if (!matcher.find()) { throw new GradleException("Could not locate appVersion in ${f} for synchronization") } - def updatedContent = matcher.replaceFirst("${matcher.group(1)}${appVersionStr}${matcher.group(3)}") + def updatedContent = matcher.replaceFirst("${matcher.group(1)}${appVersionStr}${matcher.group(4)}") if (content != updatedContent) { f.write(updatedContent, 'UTF-8') } @@ -198,6 +198,10 @@ subprojects { imports { mavenBom "org.springframework.boot:spring-boot-dependencies:$springBootVersion" } + dependencies { + // Override BOM-managed commons-lang3 for CVE-2025-48924 fix + dependency 'org.apache.commons:commons-lang3:3.20.0' + } } dependencies { @@ -523,7 +527,7 @@ dependencies { } testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly "org.junit.platform:junit-platform-launcher:$junitPlatformVersion" + testRuntimeOnly "org.junit.platform:junit-platform-launcher" testImplementation platform("com.squareup.okhttp3:okhttp-bom:5.3.2") testImplementation "com.squareup.okhttp3:mockwebserver" diff --git a/devGuide/DeveloperGuide.md b/devGuide/DeveloperGuide.md index 034a475c5f..964992927e 100644 --- a/devGuide/DeveloperGuide.md +++ b/devGuide/DeveloperGuide.md @@ -1,442 +1,5 @@ -# Stirling-PDF Developer Guide +# Developer Guide -## 1. Introduction +This guide has moved to the repository root for easier discovery. -Stirling-PDF is a robust, locally hosted, web-based PDF manipulation tool. This guide focuses on Docker-based development and testing, which is the recommended approach for working with the full version of Stirling-PDF. - -## 2. Project Overview - -Stirling-PDF is built using: - -- Spring Boot -- PDFBox -- LibreOffice -- qpdf -- Calibre (`ebook-convert` CLI) for eBook conversions -- HTML, CSS, JavaScript -- Docker -- PDF.js -- PDF-LIB.js -- Lombok - -## 3. Development Environment Setup - -### Prerequisites - -- Docker -- Git -- Java JDK 21 or later (JDK 25 recommended) -- Gradle 7.0 or later (Included within the repo) - -### Setup Steps - -1. Clone the repository: - - ```bash - git clone https://github.com/Stirling-Tools/Stirling-PDF.git - cd Stirling-PDF - ``` - -2. Install Docker and JDK 21 (or JDK 25 recommended) if not already installed. - -3. Install a recommended Java IDE such as Eclipse, IntelliJ, or VSCode - 1. Only VSCode - 1. Open VS Code. - 2. When prompted, install the recommended extensions. - 3. Alternatively, open the command palette (`Ctrl + Shift + P` or `Cmd + Shift + P` on macOS) and run: - - ```sh - Extensions: Show Recommended Extensions - ``` - - 4. Install the required extensions from the list. - -4. Lombok Setup -Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment: -Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE. - -5. Install Calibre CLI (optional but required for eBook conversions) - Ensure the `ebook-convert` binary from Calibre is available on your PATH when working on the - eBook to PDF feature. The Calibre tool group is automatically disabled when the binary is - missing, so having it installed locally allows you to exercise the full workflow. - -6. Add environment variable -For local testing, you should generally be testing the full 'Security' version of Stirling PDF. To do this, you must add the environment flag DISABLE_ADDITIONAL_FEATURES=false to your system and/or IDE build/run step. - -## 4. Project Structure - -```bash -Stirling-PDF/ -├── .github/ # GitHub-specific files (workflows, issue templates) -├── configs/ # Configuration files used by stirling at runtime (generated at runtime) -├── cucumber/ # Cucumber test files -│ ├── features/ -├── customFiles/ # Custom static files and templates (generated at runtime used to replace existing files) -├── docs/ # Documentation files -├── exampleYmlFiles/ # Example YAML configuration files -├── images/ # Image assets -├── pipeline/ # Pipeline-related files (generated at runtime) -├── scripts/ # Utility scripts -├── src/ # Source code -│ ├── main/ -│ │ ├── java/ -│ │ │ └── stirling/ -│ │ │ └── software/ -│ │ │ └── SPDF/ -│ │ │ ├── config/ -│ │ │ ├── controller/ -│ │ │ ├── model/ -│ │ │ ├── repository/ -│ │ │ ├── service/ -│ │ │ └── utils/ -│ │ └── resources/ -│ │ ├── static/ -│ │ │ ├── css/ -│ │ │ ├── js/ -│ │ │ └── pdfjs/ -│ └── test/ -│ └── java/ -│ └── stirling/ -│ └── software/ -│ └── SPDF/ -├── build.gradle # Gradle build configuration -├── Dockerfile # Main Dockerfile -├── Dockerfile.ultra-lite # Dockerfile for ultra-lite version -├── Dockerfile.fat # Dockerfile for fat version -├── docker-compose.yml # Docker Compose configuration -└── test.sh # Test script to deploy all docker versions and run cuke tests -``` - -## 5. Docker-based Development - -Stirling-PDF offers several Docker versions: - -- Full: All features included -- Ultra-Lite: Basic PDF operations only -- Fat: Includes additional libraries and fonts predownloaded - -### Example Docker Compose Files - -Stirling-PDF provides several example Docker Compose files in the `exampleYmlFiles` directory, such as: - -- `docker-compose-latest.yml`: Latest version without login and security features -- `docker-compose-latest-security.yml`: Latest version with login and security features enabled -- `docker-compose-latest-fat-security.yml`: Fat version with login and security features enabled - -These files provide pre-configured setups for different scenarios. For example, here's a snippet from `docker-compose-latest-security.yml`: - -```yaml -services: - stirling-pdf: - container_name: Stirling-PDF-Security - image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest - deploy: - resources: - limits: - memory: 4G - healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"] - interval: 5s - timeout: 10s - retries: 16 - ports: - - "8080:8080" - volumes: - - ./stirling/latest/data:/usr/share/tessdata:rw - - ./stirling/latest/config:/configs:rw - - ./stirling/latest/logs:/logs:rw - environment: - DISABLE_ADDITIONAL_FEATURES: "false" - SECURITY_ENABLELOGIN: "true" - PUID: 1002 - PGID: 1002 - UMASK: "022" - SYSTEM_DEFAULTLOCALE: en-US - UI_APPNAME: Stirling-PDF - UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security - UI_APPNAMENAVBAR: Stirling-PDF Latest - SYSTEM_MAXFILESIZE: "100" - METRICS_ENABLED: "true" - SYSTEM_GOOGLEVISIBILITY: "true" - SHOW_SURVEY: "true" - restart: on-failure:5 -``` - -To use these example files, copy the desired file to your project root and rename it to `docker-compose.yml`, or specify the file explicitly when running Docker Compose: - -```bash -docker-compose -f exampleYmlFiles/docker-compose-latest-security.yml up -``` - -### Building Docker Images - -Stirling-PDF uses different Docker images for various configurations. The build process is controlled by environment variables and uses specific Dockerfile variants. Here's how to build the Docker images: - -1. Set the security environment variable: - - ```bash - export DISABLE_ADDITIONAL_FEATURES=true # or false for to enable login and security features for builds - ``` - -2. Build the project with Gradle: - - ```bash - ./gradlew clean build - ``` - -3. Build the Docker images: - - For the latest version: - - ```bash - docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile . - ``` - - For the ultra-lite version: - - ```bash - docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite . - ``` - - For the fat version (with login and security features enabled): - - ```bash - export DISABLE_ADDITIONAL_FEATURES=false - docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat . - ``` - -Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase - -## 6. Testing - -### Comprehensive Testing Script - -Stirling-PDF provides a `test.sh` script in the root directory. This script builds all versions of Stirling-PDF, checks that each version works, and runs Cucumber tests. It's recommended to run this script before submitting a final pull request. - -To run the test script: - -```bash -./test.sh -``` - -This script performs the following actions: - -1. Builds all Docker images (full, ultra-lite, fat). -2. Runs each version to ensure it starts correctly. -3. Executes Cucumber tests against the main version and ensures feature compatibility. In the event these tests fail, your PR will not be merged. - -Note: The `test.sh` script will run automatically when you raise a PR. However, it's recommended to run it locally first to save resources and catch any issues early. - -### Full Testing with Docker - -1. Build and run the Docker container per the above instructions: - -2. Access the application at `http://localhost:8080` and manually test all features developed. - -### Local Testing (Java and UI Components) - -For quick iterations and development of Java backend, JavaScript, and UI components, you can run and test Stirling-PDF locally without Docker. This approach allows you to work on and verify changes to: - -- Java backend logic -- RESTful API endpoints -- JavaScript functionality -- User interface components and styling - -To run Stirling-PDF locally: - -1. Compile and run the project using built-in IDE methods or by running: - - ```bash - ./gradlew bootRun - ``` - -2. Access the application at `http://localhost:8080` in your web browser. - -3. Manually test the features you're working on through the UI. - -4. For API changes, use tools like Postman or curl to test endpoints directly. - -Important notes: - -- Local testing doesn't include features that depend on external tools like qpdf, LibreOffice, or Python scripts. -- There are currently no automated unit tests. All testing is done manually through the UI or API calls. (You are welcome to add JUnits!) -- Always verify your changes in the full Docker environment before submitting pull requests, as some integrations and features will only work in the complete setup. - -## 7. Contributing - -1. Fork the repository on GitHub. -2. Create a new branch for your feature or bug fix. -3. Make your changes and commit them with clear, descriptive messages and ensure any documentation is updated related to your changes. -4. Test your changes thoroughly in the Docker environment. -5. Run the `test.sh` script to ensure all versions build correctly and pass the Cucumber tests: - - ```bash - ./test.sh - ``` - -6. Push your changes to your fork. -7. Submit a pull request to the main repository. -8. See additional [contributing guidelines](../CONTRIBUTING.md). - -When you raise a PR: - -- The `test.sh` script will run automatically against your PR. -- The PR checks will verify versioning and dependency updates. -- Documentation will be automatically updated for dependency changes. -- Security issues will be checked using Snyk and PixeeBot. - -Address any issues that arise from these checks before finalizing your pull request. - -## 8. API Documentation - -API documentation is available at `/swagger-ui/index.html` when running the application. You can also view the latest API documentation [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/). - -## 9. Customization - -Stirling-PDF can be customized through environment variables or a `settings.yml` file. Key customization options include: - -- Application name and branding -- Security settings -- UI customization -- Endpoint management -- Maximum DPI for PDF to image conversion (`system.maxDPI`) - -When using Docker, pass environment variables using the `-e` flag or in your `docker-compose.yml` file. - -Example: - -```bash -docker run -p 8080:8080 -e APP_NAME="My PDF Tool" stirling-pdf:full -``` - -Refer to the main README for a full list of customization options. - -## 10. Language Translations - -For managing language translations that affect multiple files, Stirling-PDF provides a helper script: - -```bash -/scripts/replace_translation_line.sh -``` - -This script helps you make consistent replacements across language files. - -When contributing translations: - -1. Use the helper script for multi-file changes. -2. Ensure all language files are updated consistently. -3. The PR checks will verify consistency in language file updates. - -Remember to test your changes thoroughly to ensure they don't break any existing functionality. - -## Code examples - -### Adding a New Feature to the Backend (API) - -1. **Create a New Controller:** - - Create a new Java class in the `app/core/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")`. - - ```java - package stirling.software.SPDF.controller.api; - - import org.springframework.web.bind.annotation.GetMapping; - import org.springframework.web.bind.annotation.RequestMapping; - import org.springframework.web.bind.annotation.RestController; - import io.swagger.v3.oas.annotations.Operation; - import io.swagger.v3.oas.annotations.tags.Tag; - - @RestController - @RequestMapping("/api/v1/new-feature") - @Tag(name = "General", description = "General APIs") - public class NewFeatureController { - - @GetMapping - @Operation(summary = "New Feature", description = "This is a new feature endpoint.") - public String newFeature() { - return "NewFeatureResponse"; - } - } - ``` - -2. **Define the Service Layer:** (Not required but often useful) - - Create a new service class in the `app/core/src/main/java/stirling/software/SPDF/service` directory. - - Implement the business logic for the new feature. - - ```java - package stirling.software.SPDF.service; - - import org.springframework.stereotype.Service; - - @Service - public class NewFeatureService { - - public String getNewFeatureData() { - // Implement business logic here - return "New Feature Data"; - } - } - ``` - -2b. **Integrate the Service with the Controller:** - -- Autowire the service class in the controller and use it to handle the API request. - - ```java - package stirling.software.SPDF.controller.api; - - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.web.bind.annotation.GetMapping; - import org.springframework.web.bind.annotation.RequestMapping; - import org.springframework.web.bind.annotation.RestController; - import stirling.software.SPDF.service.NewFeatureService; - import io.swagger.v3.oas.annotations.Operation; - import io.swagger.v3.oas.annotations.tags.Tag; - - @RestController - @RequestMapping("/api/v1/new-feature") - @Tag(name = "General", description = "General APIs") - public class NewFeatureController { - - @Autowired - private NewFeatureService newFeatureService; - - @GetMapping - @Operation(summary = "New Feature", description = "This is a new feature endpoint.") - public String newFeature() { - return newFeatureService.getNewFeatureData(); - } - } - ``` - -## 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: - -### 1. Locate Existing Language Files - -Find the existing `messages.properties` files in the `app/core/src/main/resources` directory. You'll see files like: - -- `messages.properties` (default, usually English) -- `messages_en_GB.properties` -- `messages_fr_FR.properties` -- `messages_de_DE.properties` -- etc. - -### 2. Add New Translation Entries - -Open each of these files and add your new translation entries. For example, if you're adding a new feature called "PDF Splitter", -Use descriptive, hierarchical keys (e.g., `feature.element.description`) -you might add: - -```properties -pdfSplitter.title=PDF Splitter -pdfSplitter.description=Split your PDF into multiple documents -pdfSplitter.button.split=Split PDF -pdfSplitter.input.pages=Enter page numbers to split -``` - -Add these entries to the default GB language file and any others you wish, translating the values as appropriate for each language. - -Remember, never hard-code text in your templates or Java code. Always use translation keys to ensure proper localization. +**See [DeveloperGuide.md](../DeveloperGuide.md) for the current developer guide.** diff --git a/devGuide/README.md b/devGuide/README.md index f58f6e5d19..5c8f60577b 100644 --- a/devGuide/README.md +++ b/devGuide/README.md @@ -5,7 +5,8 @@ This directory contains all development-related documentation for Stirling PDF. ## 📚 Documentation Index ### Core Development -- **[DeveloperGuide.md](./DeveloperGuide.md)** - Main developer setup and architecture guide +- **[DeveloperGuide.md](../DeveloperGuide.md)** - Main developer setup and architecture guide (in repo root) +- **[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 diff --git a/docker/README.md b/docker/README.md index 99b86b53ea..d0a1535fff 100644 --- a/docker/README.md +++ b/docker/README.md @@ -2,6 +2,23 @@ This directory contains the organized Docker configurations for the split frontend/backend architecture. +## Using Taskfile (Recommended) + +All Docker commands can be run from the project root using [Task](https://taskfile.dev/): + +```bash +task docker:build # Build standard image +task docker:build:fat # Build fat image (all features) +task docker:build:ultra-lite # Build ultra-lite image +task docker:build:frontend # Build frontend-only image +task docker:build:engine # Build engine image +task docker:up # Start standard compose stack +task docker:up:fat # Start fat compose stack +task docker:up:ultra-lite # Start ultra-lite compose stack +task docker:down # Stop all running stacks +task docker:logs # Tail compose logs +``` + ## Directory Structure ``` diff --git a/docker/base/Dockerfile b/docker/base/Dockerfile index dfd64ee705..acb6d5a9f5 100644 --- a/docker/base/Dockerfile +++ b/docker/base/Dockerfile @@ -385,8 +385,10 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ apt-get install -y --no-install-recommends \ # Core tools ca-certificates tzdata tini bash fontconfig curl \ - ffmpeg poppler-utils fontforge \ - gosu unpaper pngquant \ + # ffmpeg disabled due to raised CVEs + # ffmpeg \ + poppler-utils fontforge \ + unpaper pngquant \ # Fonts: full coverage for standard + fat variants fonts-dejavu \ fonts-liberation2 \ @@ -622,8 +624,7 @@ RUN set -eux; \ -d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser 2>/dev/null \ || useradd -m -g stirlingpdfgroup \ -d /home/stirlingpdfuser -s /bin/bash stirlingpdfuser; \ - fi; \ - ln -sf /usr/sbin/gosu /usr/local/bin/su-exec + fi # Application directories RUN set -eux; \ diff --git a/docker/compose/docker-compose-unified-backend.yml b/docker/compose/docker-compose-unified-backend.yml index b8ebfd42b3..e8a9e3c3df 100644 --- a/docker/compose/docker-compose-unified-backend.yml +++ b/docker/compose/docker-compose-unified-backend.yml @@ -41,7 +41,7 @@ services: restart: unless-stopped healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status || exit 1"] + test: ["CMD-SHELL", "curl -f http://localhost:8080$${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status || exit 1"] interval: 30s timeout: 10s retries: 3 diff --git a/docker/compose/docker-compose-unified-both.yml b/docker/compose/docker-compose-unified-both.yml index 92e08e4aae..f600905de3 100644 --- a/docker/compose/docker-compose-unified-both.yml +++ b/docker/compose/docker-compose-unified-both.yml @@ -45,7 +45,7 @@ services: restart: unless-stopped healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status || exit 1"] + test: ["CMD-SHELL", "curl -f http://localhost:8080$${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status || exit 1"] interval: 30s timeout: 10s retries: 3 diff --git a/docker/compose/docker-compose-unified-frontend.yml b/docker/compose/docker-compose-unified-frontend.yml index c7d217b34c..0739bf8a7a 100644 --- a/docker/compose/docker-compose-unified-frontend.yml +++ b/docker/compose/docker-compose-unified-frontend.yml @@ -24,7 +24,7 @@ services: UMASK: "022" restart: unless-stopped healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status || exit 1"] + test: ["CMD-SHELL", "curl -f http://localhost:8080$${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status || exit 1"] interval: 30s timeout: 10s retries: 3 diff --git a/docker/compose/docker-compose.fat.yml b/docker/compose/docker-compose.fat.yml index a6d657f156..215471ab0b 100644 --- a/docker/compose/docker-compose.fat.yml +++ b/docker/compose/docker-compose.fat.yml @@ -10,7 +10,7 @@ services: limits: memory: 6G healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"] + test: ["CMD-SHELL", "curl -f http://localhost:8080$${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status | grep -q 'UP'"] interval: 5s timeout: 10s retries: 16 diff --git a/docker/compose/docker-compose.ultra-lite.yml b/docker/compose/docker-compose.ultra-lite.yml index 420a64137f..af2c6efc71 100644 --- a/docker/compose/docker-compose.ultra-lite.yml +++ b/docker/compose/docker-compose.ultra-lite.yml @@ -10,7 +10,7 @@ services: limits: memory: 2G healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"] + test: ["CMD-SHELL", "curl -f http://localhost:8080$${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status | grep -q 'UP'"] interval: 5s timeout: 10s retries: 16 diff --git a/docker/compose/docker-compose.yml b/docker/compose/docker-compose.yml index 359bf2f463..577338a90d 100644 --- a/docker/compose/docker-compose.yml +++ b/docker/compose/docker-compose.yml @@ -6,7 +6,7 @@ services: container_name: stirling-pdf restart: unless-stopped healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"] + test: ["CMD-SHELL", "curl -f http://localhost:8080$${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status | grep -q 'UP'"] interval: 5s timeout: 10s retries: 16 diff --git a/docker/embedded/Dockerfile b/docker/embedded/Dockerfile index ee04dcf7e7..6f6a5ff026 100644 --- a/docker/embedded/Dockerfile +++ b/docker/embedded/Dockerfile @@ -1,17 +1,22 @@ # Stirling-PDF - Full version (embedded frontend) # Uses pre-built base image for fast builds -ARG BASE_VERSION=1.0.0 -ARG BASE_IMAGE=ghcr.io/stirling-tools/stirling-pdf-base:${BASE_VERSION} +ARG BASE_VERSION=1.0.2 +ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION} # Stage 1: Build the Java application and frontend FROM gradle:9.3.1-jdk25 AS app-build +ARG TASK_VERSION=3.49.1 RUN apt-get update \ && apt-get install -y --no-install-recommends curl ca-certificates \ && update-ca-certificates \ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ + && ARCH=$(dpkg --print-architecture) \ + && curl -fsSL "https://github.com/go-task/task/releases/download/v${TASK_VERSION}/task_${TASK_VERSION}_linux_${ARCH}.deb" -o /tmp/task.deb \ + && dpkg -i /tmp/task.deb \ + && rm /tmp/task.deb \ && rm -rf /var/lib/apt/lists/* # JDK 25+: --add-exports is no longer accepted via JAVA_TOOL_OPTIONS; use JDK_JAVA_OPTIONS instead @@ -34,9 +39,11 @@ RUN gradle dependencies --no-daemon || true COPY . . +ARG PROTOTYPES_BUILD=false RUN DISABLE_ADDITIONAL_FEATURES=false \ gradle clean build \ -PbuildWithFrontend=true \ + -PprototypesMode=${PROTOTYPES_BUILD} \ -x spotlessApply -x spotlessCheck -x test -x sonarqube \ --no-daemon @@ -91,6 +98,15 @@ ENV VERSION_TAG=$VERSION_TAG \ _JVM_OPTS_BALANCED="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/configs/heap_dumps -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=4m -XX:G1PeriodicGCInterval=60000 -XX:+UseStringDeduplication -XX:+UseCompactObjectHeaders -XX:+ExplicitGCInvokesConcurrent -Dspring.threads.virtual.enabled=true -Djava.awt.headless=true" \ _JVM_OPTS_PERFORMANCE="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/configs/heap_dumps -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -XX:+UseCompactObjectHeaders -XX:+UseStringDeduplication -XX:+AlwaysPreTouch -XX:+ExplicitGCInvokesConcurrent -Dspring.threads.virtual.enabled=true -Djava.awt.headless=true" \ JAVA_CUSTOM_OPTS="" \ + HOME=/home/stirlingpdfuser \ + PUID=1000 \ + PGID=1000 \ + UMASK=022 \ + STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \ + TMPDIR=/tmp/stirling-pdf \ + TEMP=/tmp/stirling-pdf \ + TMP=/tmp/stirling-pdf \ + DBUS_SESSION_BUS_ADDRESS=/dev/null \ SAL_TMP=/tmp/stirling-pdf/libre # Metadata labels @@ -110,7 +126,7 @@ EXPOSE 8080/tcp STOPSIGNAL SIGTERM HEALTHCHECK --interval=30s --timeout=15s --start-period=120s --retries=5 \ - CMD curl -fs --max-time 10 http://localhost:8080/api/v1/info/status || exit 1 + CMD curl -fs --max-time 10 http://localhost:8080${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status || exit 1 ENTRYPOINT ["tini", "--", "/scripts/init.sh"] CMD [] diff --git a/docker/embedded/Dockerfile.fat b/docker/embedded/Dockerfile.fat index dfdaaea691..81c10943f6 100644 --- a/docker/embedded/Dockerfile.fat +++ b/docker/embedded/Dockerfile.fat @@ -2,17 +2,22 @@ # Extra fonts for air-gapped environments # Uses pre-built base image for fast builds -ARG BASE_VERSION=1.0.0 -ARG BASE_IMAGE=ghcr.io/stirling-tools/stirling-pdf-base:${BASE_VERSION} +ARG BASE_VERSION=1.0.2 +ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION} # Stage 1: Build the Java application and frontend FROM gradle:9.3.1-jdk25 AS app-build +ARG TASK_VERSION=3.49.1 RUN apt-get update \ && apt-get install -y --no-install-recommends curl ca-certificates \ && update-ca-certificates \ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ + && ARCH=$(dpkg --print-architecture) \ + && curl -fsSL "https://github.com/go-task/task/releases/download/v${TASK_VERSION}/task_${TASK_VERSION}_linux_${ARCH}.deb" -o /tmp/task.deb \ + && dpkg -i /tmp/task.deb \ + && rm /tmp/task.deb \ && rm -rf /var/lib/apt/lists/* # JDK 25+: --add-exports is no longer accepted via JAVA_TOOL_OPTIONS; use JDK_JAVA_OPTIONS instead @@ -91,8 +96,17 @@ ENV VERSION_TAG=$VERSION_TAG \ _JVM_OPTS_BALANCED="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/configs/heap_dumps -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=4m -XX:G1PeriodicGCInterval=60000 -XX:+UseStringDeduplication -XX:+UseCompactObjectHeaders -XX:+ExplicitGCInvokesConcurrent -Dspring.threads.virtual.enabled=true -Djava.awt.headless=true" \ _JVM_OPTS_PERFORMANCE="-XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/configs/heap_dumps -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -XX:+UseCompactObjectHeaders -XX:+UseStringDeduplication -XX:+AlwaysPreTouch -XX:+ExplicitGCInvokesConcurrent -Dspring.threads.virtual.enabled=true -Djava.awt.headless=true" \ JAVA_CUSTOM_OPTS="" \ + HOME=/home/stirlingpdfuser \ + PUID=1000 \ + PGID=1000 \ + UMASK=022 \ FAT_DOCKER=true \ INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \ + STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \ + TMPDIR=/tmp/stirling-pdf \ + TEMP=/tmp/stirling-pdf \ + TMP=/tmp/stirling-pdf \ + DBUS_SESSION_BUS_ADDRESS=/dev/null \ SAL_TMP=/tmp/stirling-pdf/libre # Metadata labels @@ -112,7 +126,7 @@ EXPOSE 8080/tcp STOPSIGNAL SIGTERM HEALTHCHECK --interval=30s --timeout=15s --start-period=120s --retries=5 \ - CMD curl -fs --max-time 10 http://localhost:8080/api/v1/info/status || exit 1 + CMD curl -fs --max-time 10 http://localhost:8080${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status || exit 1 ENTRYPOINT ["tini", "--", "/scripts/init.sh"] CMD [] diff --git a/docker/embedded/Dockerfile.ultra-lite b/docker/embedded/Dockerfile.ultra-lite index e0d719cd70..b748dbab82 100644 --- a/docker/embedded/Dockerfile.ultra-lite +++ b/docker/embedded/Dockerfile.ultra-lite @@ -5,12 +5,17 @@ FROM gradle:9.3.1-jdk25 AS build # Install Node.js and npm for frontend build +ARG TASK_VERSION=3.49.1 RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && npm --version \ && node --version \ + && ARCH=$(dpkg --print-architecture) \ + && curl -fsSL "https://github.com/go-task/task/releases/download/v${TASK_VERSION}/task_${TASK_VERSION}_linux_${ARCH}.deb" -o /tmp/task.deb \ + && dpkg -i /tmp/task.deb \ + && rm /tmp/task.deb \ && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -93,7 +98,7 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a bash \ curl \ shadow \ - su-exec && \ + util-linux && \ mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf /tmp/stirling-pdf/heap_dumps && \ mkdir -p /usr/share/fonts/opentype/noto && \ # User permissions diff --git a/docker/embedded/compose/docker-compose-latest-fat-endpoints-disabled.yml b/docker/embedded/compose/docker-compose-latest-fat-endpoints-disabled.yml index 512ba4f144..c1d2360b53 100644 --- a/docker/embedded/compose/docker-compose-latest-fat-endpoints-disabled.yml +++ b/docker/embedded/compose/docker-compose-latest-fat-endpoints-disabled.yml @@ -11,7 +11,7 @@ services: limits: memory: 4G healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"] + test: ["CMD-SHELL", "curl -f http://localhost:8080$${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status | grep -q 'UP'"] interval: 5s timeout: 10s retries: 16 diff --git a/docker/embedded/compose/docker-compose-latest-fat-security.yml b/docker/embedded/compose/docker-compose-latest-fat-security.yml index 9ca92ce60c..7abd1acd6c 100644 --- a/docker/embedded/compose/docker-compose-latest-fat-security.yml +++ b/docker/embedded/compose/docker-compose-latest-fat-security.yml @@ -10,7 +10,7 @@ services: limits: memory: 4G healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"] + test: ["CMD-SHELL", "curl -f http://localhost:8080$${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status | grep -q 'UP'"] interval: 5s timeout: 10s retries: 16 diff --git a/docker/embedded/compose/docker-compose-latest-security-remote-uno.yml b/docker/embedded/compose/docker-compose-latest-security-remote-uno.yml index d1b804ad4c..a80a04e133 100644 --- a/docker/embedded/compose/docker-compose-latest-security-remote-uno.yml +++ b/docker/embedded/compose/docker-compose-latest-security-remote-uno.yml @@ -6,7 +6,7 @@ services: context: ../../.. dockerfile: docker/embedded/Dockerfile healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"] + test: ["CMD-SHELL", "curl -f http://localhost:8080$${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status | grep -q 'UP'"] interval: 5s timeout: 10s retries: 16 diff --git a/docker/embedded/compose/docker-compose-latest-security.yml b/docker/embedded/compose/docker-compose-latest-security.yml index af8979bafa..42f9946dc0 100644 --- a/docker/embedded/compose/docker-compose-latest-security.yml +++ b/docker/embedded/compose/docker-compose-latest-security.yml @@ -6,7 +6,7 @@ services: context: ../../.. dockerfile: docker/embedded/Dockerfile healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"] + test: ["CMD-SHELL", "curl -f http://localhost:8080$${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status | grep -q 'UP'"] interval: 5s timeout: 10s retries: 16 diff --git a/docker/embedded/compose/docker-compose-latest-ultra-lite.yml b/docker/embedded/compose/docker-compose-latest-ultra-lite.yml index a5cae8dc54..11174d1064 100644 --- a/docker/embedded/compose/docker-compose-latest-ultra-lite.yml +++ b/docker/embedded/compose/docker-compose-latest-ultra-lite.yml @@ -10,7 +10,7 @@ services: limits: memory: 1G healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"] + test: ["CMD-SHELL", "curl -f http://localhost:8080$${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"] interval: 5s timeout: 10s retries: 16 diff --git a/docker/embedded/compose/test_cicd.yml b/docker/embedded/compose/test_cicd.yml index d1e08013c2..fe165c8644 100644 --- a/docker/embedded/compose/test_cicd.yml +++ b/docker/embedded/compose/test_cicd.yml @@ -5,12 +5,8 @@ services: build: context: ../../../ dockerfile: docker/embedded/Dockerfile.fat - deploy: - resources: - limits: - memory: 4G healthcheck: - test: ["CMD-SHELL", "curl -f -H 'X-API-KEY: 123456789' http://localhost:8080/api/v1/info/status | grep -q 'UP'"] + test: ["CMD-SHELL", "curl -f -H 'X-API-KEY: 123456789' http://localhost:8080${SYSTEM_ROOTURIPATH:-''}/api/v1/info/status | grep -q 'UP'"] interval: 5s timeout: 10s retries: 16 diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile index af80bee067..5fef0ea3e4 100644 --- a/docker/frontend/Dockerfile +++ b/docker/frontend/Dockerfile @@ -13,7 +13,7 @@ RUN npm ci COPY frontend . # Build the application -RUN npm run build +RUN npx vite build # Production stage FROM nginx:alpine@sha256:b0f7830b6bfaa1258f45d94c240ab668ced1b3651c8a222aefe6683447c7bf55 diff --git a/docs/security/VERIFYING_RELEASES.md b/docs/security/VERIFYING_RELEASES.md new file mode 100644 index 0000000000..2ddb8d905b --- /dev/null +++ b/docs/security/VERIFYING_RELEASES.md @@ -0,0 +1,114 @@ +# Verifying Stirling-PDF Release Artifacts + +Every Linux release artifact (`.AppImage`, `.rpm`, `.deb`) is signed with the +**Stirling-PDF release signing key**. Users are encouraged to verify downloads +before running them, especially when obtaining Stirling-PDF from a mirror, +redistributor, or any source other than the official +[GitHub Releases page](https://github.com/Stirling-Tools/Stirling-PDF/releases). + +## Signing key + +| Field | Value | +|--------------|----------------------------------------------------| +| User ID | `Stirling PDF Inc. ` | +| Fingerprint | `EBB9 258B FEA4 7D92 342F 00DF B8C0 96A5 9BEF 2A8B` | +| Algorithm | RSA-4096 | +| Valid until | 2031-04-16 | + +The public key is committed to this repository at +[`docs/security/signing-key.pub`](signing-key.pub) and is also published on: + +- https://keys.openpgp.org/search?q=EBB9258BFEA47D92342F00DFB8C096A59BEF2A8B +- https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xEBB9258BFEA47D92342F00DFB8C096A59BEF2A8B + +Cross-checking the fingerprint from two independent sources (the repository and +a keyserver) is the recommended way to be sure you've obtained the genuine key. + +## One-time setup — import the public key + +```bash +# Option 1 — from the repo over HTTPS +curl -fsSL https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/security/signing-key.pub \ + | gpg --import + +# Option 2 — from a keyserver +gpg --keyserver hkps://keys.openpgp.org \ + --recv-keys EBB9258BFEA47D92342F00DFB8C096A59BEF2A8B +``` + +Confirm the fingerprint matches after import: + +```bash +gpg --fingerprint contact@stirlingpdf.com +# Expected: EBB9 258B FEA4 7D92 342F 00DF B8C0 96A5 9BEF 2A8B +``` + +## Verifying an `.AppImage` + +Tauri's AppImage bundler embeds the signature inside the AppImage itself via +`appimagetool --sign`. Extract and verify: + +```bash +# --appimage-signature prints the embedded signature +./Stirling-PDF_*.AppImage --appimage-signature > sig.asc +./Stirling-PDF_*.AppImage --appimage-offset # shows the offset +# Verify the payload signature against the key +gpg --verify sig.asc Stirling-PDF_*.AppImage +``` + +A successful result looks like: + +``` +gpg: Good signature from "Stirling PDF Inc. " [ultimate] +``` + +## Verifying an `.rpm` + +RPM signatures are verified via `rpm --checksig`: + +```bash +# Import the key into rpm's keyring +sudo rpm --import docs/security/signing-key.pub # if working from a clone +# OR +sudo rpm --import https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/security/signing-key.pub + +# Verify the package +rpm --checksig Stirling-PDF-*.rpm +# Expected output ends with: "digests signatures OK" +``` + +## Verifying a `.deb` + +Debian packages are signed with a detached `.asc` file distributed alongside +the `.deb` on the release page: + +```bash +gpg --verify Stirling-PDF-*.deb.asc Stirling-PDF-*.deb +``` + +## What if verification fails? + +A failed signature check means **do not install the file**. Possible causes: + +- The download was corrupted — try again from the + [official releases](https://github.com/Stirling-Tools/Stirling-PDF/releases). +- You obtained the file from a malicious mirror — get it from the official + source. +- The signing key has rotated — check this document on the latest `main` for + the current fingerprint. + +If none of those explain it, please open a security report at +https://github.com/Stirling-Tools/Stirling-PDF/security/advisories/new. + +## Key rotation policy + +The signing key expires on **2031-04-16**. We will publish a new key at least +six months before expiry. The transition process: + +1. A new key is announced in release notes and this document is updated. +2. The last few releases will be co-signed with both the old and new keys. +3. The old key is published with a revocation notice once the transition is + complete. + +If the signing key is ever compromised, a revocation certificate will be +published immediately to both keyservers and to this document. diff --git a/docs/security/signing-key.pub b/docs/security/signing-key.pub new file mode 100644 index 0000000000..cc47ffe8ca --- /dev/null +++ b/docs/security/signing-key.pub @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGniEeQBEADHiXeQS+OD8Tzm9RoFEZZE09yD/9GTMXZh3YZuOQTHXoEYHmQE +mdytue4LdyKQtWxn6DlHnB3ea724yARtRkMcleljDYC9bcAb2/Cpysf9vTI8QxFa +Tw+T1jp/it0WC4Gsc18pfZeS+QaGwqs9MkMiy1jSnxSf3q4pncCcI+w/8ZqC7iRE +XHoIQPJLx0poSqqa0y4hbK8rM2zms/RGmktQoVuGEVSkt2dmGDGoHpKo4sueyMtr +qm4Qy3n7qQDT/Fl9YPgs9KYdCWJV8uET4WHPaUMz44798gip4m5RIexa7eNdIUid +wpfGDJwd4FTZ3MndHaJ96gW1mrIp0YzCnZxTibp2Ki/BQpw6kwgfkWLa9J97ikIu +u/4tTrZV/wUGX6nOIDsJgTdfGICjdnnt3pkp8NeBaU54NPahwlaVvp7byiehrhDe +qyLO+rDvwnraxeHTdpF2s2Rb+/gZZJKUgRqimFah8Enh6Ntwu8J39NQK5+SLZkbG ++3g6Xlnn8T9+7if830xvbXh8CmIYdMe0SEkZQQqNi9pZzkUMf3B1gar8FoLR23/u +eem04OLNAwQFFVkTAsmLB1uMYZuo01EsSl5dmnMF4bovJxhHp4mbZwBcpehA6b9R +WX29aqeJYM6j/z8MBXkjmIh7Z/Ftqc+BSWz4So0goxSsqH8D4Yvhspy95wARAQAB +tCtTdGlybGluZyBQREYgSW5jLiA8Y29udGFjdEBzdGlybGluZ3BkZi5jb20+iQJX +BBMBCABBFiEE67kli/6kfZI0LwDfuMCWpZvvKosFAmniEeQCGwMFCQlmAYAFCwkI +BwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQuMCWpZvvKosUXQ/9F6PeGwB7qNkC +22rBel7QjzF0XJVYPboLdqXKZGN4Nr1Mr57aHfETNfnDxfxG7ClEAfGuOR7McSG7 +ewPDdd4f0qCbQWkqUXOnmtb9+2ljiRHGtEg3gWUoCfIDxORaMluWO++z4VmKRS2L +Ech761cU3acgDePkHfIg62/agNlhFgrE5QRnUznyf8OKAJbcl3vlyBC9eXZ+9Atg +7FAXfohOBdn9J5i3v+AmnAJSoDfjNYseaJqCouhdPM1uNoSq798JnE3X+j2Pt/AY +2Nlz1ZdREgiHGL0zA4/ILEchv+Ypl/gtNsArsbJJdMST5conihc2L7bcJ7FJzd71 +xFczvUyhPTrPJcaryFYvR559xIDtJ5ucz6C63DAL32kmE4fnUd2UE+/pwx7pkEit +sqTq/fSZNALyq0W5IL6jgEzdC2yUodHytULk3JSS7RQakkkVHrhkJrm5UnOYXTkD +62k4tpDIBtuZ6NyD+X4PD2XBUdsdu42OetKrr7tIN/ZktfV1C5BY8+Jr4ZQSxXG5 +8Qwjfcytem4rhuumfS0pqaJ91PUNDxVTyKEBsa8a/gkLelkapnvDMU9DyND75UBw +Y81ptA2Q5u4nODJ1ow8R/6GiolC9/0mNQPHZatdI0uoC7fRUMybEvrcSl+D1YpgC +uuEh1VurvrV0J6VdwncFYrjZiwxo0A25Ag0EaeIR5AEQANFuE3C5iq6GWZ7rWnz6 +IORKb+aK7p/ifES4TXq3GVgWGslcPpnChlcfuMLo91QySRNrgnT5qkRg70JvtVil +y8hOx4rcfEEK/lKDIBmu6/9DdQhegxmjP+myXYqpMzE5l+t5wWDKWK4e2b0SluRM +x3sb0rp7mM0KM/VmMCDrDCd3zWE8Z/r40Oc/6pKJ7FmorZmU8OyLQAIiXhnANPW7 +nAUEvtjS57ctIBK58UQVfLasoKMe3JIjRU67sVGV6pdQS9b+1wv8cFIAjUdlWmem +gYy8YZP2XS1kgq6WsNYE0noxs1UafJ+r1IbNs9czT3S2Uu6hjIukKWyb1CjehOlj +HbS9WRfNyeAVz3R7HUfKukFF1a58DPe0shxdSr9xqths5337Nx+M9nVQwSSQeALl +95F2o1dokZc/Wzk83T4K89zGlpZYdehTObdPlaLH8b/p7UH47gjH3naEHpp0vQAE +w/7VkYp0Pqo9YtST8Xom05R/kDrUdNYjJHdEMSKRKh+yw9N+cyMX3yQjJFf+tUzW +d3tpdXQx9DeQR6I8os9uvoSYtg9GHaWHHxkn64y6Y6TjMF9RT//lifRxyt6dGyVW +ROkzzojqfXUTONdzO4SSNvXhD9EDx9MVnCG44WXuMf4VMj+cTQKmguENiur/Xh4k +Uq3B9NtpB+fzSX1WRqEVTTQHABEBAAGJAjwEGAEIACYWIQTruSWL/qR9kjQvAN+4 +wJalm+8qiwUCaeIR5AIbDAUJCWYBgAAKCRC4wJalm+8qixuuD/9uGhTTLxL7CiIh +D9QzkPQzo7qRl1+ca1DpM6cF+N26gHyy03AELm6HuacCDGt3+ZtpF8ADFTwQzkVq +Y1UGkd1mSxKank3uATEv7bksHkZE6pKk2RGK7JtMGmdoaUOs5BXGFRgE3ntRYM+4 +rXGld+gQpxCw5URFwD6+vaByNcxgSGCLMxWCvbAargWTqHuVoFUwbd6LV8qp/DCR +8W1HzlDoHqOyZZpYAHiyRKhlrElImE2Bxn99z5oE3beNw5kd5D87P4oSRkrlz+Kj +OSqJPWTh5gNE0z3JPsQDCNpmEuifZbJc0SF6QLg8/oYabZiM34YFMqMtsc4t4vtH +bAtUPWZoPiW0YAM90caoGQcU7kaA7Jn38QwMZ8FDv9OWMO6LQDHIY6CzXxRgakoy +OPkn/iTwCecrdqHlBtc2hINSguPw6JO35O08BmOYLe/lFTf5EWcGZFCVG5raos+2 +AZry7VjhGMmFONteuLyZ94Istse122L158M10V63S7SCFzPEAZw0mhHMOULfXt/Z +wk1Czlo6J78TsEevILG0Ft4a7hyAUiPIigZ/LqZRH2Skrk4pVVP5rFlwmITQg7dG +NldnyvBgL4CX72XIc6BumY9k3eiFzM++XuQ9Z/NkSjYkynGyjMD0FjlGQj6xHdr4 +0546Zz4onsw5L5I/NzcaG5HK1YNUNA== +=vzHS +-----END PGP PUBLIC KEY BLOCK----- diff --git a/engine/.env b/engine/.env new file mode 100644 index 0000000000..7d1aa13940 --- /dev/null +++ b/engine/.env @@ -0,0 +1,44 @@ +############################################################################### +# Environment variables used within the AI Engine. +# Values can be overridden in the uncommitted sibling `.env.local` file. +# Note: This file is committed to Git, so should not contain any private keys. +############################################################################### + +# Configure the model strings passed to pydantic-ai. Provider credentials are handled by +# pydantic-ai and should be set using the provider's native environment variables, for example +# ANTHROPIC_API_KEY or OPENAI_API_KEY. +STIRLING_SMART_MODEL=anthropic:claude-haiku-4-5 +STIRLING_FAST_MODEL=anthropic:claude-haiku-4-5 + +# Default output token limits applied by the engine for each model tier. +STIRLING_SMART_MODEL_MAX_TOKENS=8192 +STIRLING_FAST_MODEL_MAX_TOKENS=2048 + +# RAG Configuration — retrieval-augmented generation is always on. +# Embedding provider credentials are handled natively (e.g. VOYAGE_API_KEY for VoyageAI). +STIRLING_RAG_EMBEDDING_MODEL=voyageai:voyage-4 + +# Vector store backend: "sqlite" (embedded) or "pgvector" (external Postgres). +STIRLING_RAG_BACKEND=sqlite + +# Path to the sqlite-vec database file (used when backend=sqlite). +STIRLING_RAG_STORE_PATH=data/rag.db + +# Postgres DSN for pgvector (used when backend=pgvector). Leave empty when backend=sqlite. +# Example: postgresql://user:password@host:5432/dbname +STIRLING_RAG_PGVECTOR_DSN= + +STIRLING_RAG_CHUNK_SIZE=512 +STIRLING_RAG_CHUNK_OVERLAP=64 +STIRLING_RAG_TOP_K=5 + +# PostHog analytics. Set STIRLING_POSTHOG_ENABLED=true and provide an API key to enable. +STIRLING_POSTHOG_ENABLED=false +STIRLING_POSTHOG_API_KEY=phc_VOdeYnlevc2T63m3myFGjeBlRcIusRgmhfx6XL5a1iz +STIRLING_POSTHOG_HOST=https://eu.i.posthog.com + +# Log level for the stirling logger hierarchy (DEBUG, INFO, WARNING, ERROR) +STIRLING_LOG_LEVEL=INFO + +# Path to log file. Rolls daily, keeps 1 backup. Leave empty for console only. +STIRLING_LOG_FILE= diff --git a/engine/.gitignore b/engine/.gitignore index 1116f48815..890e5247ba 100644 --- a/engine/.gitignore +++ b/engine/.gitignore @@ -19,7 +19,6 @@ yarn-error.log* .vite/ # Environment -.env .env.local # LaTeX outputs diff --git a/engine/AGENTS.md b/engine/AGENTS.md index 87fa13db26..8e45662740 100644 --- a/engine/AGENTS.md +++ b/engine/AGENTS.md @@ -4,9 +4,23 @@ This file is for AI agents working in `engine/`. The engine is a Python reasoning service for Stirling. It plans and interprets work, but it does not own durable state, and it does not execute Stirling PDF operations directly. Keep the service narrow: typed contracts in, typed contracts out, with AI only where it adds reasoning value. +## Commands + +All engine commands can be run from the repository root using Task: + +- `task engine:check` — run all checks (typecheck + lint + format-check + test) +- `task engine:fix` — auto-fix lint + formatting +- `task engine:install` — install Python dependencies via uv +- `task engine:dev` — start FastAPI with hot reload (localhost:5001) +- `task engine:test` — run pytest +- `task engine:lint` — run ruff linting +- `task engine:typecheck` — run pyright +- `task engine:format` — format code with ruff +- `task engine:tool-models` — generate tool_models.py from Java OpenAPI spec + ## Code Style -- Keep `make check` passing. +- Keep `task engine:check` passing. - Use modern Python when it improves clarity. - Prefer explicit names to cleverness. - Avoid nested functions and nested classes unless the language construct requires them. diff --git a/engine/Dockerfile b/engine/Dockerfile index 817f949510..5caeb5d6bf 100644 --- a/engine/Dockerfile +++ b/engine/Dockerfile @@ -1,9 +1,20 @@ # syntax=docker/dockerfile:1.5 FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim +ARG TASK_VERSION=3.49.1 +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl ca-certificates \ + && ARCH=$(dpkg --print-architecture) \ + && curl -fsSL "https://github.com/go-task/task/releases/download/v${TASK_VERSION}/task_${TASK_VERSION}_linux_${ARCH}.deb" -o /tmp/task.deb \ + && dpkg -i /tmp/task.deb \ + && rm /tmp/task.deb \ + && rm -rf /var/lib/apt/lists/* + WORKDIR /app -COPY pyproject.toml uv.lock ./ +COPY pyproject.toml uv.lock Taskfile.yml .env ./ +COPY .taskfiles/ ./.taskfiles/ +COPY scripts/ ./scripts/ RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-dev @@ -14,4 +25,4 @@ ENV PYTHONUNBUFFERED=1 EXPOSE 5001 -CMD ["uv", "run", "uvicorn", "stirling.api.app:app", "--host", "0.0.0.0", "--port", "5001"] +CMD ["task", "engine:run"] diff --git a/engine/Makefile b/engine/Makefile deleted file mode 100644 index 71d637694e..0000000000 --- a/engine/Makefile +++ /dev/null @@ -1,82 +0,0 @@ -.PHONY: help install prep check fix lint lint-fix lint-fix-unsafe format format-check typecheck test run run-dev clean tool-models docker-build docker-run - -REPO_ROOT_DIR = .. -ROOT_DIR = . -VENV_DIR = $(ROOT_DIR)/.venv -DEPS_STAMP := $(VENV_DIR)/.deps-installed -TOOL_MODELS := $(CURDIR)/src/stirling/models/tool_models.py -FRONTEND_DIR := $(REPO_ROOT_DIR)/frontend -FRONTEND_TSX := $(FRONTEND_DIR)/node_modules/.bin/tsx - -$(DEPS_STAMP): $(ROOT_DIR)/uv.lock $(ROOT_DIR)/pyproject.toml - uv python install 3.13.8 - uv sync - touch $(DEPS_STAMP) - -help: - @echo "Engine commands:" - @echo " make install - Install production dependencies" - @echo " make prep - Set up .env file from .env.example" - @echo " make lint - Run linting checks" - @echo " make format - Format code" - @echo " make format-check - Show whether code is correctly formatted" - @echo " make typecheck - Run type checking" - @echo " make test - Run tests" - @echo " make run - Run the FastAPI backend with uvicorn" - @echo " make run-dev - Run the FastAPI backend with reload" - @echo " make tool-models - Generate src/stirling/models/tool_models.py from frontend TypeScript tool defs" - @echo " make clean - Clean up generated files" - @echo " make docker-build - Build Docker image" - @echo " make docker-run - Run Docker container" - -install: $(DEPS_STAMP) - -prep: install - uv run scripts/setup_env.py - -check: typecheck lint format-check test - -fix: lint-fix format - -lint: install - uv run ruff check . - -lint-fix: install - uv run ruff check . --fix - -lint-fix-unsafe: install - uv run ruff check . --fix --unsafe-fixes - -format: install - uv run ruff format . - -format-check: install - uv run ruff format . --diff - -typecheck: install - uv run pyright . --warnings - -test: prep - uv run pytest tests - -run: prep - cd src && PYTHONUNBUFFERED=1 uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port 5001 - -run-dev: prep - cd src && PYTHONUNBUFFERED=1 uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port 5001 --reload - -frontend-deps: - if [ ! -x "$(FRONTEND_TSX)" ]; then cd $(FRONTEND_DIR) && npm install; fi - -tool-models: install frontend-deps - uv run python scripts/generate_tool_models.py --output $(TOOL_MODELS) - $(MAKE) fix - -clean: - rm -rf $(VENV_DIR) data logs output - -docker-build: - docker build -t stirling-pdf-engine . - -docker-run: - docker run -p 5001:5001 stirling-pdf-engine diff --git a/engine/pyproject.toml b/engine/pyproject.toml index 693a281532..f1a10fe108 100644 --- a/engine/pyproject.toml +++ b/engine/pyproject.toml @@ -5,17 +5,26 @@ description = "AI Document Engine" requires-python = ">=3.13" dependencies = [ "fastapi>=0.116.0", + "pgvector>=0.3.6", + "psycopg[binary]>=3.2", "pydantic>=2.0.0", "pydantic-ai>=1.67.0", + "pydantic-ai-slim[voyageai]>=1.67.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", ] [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", ] @@ -42,9 +51,9 @@ select = [ "W", "RUF100", "UP", -] -ignore = [ - "E501", # Temporarily disable line length limit until codebase conformat + "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.pyright] @@ -55,6 +64,7 @@ reportUnnecessaryCast = "warning" reportUnnecessaryTypeIgnoreComment = "warning" reportUnusedImport = "warning" reportUnknownParameterType = "warning" +reportDeprecated = "warning" [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/engine/scripts/generate_tool_models.py b/engine/scripts/generate_tool_models.py index bc339acb0a..9c67fcae49 100644 --- a/engine/scripts/generate_tool_models.py +++ b/engine/scripts/generate_tool_models.py @@ -1,509 +1,222 @@ #!/usr/bin/env python3 +"""Generate Python tool models from the Java backend's OpenAPI spec (SwaggerDoc.json). + +Uses datamodel-code-generator to convert OpenAPI request schemas to Pydantic models. +Run via: + task engine:tool-models +""" + from __future__ import annotations import argparse import json -import keyword -import re -import subprocess -from collections.abc import Callable from dataclasses import dataclass from pathlib import Path from typing import Any -TOOL_MODELS_HEADER = """# AUTO-GENERATED FILE. DO NOT EDIT. -# Generated by scripts/generate_tool_models.py from frontend TypeScript sources. -# ruff: noqa: N815 -""" +from datamodel_code_generator import InputFileType, PythonVersion, generate +from datamodel_code_generator.enums import DataModelType +from datamodel_code_generator.format import Formatter +from referencing import Registry, Resource +from referencing.jsonschema import DRAFT202012 +# Fields inherited from PDFFile base class — not tool parameters. +BASE_CLASS_FIELDS = frozenset({"fileInput", "fileId"}) -OPERATION_TYPE_RE = re.compile(r"operationType\s*:\s*['\"]([A-Za-z0-9_]+)['\"]") -DEFAULT_REF_RE = re.compile(r"defaultParameters\s*:\s*([A-Za-z0-9_]+)") -DEFAULT_SHORTHAND_RE = re.compile(r"\bdefaultParameters\b") -IMPORT_RE = re.compile(r"import\s*\{([^}]+)\}\s*from\s*['\"]([^'\"]+)['\"]") -VAR_OBJ_RE_TEMPLATE = r"(?:export\s+)?const\s+{name}\b[^=]*=\s*\{{" +_ENGINE_ROOT = Path(__file__).resolve().parents[1] + +_FILE_HEADER = ( + "# AUTO-GENERATED FILE. DO NOT EDIT.\n" + "# Generated by scripts/generate_tool_models.py from Java OpenAPI spec (SwaggerDoc.json).\n" + "# ruff: noqa: E501" +) @dataclass -class ToolModelSpec: - tool_id: str - params: dict[str, Any] - param_types: dict[str, Any] +class ToolSpec: + path: str + enum_name: str + class_name: str -class ParseError(Exception): - pass +@dataclass +class DiscoveryResult: + tools: list[ToolSpec] + combined_schema: dict[str, Any] -def _find_matching(text: str, start: int, open_char: str, close_char: str) -> int: - depth = 0 - i = start - in_str: str | None = None - while i < len(text): - ch = text[i] - if in_str: - if ch == "\\": - i += 2 - continue - if ch == in_str: - in_str = None - i += 1 - continue - if ch in {"'", '"'}: - in_str = ch - elif ch == open_char: - depth += 1 - elif ch == close_char: - depth -= 1 - if depth == 0: - return i - i += 1 - raise ParseError(f"Unmatched {open_char}{close_char} block") +class ToolDiscovery: + """Discovers tool endpoints from an OpenAPI spec and builds a combined JSON Schema.""" - -def _extract_block(text: str, pattern: str) -> str | None: - match = re.search(pattern, text) - if not match: - return None - brace_start = text.find("{", match.end() - 1) - if brace_start == -1: - return None - brace_end = _find_matching(text, brace_start, "{", "}") - return text[brace_start : brace_end + 1] - - -def _split_top_level_items(obj_body: str) -> list[str]: - items: list[str] = [] - depth_obj = depth_arr = 0 - in_str: str | None = None - token_start = 0 - i = 0 - while i < len(obj_body): - ch = obj_body[i] - if in_str: - if ch == "\\": - i += 2 - continue - if ch == in_str: - in_str = None - i += 1 - continue - if ch in {"'", '"'}: - in_str = ch - elif ch == "{": - depth_obj += 1 - elif ch == "}": - depth_obj -= 1 - elif ch == "[": - depth_arr += 1 - elif ch == "]": - depth_arr -= 1 - elif ch == "," and depth_obj == 0 and depth_arr == 0: - piece = obj_body[token_start:i].strip() - if piece: - items.append(piece) - token_start = i + 1 - i += 1 - tail = obj_body[token_start:].strip() - if tail: - items.append(tail) - return items - - -def _resolve_import_path(repo_root: Path, current_file: Path, module_path: str) -> Path | None: - candidates: list[Path] = [] - if module_path.startswith("@app/"): - rel = module_path[len("@app/") :] - candidates.extend( - [ - repo_root / "frontend/src/core" / f"{rel}.ts", - repo_root / "frontend/src/core" / f"{rel}.tsx", - repo_root / "frontend/src/saas" / f"{rel}.ts", - repo_root / "frontend/src/saas" / f"{rel}.tsx", - repo_root / "frontend/src" / f"{rel}.ts", - repo_root / "frontend/src" / f"{rel}.tsx", - ] - ) - elif module_path.startswith("."): - base = (current_file.parent / module_path).resolve() - candidates.extend([Path(f"{base}.ts"), Path(f"{base}.tsx")]) - for candidate in candidates: - if candidate.exists(): - return candidate - return None - - -def _parse_literal_value(value: str, resolver: Callable[[str], dict[str, Any] | None]) -> Any: - value = value.strip() - if not value: - return None - if value.startswith("{") and value.endswith("}"): - return _parse_object_literal(value, resolver) - if value.startswith("[") and value.endswith("]"): - inner = value[1:-1].strip() - if not inner: - return [] - return [_parse_literal_value(item, resolver) for item in _split_top_level_items(inner)] - if value.startswith(("'", '"')) and value.endswith(("'", '"')): - return value[1:-1] - if value in {"true", "false"}: - return value == "true" - if value == "null": - return None - if re.fullmatch(r"-?\d+", value): - return int(value) - if re.fullmatch(r"-?\d+\.\d+", value): - return float(value) - resolved = resolver(value) - if resolved is not None: - return resolved - return None - - -def _parse_object_literal(obj_text: str, resolver: Callable[[str], dict[str, Any] | None]) -> dict[str, Any]: - body = obj_text.strip()[1:-1] - result: dict[str, Any] = {} - for item in _split_top_level_items(body): - if item.startswith("..."): - spread_name = item[3:].strip() - spread = resolver(spread_name) - if isinstance(spread, dict): - result.update(spread) - continue - if ":" not in item: - continue - key, raw_value = item.split(":", 1) - key = key.strip().strip("'\"") - result[key] = _parse_literal_value(raw_value.strip(), resolver) - return result - - -def _extract_imports(source: str) -> dict[str, str]: - imports: dict[str, str] = {} - for names, module_path in IMPORT_RE.findall(source): - for part in names.split(","): - segment = part.strip() - if not segment: - continue - if " as " in segment: - original, alias = [x.strip() for x in segment.split(" as ", 1)] - imports[alias] = module_path - imports[original] = module_path - else: - imports[segment] = module_path - return imports - - -def _resolve_object_identifier(repo_root: Path, file_path: Path, source: str, identifier: str) -> dict[str, Any] | None: - var_pattern = VAR_OBJ_RE_TEMPLATE.format(name=re.escape(identifier)) - block = _extract_block(source, var_pattern) - imports = _extract_imports(source) - - def resolver(name: str) -> dict[str, Any] | None: - local_block = _extract_block(source, VAR_OBJ_RE_TEMPLATE.format(name=re.escape(name))) - if local_block: - return _parse_object_literal(local_block, resolver) - import_path = imports.get(name) - if not import_path: - return None - resolved_file = _resolve_import_path(repo_root, file_path, import_path) - if not resolved_file: - return None - imported_source = resolved_file.read_text(encoding="utf-8") - return _resolve_object_identifier(repo_root, resolved_file, imported_source, name) - - if block: - return _parse_object_literal(block, resolver) - import_path = imports.get(identifier) - if not import_path: - return None - resolved_file = _resolve_import_path(repo_root, file_path, import_path) - if not resolved_file: - return None - imported_source = resolved_file.read_text(encoding="utf-8") - return _resolve_object_identifier(repo_root, resolved_file, imported_source, identifier) - - -def _infer_py_type(value: Any) -> str: - if isinstance(value, bool): - return "bool" - if isinstance(value, int): - return "int" - if isinstance(value, float): - return "float" - if isinstance(value, str): - return "str" - if isinstance(value, list): - return "list[Any]" - if isinstance(value, dict): - return "dict[str, Any]" - return "Any" - - -def _spec_is_none(spec: dict[str, Any]) -> bool: - return spec.get("kind") == "null" - - -def _py_type_from_spec(spec: dict[str, Any]) -> str: - kind = spec.get("kind") - if kind == "string": - return "str" - if kind == "number": - return "float" - if kind == "boolean": - return "bool" - if kind == "date": - return "str" - if kind == "enum": - values = spec.get("values") - if isinstance(values, list) and values: - literal_values = ", ".join(_py_repr(v) for v in values) - return f"Literal[{literal_values}]" - if kind == "ref": - ref_name = spec.get("name") - if isinstance(ref_name, str) and ref_name.endswith("Parameters"): - return f"{ref_name[:-10]}Params" - if kind == "array": - element = spec.get("element") - inner = _py_type_from_spec(element) if isinstance(element, dict) else "Any" - return f"list[{inner}]" - if kind == "object": - dict_value = spec.get("dictValue") - if isinstance(dict_value, dict): - inner = _py_type_from_spec(dict_value) - return f"dict[str, {inner}]" - properties = spec.get("properties") - if isinstance(properties, dict) and properties: - property_types = {_py_type_from_spec(p) for p in properties.values() if isinstance(p, dict)} - if len(property_types) == 1: - inner = next(iter(property_types)) - return f"dict[str, {inner}]" - return "dict[str, Any]" - if kind in {"null"}: - return "Any" - return "Any" - - -def _to_class_name(tool_id: str) -> str: - cleaned = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", tool_id) - cleaned = re.sub(r"[^A-Za-z0-9]+", " ", cleaned) - parts = [part.capitalize() for part in cleaned.split() if part] - return "".join(parts) + "Params" - - -def _to_snake_case(name: str) -> str: - snake = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name) - snake = re.sub(r"[^A-Za-z0-9]+", "_", snake).strip("_").lower() - if not snake: - snake = "param" - if snake[0].isdigit(): - snake = f"param_{snake}" - if keyword.iskeyword(snake): - snake = f"{snake}_" - return snake - - -def _build_field_name_map(params: dict[str, Any]) -> dict[str, str]: - field_map: dict[str, str] = {} - used: set[str] = set() - for original_key in sorted(params): - base_name = _to_snake_case(original_key) - candidate = base_name - suffix = 2 - while candidate in used: - candidate = f"{base_name}_{suffix}" - suffix += 1 - used.add(candidate) - field_map[original_key] = candidate - return field_map - - -def _to_enum_member_name(tool_id: str) -> str: - return _to_snake_case(tool_id).upper() - - -def _build_enum_member_map(specs: list[ToolModelSpec]) -> dict[str, str]: - member_map: dict[str, str] = {} - used: set[str] = set() - for spec in specs: - base_name = _to_enum_member_name(spec.tool_id) - candidate = base_name - suffix = 2 - while candidate in used: - candidate = f"{base_name}_{suffix}" - suffix += 1 - used.add(candidate) - member_map[spec.tool_id] = candidate - return member_map - - -def _py_repr(value: Any) -> str: - return ( - json.dumps(value, ensure_ascii=True).replace("true", "True").replace("false", "False").replace("null", "None") + # Namespaces exposed to the LLM as callable tools. Largely matches ``InternalApiClient.java``. + # Note: ``/api/v1/filter/`` is intentionally excluded because those APIs are for pipeline processing, + # not tool execution. + ALLOWED_PATH_PREFIXES = ( + "/api/v1/general/", + "/api/v1/misc/", + "/api/v1/security/", + "/api/v1/convert/", ) + def __init__(self, spec: dict[str, Any]): + resource = Resource.from_contents(spec, default_specification=DRAFT202012) + self.resolver = Registry().with_resource("", resource).resolver() + self.spec = spec -def discover_tool_specs(repo_root: Path) -> list[ToolModelSpec]: - frontend_dir = repo_root / "frontend" - extractor = frontend_dir / "scripts/export-tool-specs.ts" - command = ["node", "--import", "tsx", str(extractor)] - result = subprocess.run( - command, - check=True, - capture_output=True, - text=True, - cwd=str(frontend_dir), + def discover(self) -> DiscoveryResult: + tools: list[ToolSpec] = [] + defs: dict[str, Any] = {} + used_enum: set[str] = set() + used_class: set[str] = set() + + for path, path_item in sorted(self.spec.get("paths", {}).items()): + if "{" in path or not any(path.startswith(p) for p in self.ALLOWED_PATH_PREFIXES): + continue + properties = self._get_request_properties(path_item) + if not properties: + continue + clean_props = self._filter_properties(properties) + if not clean_props: + continue + + enum_name = _deduplicate(_path_to_enum_name(path), used_enum) + class_name = _deduplicate(_path_to_class_name(path), used_class) + + defs[class_name] = {"type": "object", "properties": clean_props} + tools.append(ToolSpec(path, enum_name, class_name)) + + combined_schema: dict[str, Any] = { + "$defs": defs, + "anyOf": [{"$ref": f"#/$defs/{t.class_name}"} for t in tools], + } + return DiscoveryResult(tools=tools, combined_schema=combined_schema) + + def _resolve_ref(self, schema: dict[str, Any]) -> dict[str, Any]: + if "$ref" in schema: + return self.resolver.lookup(schema["$ref"]).contents + return schema + + def _get_request_properties(self, path_item: dict[str, Any]) -> dict[str, Any] | None: + post = path_item.get("post") + if not post: + return None + content = post.get("requestBody", {}).get("content", {}) + for media_type in ("multipart/form-data", "application/json"): + if media_type in content: + schema = content[media_type].get("schema") + if schema: + return self._resolve_ref(schema).get("properties") + return None + + def _filter_properties(self, properties: dict[str, Any]) -> dict[str, Any]: + """Remove base-class fields and binary upload fields, resolving any $refs.""" + clean: dict[str, Any] = {} + for name, prop in properties.items(): + if name in BASE_CLASS_FIELDS: + continue + prop = self._resolve_ref(prop) + if prop.get("type") == "string" and prop.get("format") == "binary": + continue + clean[name] = prop + return clean + + +def _tool_name_segments(path: str) -> str: + """Extract a descriptive name from the endpoint path. + + Converters use two segments (e.g. /api/v1/convert/cbr/pdf → cbr-to-pdf). + Other tools use the last segment (e.g. /api/v1/misc/compress-pdf → compress-pdf). + """ + parts = path.rstrip("/").split("/") + if "/api/v1/convert/" in path and len(parts) >= 6: + return f"{parts[-2]}-to-{parts[-1]}" + return parts[-1] + + +def _path_to_enum_name(path: str) -> str: + return _tool_name_segments(path).replace("-", "_").upper() + + +def _path_to_class_name(path: str) -> str: + return "".join(p.capitalize() for p in _tool_name_segments(path).split("-")) + "Params" + + +def _deduplicate(name: str, used: set[str]) -> str: + """Return name, appending 2, 3, ... if already in used. Adds result to used.""" + candidate = name + n = 2 + while candidate in used: + candidate = f"{name}{n}" + n += 1 + used.add(candidate) + return candidate + + +def generate_models_code(combined_schema: dict[str, Any]) -> str: + """Run datamodel-code-generator once on the combined schema.""" + code = generate( + input_=json.dumps(combined_schema, sort_keys=True), + input_file_type=InputFileType.JsonSchema, + output_model_type=DataModelType.PydanticV2BaseModel, + target_python_version=PythonVersion.PY_313, + snake_case_field=True, + base_class="stirling.models.base.ApiModel", + field_constraints=True, + no_alias=True, + set_default_enum_member=True, + additional_imports=["enum.StrEnum"], + enable_version_header=False, + custom_file_header=_FILE_HEADER, + formatters=[Formatter.RUFF_FORMAT, Formatter.RUFF_CHECK], + settings_path=_ENGINE_ROOT / "pyproject.toml", ) - raw = json.loads(result.stdout) - specs: list[ToolModelSpec] = [] - for item in raw: - tool_id = item.get("tool_id") - if not isinstance(tool_id, str) or not tool_id: - continue - params = item.get("params") - param_types = item.get("param_types") - specs.append( - ToolModelSpec( - tool_id=tool_id, - params=params if isinstance(params, dict) else {}, - param_types=param_types if isinstance(param_types, dict) else {}, - ) - ) - return sorted(specs, key=lambda spec: spec.tool_id) + return str(code or "") -def write_models_module(out_path: Path, specs: list[ToolModelSpec]) -> None: - lines: list[str] = [ - TOOL_MODELS_HEADER, - "from __future__ import annotations\n\n", - "from enum import StrEnum\n", - "from typing import Any, Literal\n\n", - "from stirling.models.base import ApiModel\n", +def write_output(out_path: Path, tools: list[ToolSpec], models_code: str) -> None: + union_lines = ["type ParamToolModel = ("] + for i, tool in enumerate(tools): + prefix = " | " if i > 0 else " " + union_lines.append(f"{prefix}{tool.class_name}") + union_lines.append(")") + union_lines.append("type ParamToolModelType = type[ParamToolModel]") + + enum_lines = [ + "class ToolEndpoint(StrEnum):", + *(f' {t.enum_name} = "{t.path}"' for t in tools), ] - class_names: dict[str, str] = {spec.tool_id: _to_class_name(spec.tool_id) for spec in specs} - class_name_to_tool_id = {name: tool_id for tool_id, name in class_names.items()} + ops_lines = [ + "OPERATIONS: dict[ToolEndpoint, ParamToolModelType] = {", + *(f" ToolEndpoint.{t.enum_name}: {t.class_name}," for t in tools), + "}", + ] - def extract_class_dependencies(spec: ToolModelSpec) -> set[str]: - deps: set[str] = set() - if not isinstance(spec.param_types, dict): - return deps - for entry in spec.param_types.values(): - if not isinstance(entry, dict): - continue - type_spec = entry - if "type" in entry and isinstance(entry.get("type"), dict): - type_spec = entry["type"] - if not isinstance(type_spec, dict): - continue - if type_spec.get("kind") != "ref": - continue - ref_name = type_spec.get("name") - if isinstance(ref_name, str) and ref_name.endswith("Parameters"): - ref_class = f"{ref_name[:-10]}Params" - if ref_class in class_name_to_tool_id: - deps.add(ref_class) - return deps - - dependencies_by_class: dict[str, set[str]] = {} - for spec in specs: - class_name = class_names[spec.tool_id] - dependencies_by_class[class_name] = extract_class_dependencies(spec) - - remaining = set(class_names.values()) - ordered_class_names: list[str] = [] - while remaining: - progress = False - for class_name in sorted(remaining): - deps = dependencies_by_class.get(class_name, set()) - if deps.issubset(set(ordered_class_names)): - ordered_class_names.append(class_name) - remaining.remove(class_name) - progress = True - break - if not progress: - ordered_class_names.extend(sorted(remaining)) - break - - ordered_specs = [next(spec for spec in specs if class_names[spec.tool_id] == name) for name in ordered_class_names] - - for spec in ordered_specs: - class_name = class_names[spec.tool_id] - lines.append(f"class {class_name}(ApiModel):\n") - all_param_keys = set(spec.params) - if isinstance(spec.param_types, dict): - all_param_keys.update(spec.param_types.keys()) - - if not all_param_keys: - lines.append(" pass\n\n\n") - continue - - field_name_map = _build_field_name_map({key: True for key in all_param_keys}) - for key in sorted(all_param_keys): - field_name = field_name_map[key] - value = spec.params.get(key) - type_spec = spec.param_types.get(key) if isinstance(spec.param_types, dict) else None - if isinstance(type_spec, dict): - py_type = _py_type_from_spec(type_spec) - else: - py_type = _infer_py_type(value) - - if value is None and (isinstance(type_spec, dict) and _spec_is_none(type_spec)): - if py_type != "Any" and "| None" not in py_type: - py_type = f"{py_type} | None" - lines.append(f" {field_name}: {py_type} = None\n") - elif value is None: - lines.append(f" {field_name}: {py_type} | None = None\n") - else: - if isinstance(type_spec, dict) and type_spec.get("kind") == "ref" and isinstance(value, dict): - lines.append(f" {field_name}: {py_type} = {py_type}.model_validate({_py_repr(value)})\n") - continue - lines.append(f" {field_name}: {py_type} = {_py_repr(value)}\n") - lines.append("\n\n") - - if class_names: - union_members = " | ".join(class_names[tool_id] for tool_id in sorted(class_names)) - lines.append(f"type ParamToolModel = {union_members}\n") - lines.append("type ParamToolModelType = type[ParamToolModel]\n\n") - else: - lines.append("type ParamToolModel = ApiModel\n") - lines.append("type ParamToolModelType = type[ParamToolModel]\n\n") - - enum_member_map = _build_enum_member_map(specs) - - lines.append("class OperationId(StrEnum):\n") - - for spec in specs: - lines.append(f" {enum_member_map[spec.tool_id]} = {spec.tool_id!r}\n") - - lines.extend( - [ - "\n\n", - "OPERATIONS: dict[OperationId, ParamToolModelType] = {\n", - ] - ) - - for spec in specs: - model_name = _to_class_name(spec.tool_id) - lines.append(f" OperationId.{enum_member_map[spec.tool_id]}: {model_name},\n") - lines.append("}\n") - out_path.write_text("".join(lines), encoding="utf-8") + parts = [models_code, "\n", *union_lines, "\n", *enum_lines, "\n", *ops_lines, ""] + out_path.write_text("\n".join(parts), encoding="utf-8") def main() -> None: - parser = argparse.ArgumentParser(description="Generate tool models from frontend TypeScript tool definitions") - parser.add_argument("--spec", help="Deprecated (ignored)", default="") - parser.add_argument("--output", default="", help="Path to tool_models.py") - parser.add_argument("--ai-output", default="", help="Deprecated (ignored)") + parser = argparse.ArgumentParser(description="Generate Python tool models from 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") args = parser.parse_args() - repo_root = Path(__file__).resolve().parents[3] - specs = discover_tool_specs(repo_root) + 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) - output_path = Path(args.output) if args.output else (repo_root / "src/stirling/models/tool_models.py") + with open(spec_path) as f: + spec = json.load(f) - write_models_module(output_path, specs) - print(f"Wrote {len(specs)} tool model specs") + 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}") if __name__ == "__main__": diff --git a/engine/scripts/setup_env.py b/engine/scripts/setup_env.py index 9626e2c8cf..c459679238 100644 --- a/engine/scripts/setup_env.py +++ b/engine/scripts/setup_env.py @@ -1,48 +1,24 @@ """ -Copies .env from .env.example if missing, and errors if any keys from the example -are absent from the actual .env file. +Ensures `.env.local` exists so developers have a place to put overrides +(API keys, local model choices, etc.) without touching the committed `.env`. Usage: uv run scripts/setup_env.py """ -import os -import shutil -import sys from pathlib import Path -from dotenv import dotenv_values - ROOT = Path(__file__).parent.parent -EXAMPLE_FILE = ROOT / "config" / ".env.example" -ENV_FILE = ROOT / ".env" +ENV_LOCAL_FILE = ROOT / ".env.local" -print("setup-env: see engine/config/.env.example for documentation") +TEMPLATE = """\ +############################################################################### +# Local overrides for `engine/.env` +# Put API keys and machine-specific settings here. Any variable defined here +# takes precedence over the committed `.env` +############################################################################### +""" -if not EXAMPLE_FILE.exists(): - print(f"setup-env: {EXAMPLE_FILE.name} not found, skipping", file=sys.stderr) - sys.exit(0) - -if not ENV_FILE.exists(): - shutil.copy(EXAMPLE_FILE, ENV_FILE) - print("setup-env: created .env from .env.example") - -env_keys = set(dotenv_values(ENV_FILE).keys()) | set(os.environ.keys()) -example_keys = set(dotenv_values(EXAMPLE_FILE).keys()) -missing = sorted(example_keys - env_keys) - -if missing: - sys.exit( - "setup-env: .env is missing keys from .env.example:\n" - + "\n".join(f" {k}" for k in missing) - + "\n Add them manually or delete your local .env to re-copy from config/.env.example." - ) - -extra = sorted(k for k in dotenv_values(ENV_FILE) if k.startswith("STIRLING_") and k not in example_keys) -if extra: - print( - "setup-env: .env contains STIRLING_ keys not in config/.env.example:\n" - + "\n".join(f" {k}" for k in extra) - + "\n Add them to config/.env.example if they are intentional.", - file=sys.stderr, - ) +if not ENV_LOCAL_FILE.exists(): + ENV_LOCAL_FILE.write_text(TEMPLATE) + print("setup-env: created empty .env.local for local overrides") diff --git a/engine/src/stirling/agents/ledger/__init__.py b/engine/src/stirling/agents/ledger/__init__.py new file mode 100644 index 0000000000..a6b4831f4f --- /dev/null +++ b/engine/src/stirling/agents/ledger/__init__.py @@ -0,0 +1,5 @@ +"""Math Auditor Agent (mathAuditorAgent) — AI-powered math validation for PDF documents.""" + +from .agent import MathAuditorAgent + +__all__ = ["MathAuditorAgent"] diff --git a/engine/src/stirling/agents/ledger/agent.py b/engine/src/stirling/agents/ledger/agent.py new file mode 100644 index 0000000000..f000b604e4 --- /dev/null +++ b/engine/src/stirling/agents/ledger/agent.py @@ -0,0 +1,550 @@ +""" +Math Auditor Agent (mathAuditorAgent) — pydantic-ai agents for PDF math validation. + +Examiner (Round 1, /api/v1/ai/math-auditor-agent/examine) + Receives a FolioManifest and returns a Requisition declaring what + Java must extract before validation can begin. + +Audit pipeline (Round 2, /api/v1/ai/math-auditor-agent/deliberate) + Processes Evidence per-page: + 1. Deterministic pass — ArithmeticScanner on every folio + 2. Fast-model pass — extract named figures from each page + 3. FigureTracker — cross-page consistency check + 4. Fast-model call — generate human-readable summary + 5. Assemble Verdict programmatically + +Neither agent ever touches a PDF file. All content arrives pre-extracted +by Java, which owns the PDF from start to finish. +""" + +from __future__ import annotations + +import asyncio +import logging +from collections.abc import Coroutine +from decimal import Decimal, InvalidOperation +from typing import Any + +from pydantic import BaseModel, Field +from pydantic_ai import Agent +from pydantic_ai.exceptions import AgentRunError + +from stirling.contracts.ledger import ( + Discrepancy, + DiscrepancyKind, + Evidence, + Folio, + FolioManifest, + Requisition, + Severity, + Verdict, +) +from stirling.logging import Pretty +from stirling.services import AppRuntime + +from .prompts import ( + EXAMINER_SYSTEM_PROMPT, + FIGURE_EXTRACTOR_PROMPT, + STATEMENT_VERIFIER_PROMPT, + SUMMARY_PROMPT, + TABLE_FORMULA_PROMPT, +) +from .validators import ArithmeticScanner, FigureTracker, FormulaEvaluator + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Structured output models for the per-page figure extractor +# --------------------------------------------------------------------------- + + +class ExtractedFigure(BaseModel): + """A single named figure found on a page.""" + + label: str = Field(description="Normalised name, e.g. 'Total Revenue', 'VAT'.") + value: str = Field(description="Numeric value as a string, e.g. '1200.00'.") + raw: str = Field(description="Original text from the document, e.g. '£1,200.00'.") + + +class FigureExtractionResult(BaseModel): + """All named figures found on a single page.""" + + figures: list[ExtractedFigure] = Field(default_factory=list) + + +class FormulaCheck(BaseModel): + """One verifiable mathematical relationship in a table.""" + + description: str = Field(description="Human-readable, e.g. 'Line Total = Qty × Unit Price'") + formula: str = Field(description="Expression: 'col3 = col1 * col2' or 'cell(4,3) = sum(col3, 1-3)'") + scope: str = Field(description="'each_row' | 'column_total' | 'single_cell'") + row_range: list[int] | None = Field(default=None, description="Data rows to check (for each_row scope)") + target_row: int | None = Field(default=None, description="Row index of total (for column_total/single_cell)") + target_col: int | None = Field(default=None, description="Column index (for column_total/single_cell)") + + +class TableFormulas(BaseModel): + """All verifiable formulas found in one table.""" + + formulas: list[FormulaCheck] = Field(default_factory=list) + + +class StatementCheck(BaseModel): + """One prose claim and its verification result.""" + + claim: str = Field(description="The exact text of the claim") + verification: str = Field(description="Type: percentage_change, comparison, ratio, trend, average, other") + values_referenced: list[str] = Field(default_factory=list, description="Numbers used in the check") + expected_result: str = Field(description="What the calculation actually yields") + actual_claim: str = Field(description="What the text claims") + is_valid: bool = Field(description="True if the claim is correct within tolerance") + explanation: str = Field(description="One-line working showing the calculation") + + +class StatementsResult(BaseModel): + """All verifiable prose claims found on a page.""" + + statements: list[StatementCheck] = Field(default_factory=list) + + +# --------------------------------------------------------------------------- +# MathAuditorAgent — main entry point, instantiated once at startup +# --------------------------------------------------------------------------- + + +class MathAuditorAgent: + """ + Encapsulates the Ledger Auditor pipeline. + + Instantiated once at app startup with an AppRuntime, which provides + pre-built Model objects and ModelSettings. + """ + + def __init__(self, runtime: AppRuntime) -> None: + fast_model = runtime.fast_model + model_settings = runtime.fast_model_settings + self._runtime = runtime + self._examiner = Agent( + model=fast_model, + deps_type=FolioManifest, + output_type=Requisition, + system_prompt=EXAMINER_SYSTEM_PROMPT, + model_settings=model_settings, + ) + self._figure_extractor = Agent( + model=fast_model, + output_type=FigureExtractionResult, + system_prompt=FIGURE_EXTRACTOR_PROMPT, + model_settings=model_settings, + ) + self._table_analyser = Agent( + model=fast_model, + output_type=TableFormulas, + system_prompt=TABLE_FORMULA_PROMPT, + model_settings=model_settings, + ) + self._statement_verifier = Agent( + model=fast_model, + output_type=StatementsResult, + system_prompt=STATEMENT_VERIFIER_PROMPT, + model_settings=model_settings, + ) + self._summary_agent = Agent( + model=fast_model, + output_type=str, + system_prompt=SUMMARY_PROMPT, + model_settings=model_settings, + ) + self._llm_semaphore = asyncio.Semaphore(10) + + # ------------------------------------------------------------------ + # Round 1: Examine + # ------------------------------------------------------------------ + + async def examine(self, manifest: FolioManifest) -> Requisition: + """Inspect a FolioManifest and declare the Requisition.""" + logger.info( + "[math-auditor-agent] session=%s round=%d examining %d folios", + manifest.session_id, + manifest.round, + manifest.page_count, + ) + + user_prompt = "Examine this folio manifest and declare your requisition:\n" + manifest.model_dump_json() + logger.debug("REQUEST (examine)\n%s", Pretty({"user_prompt": user_prompt})) + + result = await self._examiner.run(user_prompt, deps=manifest) + req = result.output + + logger.debug("RESPONSE (examine)\n%s", Pretty(req.model_dump())) + logger.info( + "[math-auditor-agent] session=%s requisition: text=%s tables=%s ocr=%s", + manifest.session_id, + req.need_text, + req.need_tables, + req.need_ocr, + ) + return req + + # ------------------------------------------------------------------ + # Round 2: Deliberate (deterministic-first pipeline) + # ------------------------------------------------------------------ + + async def audit(self, evidence: Evidence, tolerance: Decimal = Decimal("0.01")) -> Verdict: + """ + Audit the evidence using a deterministic-first pipeline: + + 1. Run ArithmeticScanner on every folio (no LLM) + 2. Extract named figures per-page with fast model + 3. Run FigureTracker cross-page consistency check (no LLM) + 4. Generate human summary with fast model + 5. Assemble Verdict + """ + return await self._audit_inner(evidence, tolerance) + + async def _audit_inner( + self, + evidence: Evidence, + tolerance: Decimal, + ) -> Verdict: + logger.info( + "[math-auditor-agent] session=%s round=%d auditing %d folios (final=%s)", + evidence.session_id, + evidence.round, + len(evidence.folios), + evidence.final_round, + ) + + all_discrepancies: list[Discrepancy] = [] + pages_examined: list[int] = [] + figure_tracker = FigureTracker(tolerance=tolerance) + + # Step 1: Arithmetic scanning (deterministic, instant) + arithmetic_scanner = ArithmeticScanner(tolerance=tolerance) + for folio in evidence.folios: + pages_examined.append(folio.page) + text = folio.readable_text + if text and text.strip(): + results = arithmetic_scanner.scan(folio.page, text) + all_discrepancies.extend(results) + logger.debug( + "TOOL (scan_arithmetic)\nArgs: %s\nResult: %s", + Pretty({"page": folio.page, "text_length": len(text)}), + Pretty([d.model_dump() for d in results]), + ) + + # Step 2: Parallel LLM calls — formula inference + figure extraction + # These are independent per-page so we fire them all concurrently. + formula_evaluator = FormulaEvaluator(tolerance=tolerance) + folios_with_text = [f for f in evidence.folios if f.readable_text.strip()] + + # Collect all tables as (page, csv) pairs for formula inference + table_tasks: list[tuple[int, str]] = [] + for folio in evidence.folios: + if folio.tables: + for table_csv in folio.tables: + table_tasks.append((folio.page, table_csv)) + + logger.info( + "[math-auditor-agent] session=%s step 2: %d formula + %d figure LLM calls (parallel)", + evidence.session_id, + len(table_tasks), + len(folios_with_text), + ) + + # Fire all LLM calls concurrently (bounded by _llm_semaphore) + formula_coros = [self._throttled(self._infer_formulas(csv)) for _, csv in table_tasks] + figure_coros = [self._throttled(self._extract_figures_for_page(f)) for f in folios_with_text] + statement_coros = [self._throttled(self._verify_statements(f)) for f in folios_with_text] + all_results = await asyncio.gather( + *formula_coros, + *figure_coros, + *statement_coros, + return_exceptions=True, + ) + + n_formulas = len(table_tasks) + n_figures = len(folios_with_text) + + # Process formula results + for i, (page, table_csv) in enumerate(table_tasks): + result = all_results[i] + if isinstance(result, BaseException): + logger.warning("[math-auditor-agent] formula inference failed for page %d: %s", page, result) + continue + assert isinstance(result, TableFormulas) + formulas = result + if not formulas.formulas: + logger.info("[math-auditor-agent] page %d: no verifiable formulas found", page) + continue + for fc in formulas.formulas: + checked = formula_evaluator.evaluate( + page=page, + table_csv=table_csv, + formula=fc.formula, + scope=fc.scope, + description=fc.description, + row_range=fc.row_range, + target_row=fc.target_row, + target_col=fc.target_col, + ) + all_discrepancies.extend(checked) + logger.debug( + "TOOL (check_formula)\nArgs: %s\nResult: %s", + Pretty({"page": page, "formula": fc.formula, "scope": fc.scope, "description": fc.description}), + Pretty([d.model_dump() for d in checked]), + ) + + # Process figure results + for i, folio in enumerate(folios_with_text): + result = all_results[n_formulas + i] + if isinstance(result, BaseException): + logger.warning("[math-auditor-agent] figure extraction failed for page %d: %s", folio.page, result) + continue + assert isinstance(result, list) + for fig, page in result: + try: + decimal_value = Decimal(fig.value.replace(",", "").strip()) + except (InvalidOperation, ValueError): + logger.warning( + "[math-auditor-agent] skipping figure %r on page %d: non-numeric value %r", + fig.label, + page, + fig.value, + ) + continue + figure_tracker.record( + label=fig.label, + value=decimal_value, + page=page, + raw=fig.raw, + ) + + # Process statement verification results + for i, folio in enumerate(folios_with_text): + result = all_results[n_formulas + n_figures + i] + if isinstance(result, BaseException): + logger.warning("[math-auditor-agent] statement verification failed for page %d: %s", folio.page, result) + continue + assert isinstance(result, StatementsResult) + stmts = result + for sc in stmts.statements: + if not sc.is_valid: + all_discrepancies.append( + Discrepancy( + page=folio.page, + kind=DiscrepancyKind.STATEMENT, + severity=Severity.ERROR, + description=f"{sc.claim}: {sc.explanation}", + stated=sc.actual_claim, + expected=sc.expected_result, + context=sc.claim, + ) + ) + logger.debug( + "TOOL (verify_statement)\nArgs: %s\nResult: %s", + Pretty({"page": folio.page, "claim": sc.claim}), + Pretty(sc.model_dump()), + ) + + logger.info( + "[math-auditor-agent] session=%s step 2 complete: %d figures registered", + evidence.session_id, + figure_tracker.entry_count, + ) + + # Step 3: Cross-page consistency — deterministic + consistency_discrepancies = figure_tracker.conflicts() + all_discrepancies.extend(consistency_discrepancies) + if consistency_discrepancies: + logger.debug( + "TOOL (check_figure_consistency)\nResult: %s", + Pretty([d.model_dump() for d in consistency_discrepancies]), + ) + + # Step 4: Summary — fast model, small payload + # Collect verification stats for the summary + total_tables = sum(len(f.tables) for f in evidence.folios if f.tables) + total_formulas_checked = sum(len(r.formulas) for r in all_results[:n_formulas] if isinstance(r, TableFormulas)) + total_statements_checked = sum( + len(r.statements) for r in all_results[n_formulas + n_figures :] if isinstance(r, StatementsResult) + ) + verification_stats = ( + f"Verified: {len(pages_examined)} pages, {total_tables} tables " + f"({total_formulas_checked} formulas), " + f"{figure_tracker.entry_count} figures tracked, " + f"{total_statements_checked} prose claims checked." + ) + + logger.info( + "[math-auditor-agent] session=%s step 4: generating summary (%d discrepancies)", + evidence.session_id, + len(all_discrepancies), + ) + pages_examined.sort() + summary = await self._generate_summary( + all_discrepancies, + pages_examined, + evidence.unauditable_pages, + verification_stats, + ) + + # Step 5: Assemble Verdict + error_count = sum(1 for d in all_discrepancies if d.severity == Severity.ERROR) + verdict = Verdict( + session_id=evidence.session_id, + discrepancies=all_discrepancies, + pages_examined=pages_examined, + rounds_taken=evidence.round, + summary=summary, + clean=error_count == 0, + unauditable_pages=evidence.unauditable_pages, + ) + + logger.debug("RESPONSE (deliberate)\n%s", Pretty(verdict.model_dump())) + logger.info( + "[math-auditor-agent] session=%s verdict: %d errors, %d warnings, clean=%s", + evidence.session_id, + verdict.error_count, + verdict.warning_count, + verdict.clean, + ) + return verdict + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + async def _throttled[T](self, coro: Coroutine[Any, Any, T]) -> T: + """Wrap a coroutine with the LLM concurrency semaphore.""" + async with self._llm_semaphore: + return await coro + + async def _infer_formulas(self, table_csv: str) -> TableFormulas: + """Ask the fast model to infer verifiable formulas from a CSV table.""" + try: + result = await self._table_analyser.run(f"CSV table:\n{table_csv}") + formulas = result.output + except AgentRunError: + logger.warning("[math-auditor-agent] formula inference failed, skipping table", exc_info=True) + formulas = TableFormulas(formulas=[]) + + logger.debug( + "TOOL (infer_formulas)\nArgs: %s\nResult: %s", + Pretty({"table_csv": table_csv[:300]}), + Pretty(formulas.model_dump()), + ) + return formulas + + async def _verify_statements( + self, + folio: Folio, + ) -> StatementsResult: + """Ask the fast model to find and verify prose claims on a page.""" + text = folio.readable_text + if not text or not text.strip(): + return StatementsResult(statements=[]) + + # Build context: page text + any table CSVs + prompt = f"Page {folio.page + 1} text:\n{text}" + if folio.tables: + prompt += "\n\nTable data on this page:\n" + for i, csv in enumerate(folio.tables): + prompt += f"\nTable {i + 1}:\n{csv}" + + try: + result = await self._statement_verifier.run(prompt) + stmts = result.output + except AgentRunError: + logger.warning("[math-auditor-agent] statement verification failed for page %d", folio.page, exc_info=True) + stmts = StatementsResult(statements=[]) + + if stmts.statements: + logger.debug( + "TOOL (verify_statements)\nArgs: %s\nResult: %s", + Pretty({"page": folio.page, "text_length": len(text), "n_tables": len(folio.tables or [])}), + Pretty([s.model_dump() for s in stmts.statements]), + ) + return stmts + + async def _extract_figures_for_page( + self, + folio: Folio, + ) -> list[tuple[ExtractedFigure, int]]: + text = folio.readable_text + if not text or not text.strip(): + return [] + + logger.info("[math-auditor-agent] extracting figures from page %d (%d chars)", folio.page, len(text)) + prompt = f"Page {folio.page + 1} text:\n{text}" + try: + result = await self._figure_extractor.run(prompt) + figures = result.output.figures + except AgentRunError: + logger.warning( + "[math-auditor-agent] figure extraction failed for page %d, skipping", + folio.page, + exc_info=True, + ) + figures = [] + + logger.debug( + "TOOL (extract_figures)\nArgs: %s\nResult: %s", + Pretty({"page": folio.page, "text_length": len(text)}), + Pretty([f.model_dump() for f in figures]), + ) + + return [(fig, folio.page) for fig in figures] + + async def _generate_summary( + self, + discrepancies: list[Discrepancy], + pages_examined: list[int], + unauditable_pages: list[int], + verification_stats: str, + ) -> str: + error_count = sum(1 for d in discrepancies if d.severity == Severity.ERROR) + warning_count = sum(1 for d in discrepancies if d.severity == Severity.WARNING) + + prompt = ( + f"{verification_stats}\n" + f"Errors: {error_count}, Warnings: {warning_count}, " + f"Pages examined: {len(pages_examined)}, " + f"Unauditable pages: {unauditable_pages or 'none'}.\n" + ) + if discrepancies: + prompt += "Discrepancies:\n" + for d in discrepancies: + prompt += f" - [{d.severity}] p{d.page + 1}: {d.description}\n" + + try: + result = await self._summary_agent.run(prompt) + summary = result.output + except AgentRunError: + logger.warning("[math-auditor-agent] summary generation failed, using fallback", exc_info=True) + summary = self._fallback_summary(error_count, warning_count, pages_examined, unauditable_pages) + + logger.debug("RESPONSE (summary)\n%s", Pretty({"summary": summary})) + return summary + + @staticmethod + def _fallback_summary( + error_count: int, + warning_count: int, + pages_examined: list[int], + unauditable_pages: list[int], + ) -> str: + parts = [] + if error_count == 0 and warning_count == 0: + parts.append(f"No mathematical errors found across {len(pages_examined)} pages.") + else: + if error_count: + parts.append(f"Found {error_count} error{'s' if error_count != 1 else ''}.") + if warning_count: + parts.append(f"Found {warning_count} warning{'s' if warning_count != 1 else ''}.") + if unauditable_pages: + parts.append( + f"Pages {', '.join(str(p + 1) for p in unauditable_pages)} could not be audited (OCR unavailable)." + ) + return " ".join(parts) diff --git a/engine/src/stirling/agents/ledger/prompts.py b/engine/src/stirling/agents/ledger/prompts.py new file mode 100644 index 0000000000..78e97c4f9a --- /dev/null +++ b/engine/src/stirling/agents/ledger/prompts.py @@ -0,0 +1,147 @@ +""" +Ledger Auditor — system prompts. + +One prompt per role; keep them short and directive. Each agent is a +specialist with a narrow remit, not a general assistant. +""" + +EXAMINER_SYSTEM_PROMPT = """\ +You are the Examiner, the first stage of the Ledger Auditor pipeline. + +You receive a FolioManifest: a list of page types (text / image / mixed) \ +for a PDF document. Your sole task is to declare exactly which pages you \ +need Java to extract content from so that the Auditor can verify the \ +document's mathematics. + +Rules: +- Request BOTH text AND table extraction for every 'text' or 'mixed' page. \ + Tables are critical — the Auditor cannot verify totals without them. \ + Tabula extraction is cheap; missing a table is not. +- Request OCR for any page classified as 'image' or 'mixed' (PDFBox cannot \ + read image-only content). +- Be conservative — if in doubt, request the page. False negatives \ + (missed errors) are worse than false positives (wasted extraction). +- Do not request pages that are clearly decorative (cover pages, blank pages) \ + unless you cannot tell from the manifest alone. +- Return a Requisition with your page lists and a plain-English rationale \ + that will appear in server logs. +""" + +FIGURE_EXTRACTOR_PROMPT = """\ +You are a figure extractor for financial document auditing. + +You receive the text content of a single PDF page. Your task is to \ +identify every significant named numeric figure on the page. + +A "named figure" is a labelled number that could appear elsewhere in \ +the document under the same name — for example: + "Total Revenue: £1,200,000" + "Net Profit $45,000" + "VAT (20%): 240.00" + "Subtotal ......... 3,500" + +For each figure, return: +- label: a normalised name (e.g. "Total Revenue", "Net Profit", "VAT") +- value: the numeric value as a plain decimal string (e.g. "1200000") +- raw: the original text as it appears in the document + +Rules: +- Only extract figures that have a clear label/name attached. +- Do not extract bare numbers without context. +- Strip currency symbols and thousands separators from value. +- If a figure appears multiple times on the same page, extract each. +- Return an empty list if no named figures are found. +- Be precise — do not invent figures that are not in the text. +""" + +TABLE_FORMULA_PROMPT = """\ +You are a table formula analyser for financial document auditing. + +You receive a CSV table extracted from a PDF. Your task is to identify \ +every verifiable mathematical relationship between cells. + +Relationships fall into three scopes: + +1. "each_row" — a formula that should hold for every data row. + Example: "col3 = col1 * col2" (Line Total = Qty × Unit Price) + +2. "column_total" — a total row where cells = sum of the column above. + Example: a Subtotal row where each cell sums the column. + +3. "single_cell" — one specific cell computed from others. + Example: "cell(5,3) = cell(4,3) * 0.1" (Tax = Subtotal × 10%) + +Formula syntax (use exactly this): + - Column references: col0, col1, col2 ... (0-indexed) + - Cell references: cell(row, col) — 0-indexed, header is row 0 + - Operators: + - * / + - sum(colN, start-end) — sum of colN from row start to row end inclusive + - Decimal numbers: 0.1, 100, etc. + +Rules: +- Row 0 is the header. First data row is row 1. +- Include the left-hand side: "col3 = col1 * col2" not just "col1 * col2" +- For column_total scope, set target_row to the total row index. \ + Set target_col to a specific column or null to check all numeric columns. +- For each_row scope, set row_range to the data rows (exclude header \ + and total rows). +- Only return formulas you are confident about. Skip columns/rows \ + where the relationship is unclear. +- Return an empty list if the table has no verifiable math. +""" + +STATEMENT_VERIFIER_PROMPT = """\ +You are a statement verifier for financial document auditing. + +You receive the text of a single PDF page, plus any table data from \ +that page. Your task is to find prose claims that make mathematical \ +assertions, and verify whether each claim is correct. + +A "verifiable claim" is a sentence that states a mathematical fact \ +about numbers present on the page or derivable from the data. Examples: + - "Revenue grew 15% year-over-year" + - "Costs decreased month on month" + - "Department A represents 40% of total spend" + - "Net margin improved to 12.4%" + - "Average transaction value was $250" + +For each claim you find: +1. Identify the numbers referenced in the claim +2. Perform the calculation yourself using the data on the page +3. Compare your result to what the claim states +4. Determine if the claim is valid (within reasonable rounding) + +Return: +- claim: the exact text of the claim +- verification: the type — "percentage_change", "comparison", \ + "ratio", "trend", "average", or "other" +- values_referenced: the specific numbers used in your check +- expected_result: what the calculation actually yields +- actual_claim: what the text claims +- is_valid: true if the claim is correct within 1% tolerance +- explanation: show your working, one line + +Rules: +- Only check claims that can be verified from data on this page. +- If a claim references data not on the page, skip it. +- "Decreased month on month" means EVERY consecutive pair decreased. +- Percentage claims allow 1% absolute tolerance (14.8% ≈ 15%). +- Return an empty list if there are no verifiable claims. +- Do not fabricate claims that are not in the text. +""" + +SUMMARY_PROMPT = """\ +You are a summary writer for a PDF math audit tool. + +You receive a list of discrepancies (errors and warnings) found in a \ +document, plus coverage statistics and a breakdown of what was verified. \ +Write a two to three sentence summary suitable for an end user. + +Rules: +- Start with what was verified: e.g. "Audited 6 pages: checked 4 tables \ + (12 formulas), scanned 6 pages for arithmetic, extracted 20 figures \ + for cross-page consistency, and verified 3 prose claims." +- Then state the outcome: errors found or clean. +- Mention unauditable pages if any exist. +- Be concise and factual. Do not repeat individual discrepancy details. +""" diff --git a/engine/src/stirling/agents/ledger/validators/__init__.py b/engine/src/stirling/agents/ledger/validators/__init__.py new file mode 100644 index 0000000000..d4ba9dc1cd --- /dev/null +++ b/engine/src/stirling/agents/ledger/validators/__init__.py @@ -0,0 +1,5 @@ +from .arithmetic import ArithmeticScanner +from .figures import FigureTracker +from .formula import FormulaEvaluator + +__all__ = ["ArithmeticScanner", "FigureTracker", "FormulaEvaluator"] diff --git a/engine/src/stirling/agents/ledger/validators/_parsing.py b/engine/src/stirling/agents/ledger/validators/_parsing.py new file mode 100644 index 0000000000..52a06271c3 --- /dev/null +++ b/engine/src/stirling/agents/ledger/validators/_parsing.py @@ -0,0 +1,31 @@ +"""Shared parsing helpers for ledger validators.""" + +from __future__ import annotations + +import csv +import io +import re +from decimal import Decimal, InvalidOperation + +# Strip common currency symbols and thousands separators before parsing. +STRIP_PATTERN = re.compile(r"[£$€¥,\s]") + + +def to_decimal(raw: str) -> Decimal | None: + """Parse a cell value to Decimal, returning None for non-numeric cells.""" + cleaned = STRIP_PATTERN.sub("", raw.strip()) + if not cleaned or cleaned in {"-", "—", "n/a", "N/A", "na", "NA"}: + return None + # Handle parenthesised negatives: (123.45) → -123.45 + if cleaned.startswith("(") and cleaned.endswith(")"): + cleaned = "-" + cleaned[1:-1] + try: + return Decimal(cleaned) + except InvalidOperation: + return None + + +def parse_csv(table_csv: str) -> list[list[str]]: + """Parse a CSV string into rows, dropping completely empty rows.""" + reader = csv.reader(io.StringIO(table_csv.strip())) + return [row for row in reader if any(cell.strip() for cell in row)] diff --git a/engine/src/stirling/agents/ledger/validators/arithmetic.py b/engine/src/stirling/agents/ledger/validators/arithmetic.py new file mode 100644 index 0000000000..31856ffa8c --- /dev/null +++ b/engine/src/stirling/agents/ledger/validators/arithmetic.py @@ -0,0 +1,152 @@ +""" +ArithmeticScanner — finds and verifies inline arithmetic expressions in text. + +Targets patterns commonly found in financial documents: + "100 + 200 + 150 = 450" + "Total: 1,250 (500 + 400 + 350)" + "Net profit of £1,200 (£2,000 revenue less £800 costs)" + +All arithmetic is performed in Decimal. The scanner does not use an LLM — +it is a deterministic regex-and-eval pipeline. +""" + +from __future__ import annotations + +import logging +import re +from decimal import Decimal + +from stirling.contracts.ledger import Discrepancy, DiscrepancyKind, Severity + +from ._parsing import STRIP_PATTERN as _STRIP +from ._parsing import to_decimal as _to_decimal + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Regex patterns +# --------------------------------------------------------------------------- + +# Currency / number token: optional sign, optional currency symbol, +# digits with optional thousands separator and decimal point. +_NUM = r"[£$€¥]?-?[\d,]+(?:\.\d+)?" + +# "A + B + C = D" or "A + B + C = D" with arbitrary spacing +_EQUALS_EXPR = re.compile( + rf"({_NUM}(?:\s*[+\-]\s*{_NUM})+)\s*=\s*({_NUM})", + re.IGNORECASE, +) + +# "Total: X (A + B + C)" — the total comes before the addends +_TOTAL_THEN_ADDENDS = re.compile( + rf"(?:total|sum|grand total|subtotal)\s*[:\-]?\s*({_NUM})\s*\(({_NUM}(?:\s*[+\-]\s*{_NUM})+)\)", + re.IGNORECASE, +) + + +def _parse(token: str) -> Decimal | None: + """Parse a regex-matched token to Decimal.""" + return _to_decimal(token) + + +def _eval_expression(expr: str) -> Decimal | None: + """ + Evaluate a simple additive expression of the form A +/- B +/- C ... + Returns None if the expression cannot be parsed. + """ + # Tokenise: split on + or -, keep the operator. + tokens = re.split(r"([+\-])", _STRIP.sub("", expr.strip())) + result = Decimal(0) + operator = "+" + for token in tokens: + token = token.strip() + if not token: + continue # skip empty tokens (e.g. from leading negative) + if token in ("+", "-"): + operator = token + continue + val = _parse(token) + if val is None: + return None + result = result + val if operator == "+" else result - val + return result + + +class ArithmeticScanner: + """ + Scans a block of text for arithmetic expressions and checks them. + + Parameters + ---------- + tolerance: + Maximum absolute difference before an expression is flagged as wrong. + """ + + def __init__(self, tolerance: Decimal = Decimal("0.01")) -> None: + self.tolerance = tolerance + + def scan(self, page: int, text: str) -> list[Discrepancy]: + """ + Find all verifiable arithmetic expressions in *text* and return + a Discrepancy for each one that does not balance within tolerance. + """ + discrepancies: list[Discrepancy] = [] + discrepancies.extend(self._check_equals_expressions(page, text)) + discrepancies.extend(self._check_total_then_addends(page, text)) + return discrepancies + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _check_equals_expressions(self, page: int, text: str) -> list[Discrepancy]: + """Handle patterns like '100 + 200 = 300'.""" + found: list[Discrepancy] = [] + for match in _EQUALS_EXPR.finditer(text): + expr_str = match.group(1) + stated_str = match.group(2) + + computed = _eval_expression(expr_str) + stated = _parse(stated_str) + if computed is None or stated is None: + continue + + if abs(computed - stated) > self.tolerance: + found.append( + Discrepancy( + page=page, + kind=DiscrepancyKind.ARITHMETIC, + severity=Severity.ERROR, + description=f"Arithmetic error: {expr_str.strip()} should equal {computed}, not {stated}", + stated=str(stated), + expected=str(computed), + context=match.group(0), + ) + ) + return found + + def _check_total_then_addends(self, page: int, text: str) -> list[Discrepancy]: + """Handle patterns like 'Total: 450 (100 + 200 + 150)'.""" + found: list[Discrepancy] = [] + for match in _TOTAL_THEN_ADDENDS.finditer(text): + stated_str = match.group(1) + expr_str = match.group(2) + + stated = _parse(stated_str) + computed = _eval_expression(expr_str) + if stated is None or computed is None: + continue + + if abs(computed - stated) > self.tolerance: + found.append( + Discrepancy( + page=page, + kind=DiscrepancyKind.ARITHMETIC, + severity=Severity.ERROR, + description=f"Stated total {stated} does not match addends ({expr_str.strip()} = {computed})", + stated=str(stated), + expected=str(computed), + context=match.group(0), + ) + ) + return found diff --git a/engine/src/stirling/agents/ledger/validators/figures.py b/engine/src/stirling/agents/ledger/validators/figures.py new file mode 100644 index 0000000000..7790c8dfb5 --- /dev/null +++ b/engine/src/stirling/agents/ledger/validators/figures.py @@ -0,0 +1,98 @@ +""" +FigureTracker — cross-page consistency checker for named figures. + +Collects named numeric figures as the auditor encounters them (e.g. +"Total Revenue: £1,200,000") and surfaces any that appear under the same +label but with a different value on another page — a classic symptom of +copy-paste errors or stale data in executive summaries. + +The tracker is intentionally simple: normalise labels, compare values +within tolerance, emit Discrepancy for each conflict. +""" + +from __future__ import annotations + +import logging +import re +from decimal import Decimal + +from pydantic import BaseModel + +from stirling.contracts.ledger import Discrepancy, DiscrepancyKind, Severity + +logger = logging.getLogger(__name__) + + +class FigureRecord(BaseModel): + """A named numeric figure seen on a specific page.""" + + label: str + value: Decimal + page: int + raw: str + + +# Strip punctuation that varies between contexts ("revenue:" vs "revenue —") +_LABEL_NOISE = re.compile(r"[:\-—\s]+") + + +def _normalise_label(label: str) -> str: + return _LABEL_NOISE.sub(" ", label.lower()).strip() + + +class FigureTracker: + """ + Accumulates named figures during an audit and checks them for consistency. + + Typical usage: + tracker = FigureTracker() + tracker.record("Net Profit", Decimal("1200.00"), page=3, raw="£1,200.00") + tracker.record("Net Profit", Decimal("1250.00"), page=7, raw="£1,250.00") + discrepancies = tracker.conflicts() # returns one Discrepancy + """ + + def __init__(self, tolerance: Decimal = Decimal("0.01")) -> None: + self.tolerance = tolerance + self._ledger: dict[str, list[FigureRecord]] = {} + + def record(self, label: str, value: Decimal, page: int, raw: str) -> None: + """Register a named figure sighting.""" + key = _normalise_label(label) + self._ledger.setdefault(key, []).append(FigureRecord(label=key, value=value, page=page, raw=raw)) + + def conflicts(self) -> list[Discrepancy]: + """ + Return a Discrepancy for every label that has sightings whose value + differs from the first-seen (canonical) value by more than tolerance. + + O(n) per label — each record is compared against the canonical only. + """ + discrepancies: list[Discrepancy] = [] + + for label, records in self._ledger.items(): + if len(records) < 2: + continue + canonical = records[0] + for other in records[1:]: + if abs(canonical.value - other.value) > self.tolerance: + discrepancies.append( + Discrepancy( + page=other.page, + kind=DiscrepancyKind.CONSISTENCY, + severity=Severity.WARNING, + description=( + f'"{label}" stated as {canonical.raw} on page' + f" {canonical.page + 1}" + f" but {other.raw} on page {other.page + 1}" + ), + stated=other.raw, + expected=canonical.raw, + context=(f"First seen: page {canonical.page + 1} | Later: page {other.page + 1}"), + ) + ) + + return discrepancies + + @property + def entry_count(self) -> int: + return sum(len(v) for v in self._ledger.values()) diff --git a/engine/src/stirling/agents/ledger/validators/formula.py b/engine/src/stirling/agents/ledger/validators/formula.py new file mode 100644 index 0000000000..fe9ae81c57 --- /dev/null +++ b/engine/src/stirling/agents/ledger/validators/formula.py @@ -0,0 +1,375 @@ +""" +FormulaEvaluator — verifies LLM-inferred formulas against CSV table data. + +Supports a safe expression syntax: + - Column refs: col0, col1, col2 ... + - Cell refs: cell(row, col) + - Operators: + - * / + - Functions: sum(colN, rows start-end) + +All arithmetic is Decimal. No eval(), no arbitrary code execution. +""" + +from __future__ import annotations + +import logging +import re +from decimal import Decimal, InvalidOperation + +from stirling.contracts.ledger import Discrepancy, DiscrepancyKind, Severity + +from ._parsing import parse_csv as _parse_csv +from ._parsing import to_decimal as _to_decimal + +logger = logging.getLogger(__name__) + + +class FormulaEvaluator: + """ + Evaluates formula expressions against parsed CSV rows. + + Formulas use a simple syntax: + "col3 = col1 * col2" — per-row check + "cell(4,3) = sum(col3, 1-3)" — single cell check + """ + + def __init__(self, tolerance: Decimal = Decimal("0.01")) -> None: + self.tolerance = tolerance + + def evaluate( + self, + page: int, + table_csv: str, + formula: str, + scope: str, + description: str, + row_range: list[int] | None = None, + target_row: int | None = None, + target_col: int | None = None, + ) -> list[Discrepancy]: + """ + Evaluate a formula against table data. + + scope: "each_row" | "column_total" | "single_cell" + """ + rows = _parse_csv(table_csv) + if len(rows) < 2: + return [] + + if scope == "each_row": + return self._check_each_row(page, rows, formula, description, row_range) + elif scope == "column_total": + return self._check_column_total(page, rows, formula, description, target_row, target_col) + elif scope == "single_cell": + return self._check_single_cell(page, rows, formula, description, target_row, target_col) + else: + logger.warning("[formula] unknown scope %r, skipping", scope) + return [] + + def _check_each_row( + self, + page: int, + rows: list[list[str]], + formula: str, + description: str, + row_range: list[int] | None, + ) -> list[Discrepancy]: + """Verify formula holds for each data row.""" + discrepancies: list[Discrepancy] = [] + + # Parse "colX = expr" format + parts = formula.split("=", 1) + if len(parts) != 2: + return [] + lhs = parts[0].strip() + rhs = parts[1].strip() + + lhs_col = self._parse_col_ref(lhs) + if lhs_col is None: + return [] + + check_rows = row_range if row_range else list(range(1, len(rows))) + + for row_idx in check_rows: + if row_idx >= len(rows): + continue + row = rows[row_idx] + + stated = self._get_cell(row, lhs_col) + if stated is None: + continue + + computed = self._eval_row_expr(rhs, row, rows) + if computed is None: + continue + + if abs(stated - computed) > self.tolerance: + discrepancies.append( + Discrepancy( + page=page, + kind=DiscrepancyKind.TALLY, + severity=Severity.ERROR, + description=f"{description}: row {row_idx} — stated {stated}, expected {computed}", + stated=str(stated), + expected=str(computed), + context=f"row {row_idx}, {formula}", + ) + ) + + return discrepancies + + def _check_column_total( + self, + page: int, + rows: list[list[str]], + formula: str, + description: str, + target_row: int | None, + target_col: int | None, + ) -> list[Discrepancy]: + """Verify that a total row contains correct column sums.""" + if target_row is None or target_row >= len(rows): + return [] + + discrepancies: list[Discrepancy] = [] + total_row = rows[target_row] + + # Determine which columns to check + cols_to_check: list[int] = [] + if target_col is not None: + cols_to_check = [target_col] + else: + # Check all numeric columns in the total row + cols_to_check = list(range(len(total_row))) + + # Determine addend rows (all rows between header and total row) + addend_rows = list(range(1, target_row)) + + for col in cols_to_check: + stated = self._get_cell(total_row, col) + if stated is None: + continue + + computed = Decimal(0) + has_addends = False + for r_idx in addend_rows: + if r_idx >= len(rows): + continue + val = self._get_cell(rows[r_idx], col) + if val is not None: + computed += val + has_addends = True + + if not has_addends: + continue + + if abs(stated - computed) > self.tolerance: + discrepancies.append( + Discrepancy( + page=page, + kind=DiscrepancyKind.TALLY, + severity=Severity.ERROR, + description=f"{description}: column {col} — stated {stated}, expected {computed}", + stated=str(stated), + expected=str(computed), + context=f"column {col}, total row {target_row}", + ) + ) + + return discrepancies + + def _check_single_cell( + self, + page: int, + rows: list[list[str]], + formula: str, + description: str, + target_row: int | None, + target_col: int | None, + ) -> list[Discrepancy]: + """Verify a single cell formula (e.g. Grand Total = Subtotal + Tax).""" + parts = formula.split("=", 1) + if len(parts) != 2: + return [] + + # Parse target from LHS cell(r,c) if not provided explicitly + if target_row is None or target_col is None: + lhs_match = re.match(r"cell\(\s*(\d+)\s*,\s*(\d+)\s*\)", parts[0].strip()) + if lhs_match: + target_row = int(lhs_match.group(1)) + target_col = int(lhs_match.group(2)) + else: + return [] + + if target_row >= len(rows): + return [] + + rhs = parts[1].strip() + + stated = self._get_cell(rows[target_row], target_col) + if stated is None: + return [] + + computed = self._eval_row_expr(rhs, rows[target_row], rows) + if computed is None: + return [] + + if abs(stated - computed) > self.tolerance: + return [ + Discrepancy( + page=page, + kind=DiscrepancyKind.TALLY, + severity=Severity.ERROR, + description=f"{description}: stated {stated}, expected {computed}", + stated=str(stated), + expected=str(computed), + context=f"cell({target_row},{target_col}), {formula}", + ) + ] + return [] + + # ------------------------------------------------------------------ + # Expression evaluation + # ------------------------------------------------------------------ + + def _eval_row_expr(self, expr: str, row: list[str], all_rows: list[list[str]]) -> Decimal | None: + """ + Evaluate an expression in the context of a specific row. + Supports: colN refs, cell(r,c) refs, +, -, *, / + Also supports: sum(colN, start-end) + """ + # Handle sum() function first + sum_pattern = re.compile(r"sum\(\s*col(\d+)\s*,\s*(\d+)\s*-\s*(\d+)\s*\)") + resolved = expr + for match in sum_pattern.finditer(expr): + col = int(match.group(1)) + start = int(match.group(2)) + end = int(match.group(3)) + total = Decimal(0) + for r_idx in range(start, end + 1): + if r_idx < len(all_rows): + val = self._get_cell(all_rows[r_idx], col) + if val is not None: + total += val + resolved = resolved.replace(match.group(0), str(total)) + + # Handle cell(r, c) references + cell_pattern = re.compile(r"cell\(\s*(\d+)\s*,\s*(\d+)\s*\)") + for match in cell_pattern.finditer(resolved): + r = int(match.group(1)) + c = int(match.group(2)) + if r < len(all_rows): + val = self._get_cell(all_rows[r], c) + if val is not None: + resolved = resolved.replace(match.group(0), str(val)) + else: + return None + else: + return None + + # Replace colN references with values from the current row. + # Use re.sub with word boundaries to avoid col1 corrupting col12. + _failed = False + + def _col_replacer(m: re.Match[str]) -> str: + nonlocal _failed + col_idx = int(m.group(1)) + val = self._get_cell(row, col_idx) + if val is None: + _failed = True + return m.group(0) + return str(val) + + resolved = re.sub(r"\bcol(\d+)\b", _col_replacer, resolved) + if _failed: + return None + + # Evaluate the resulting arithmetic expression safely + return self._safe_eval(resolved) + + def _safe_eval(self, expr: str) -> Decimal | None: + """ + Evaluate a simple arithmetic expression containing only + numbers and +, -, *, / operators. Respects standard operator + precedence (* and / bind tighter than + and -). No eval(). + """ + try: + raw = re.findall(r"\d+(?:\.\d+)?|[+\-*/]", expr.strip()) + if not raw: + return None + + # Build (values, ops) lists, merging a leading '-' or an + # operator-adjacent '-' into the next number token. + values: list[Decimal] = [] + ops: list[str] = [] + i = 0 + while i < len(raw): + tok = raw[i] + if tok in "+-*/" and not values and tok == "-": + # Leading negative: merge with next number + i += 1 + if i >= len(raw): + return None + values.append(Decimal("-" + raw[i])) + elif tok in "+-*/": + # Operator followed by '-' → negative operand + if ( + tok in "+-*/" + and i + 1 < len(raw) + and raw[i + 1] == "-" + and i + 2 < len(raw) + and raw[i + 2] not in "+-*/" + ): + ops.append(tok) + values.append(Decimal("-" + raw[i + 2])) + i += 2 # skip the '-' and the number + else: + ops.append(tok) + else: + values.append(Decimal(tok)) + i += 1 + + if not values: + return None + + # Pass 1: evaluate * and / + j = 0 + while j < len(ops): + if ops[j] in ("*", "/"): + if ops[j] == "*": + values[j] = values[j] * values[j + 1] + else: + if values[j + 1] == 0: + return None + values[j] = values[j] / values[j + 1] + values.pop(j + 1) + ops.pop(j) + else: + j += 1 + + # Pass 2: evaluate + and - + result = values[0] + for j, op in enumerate(ops): + if op == "+": + result += values[j + 1] + elif op == "-": + result -= values[j + 1] + + return result + except (InvalidOperation, IndexError, ValueError): + return None + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + @staticmethod + def _parse_col_ref(ref: str) -> int | None: + match = re.match(r"col(\d+)", ref.strip()) + return int(match.group(1)) if match else None + + @staticmethod + def _get_cell(row: list[str], col: int) -> Decimal | None: + if col >= len(row): + return None + return _to_decimal(row[col]) diff --git a/engine/src/stirling/agents/orchestrator.py b/engine/src/stirling/agents/orchestrator.py index 23919a02de..0596dd3b68 100644 --- a/engine/src/stirling/agents/orchestrator.py +++ b/engine/src/stirling/agents/orchestrator.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import assert_never from pydantic_ai import Agent from pydantic_ai.output import ToolOutput @@ -13,15 +14,20 @@ from stirling.agents.user_spec import UserSpecAgent from stirling.contracts import ( AgentDraftRequest, AgentDraftWorkflowResponse, + ExtractedTextArtifact, OrchestratorRequest, OrchestratorResponse, PdfEditRequest, PdfEditResponse, PdfQuestionRequest, PdfQuestionResponse, + SupportedCapability, + ToolOperationStep, UnsupportedCapabilityResponse, + format_conversation_history, ) -from stirling.contracts.form_fill import KnowledgeUpdateResponse +from stirling.contracts.pdf_edit import EditPlanResponse +from stirling.models.agent_tool_models import AgentToolId, MathAuditorAgentParams from stirling.services import AppRuntime @@ -57,6 +63,14 @@ class OrchestratorAgent: name="delegate_user_spec", description="Delegate requests to create or revise a user agent spec and return the draft result.", ), + ToolOutput( + self.math_auditor_agent, + name="math_auditor_agent", + description=( + "Delegate requests to check arithmetic, validate table totals, " + "audit financial calculations, or verify mathematical accuracy in PDFs." + ), + ), ToolOutput( self.unsupported_capability, name="unsupported_capability", @@ -67,44 +81,91 @@ class OrchestratorAgent: system_prompt=( "You are the top-level orchestrator. " "Choose exactly one output function that best handles the request. " - "Use delegate_pdf_edit for requested PDF modifications. " - "Use delegate_pdf_question for questions about the contents of a PDF. " - "Use delegate_form_fill for requests to extract personal information for form filling. " + "Use delegate_pdf_edit for requested modifications of single or multiple PDFs. " + "Use delegate_pdf_question for questions about PDF contents. " "Use delegate_user_spec for requests to create or define an agent spec. " + "Use math_auditor_agent for requests to check arithmetic, validate " + "table totals, audit financial calculations, or verify math in PDFs. " "Use unsupported_capability only when none of the other outputs fit." ), model_settings=runtime.fast_model_settings, ) async def handle(self, request: OrchestratorRequest) -> OrchestratorResponse: + if request.resume_with is not None: + return await self._resume(request, request.resume_with) result = await self.agent.run( - request.user_message, + self._build_prompt(request), deps=OrchestratorDeps(runtime=self.runtime, request=request), ) return result.output - async def delegate_form_fill(self, ctx: RunContext[OrchestratorDeps]) -> KnowledgeUpdateResponse: - request = ctx.deps.request - return await DocumentExtractorAgent(ctx.deps.runtime).extract_single( - document_text=request.user_message, - user_message=request.user_message, - ) + async def _resume(self, request: OrchestratorRequest, capability: SupportedCapability) -> OrchestratorResponse: + """Fast-path to get back to the correct endpoint without having to call AI.""" + match capability: + case SupportedCapability.PDF_QUESTION: + return await self._run_pdf_question(request) + case SupportedCapability.PDF_EDIT: + return await self._run_pdf_edit(request) + case SupportedCapability.AGENT_DRAFT: + return await self._run_agent_draft(request) + case ( + SupportedCapability.ORCHESTRATE + | SupportedCapability.AGENT_REVISE + | SupportedCapability.AGENT_NEXT_ACTION + | SupportedCapability.MATH_AUDITOR_AGENT + ): + raise ValueError(f"Cannot resume orchestrator with capability: {capability}") + case _ as unreachable: + assert_never(unreachable) async def delegate_pdf_edit(self, ctx: RunContext[OrchestratorDeps]) -> PdfEditResponse: - request = ctx.deps.request - return await PdfEditAgent(ctx.deps.runtime).handle( - PdfEditRequest(user_message=request.user_message, conversation_id=request.conversation_id) + return await self._run_pdf_edit(ctx.deps.request) + + async def _run_pdf_edit(self, request: OrchestratorRequest) -> PdfEditResponse: + return await PdfEditAgent(self.runtime).handle( + PdfEditRequest( + user_message=request.user_message, + file_names=request.file_names, + conversation_history=request.conversation_history, + ) ) async def delegate_pdf_question(self, ctx: RunContext[OrchestratorDeps]) -> PdfQuestionResponse: - request = ctx.deps.request - return await PdfQuestionAgent(ctx.deps.runtime).handle( - PdfQuestionRequest(question=request.user_message, conversation_id=request.conversation_id) + return await self._run_pdf_question(ctx.deps.request) + + async def _run_pdf_question(self, request: OrchestratorRequest) -> PdfQuestionResponse: + extracted_text = self._get_extracted_text_artifact(request) + return await PdfQuestionAgent(self.runtime).handle( + PdfQuestionRequest( + question=request.user_message, + file_names=request.file_names, + page_text=extracted_text.files if extracted_text is not None else [], + conversation_history=request.conversation_history, + ) ) async def delegate_user_spec(self, ctx: RunContext[OrchestratorDeps]) -> AgentDraftWorkflowResponse: - request = ctx.deps.request - return await UserSpecAgent(ctx.deps.runtime).draft(AgentDraftRequest(user_message=request.user_message)) + return await self._run_agent_draft(ctx.deps.request) + + async def _run_agent_draft(self, request: OrchestratorRequest) -> AgentDraftWorkflowResponse: + return await UserSpecAgent(self.runtime).draft( + AgentDraftRequest( + user_message=request.user_message, + conversation_history=request.conversation_history, + ) + ) + + async def math_auditor_agent(self, ctx: RunContext[OrchestratorDeps]) -> EditPlanResponse: + return EditPlanResponse( + summary="Validate mathematical calculations in the document.", + steps=[ + ToolOperationStep( + tool=AgentToolId.MATH_AUDITOR_AGENT, + parameters=MathAuditorAgentParams(), + ) + ], + ) async def unsupported_capability( self, @@ -113,3 +174,34 @@ class OrchestratorAgent: message: str, ) -> UnsupportedCapabilityResponse: return UnsupportedCapabilityResponse(capability=capability, message=message) + + def _get_extracted_text_artifact(self, request: OrchestratorRequest) -> ExtractedTextArtifact | None: + for artifact in request.artifacts: + if isinstance(artifact, ExtractedTextArtifact): + return artifact + return None + + def _build_prompt(self, request: OrchestratorRequest) -> str: + artifact_summary = self._describe_artifacts(request) + file_names = ", ".join(request.file_names) if request.file_names else "Unknown files" + history = format_conversation_history(request.conversation_history) + return ( + f"Conversation history:\n{history}\n" + f"User message: {request.user_message}\n" + f"Files: {file_names}\n" + f"Available artifacts:\n{artifact_summary}" + ) + + def _describe_artifacts(self, request: OrchestratorRequest) -> str: + if not request.artifacts: + return "- none" + + descriptions: list[str] = [] + for artifact in request.artifacts: + if isinstance(artifact, ExtractedTextArtifact): + total_pages = sum(len(f.pages) for f in artifact.files) + file_names = [f.file_name for f in artifact.files] + descriptions.append(f"- extracted_text: {total_pages} pages from {file_names}") + continue + descriptions.append("- unknown artifact") + return "\n".join(descriptions) diff --git a/engine/src/stirling/agents/pdf_edit.py b/engine/src/stirling/agents/pdf_edit.py index 1ba014dec4..40b1365783 100644 --- a/engine/src/stirling/agents/pdf_edit.py +++ b/engine/src/stirling/agents/pdf_edit.py @@ -13,14 +13,15 @@ from stirling.contracts import ( PdfEditRequest, PdfEditResponse, ToolOperationStep, + format_conversation_history, ) -from stirling.models import OPERATIONS, ApiModel, OperationId, ParamToolModel +from stirling.models import OPERATIONS, ApiModel, ParamToolModel, ToolEndpoint from stirling.services import AppRuntime class PdfEditPlanSelection(ApiModel): outcome: Literal["plan"] = "plan" - operations: list[OperationId] = Field(min_length=1) + operations: list[ToolEndpoint] = Field(min_length=1) summary: str rationale: str | None = None @@ -41,7 +42,7 @@ class PdfEditParameterSelector: async def select( self, request: PdfEditRequest, - operation_plan: list[OperationId], + operation_plan: list[ToolEndpoint], operation_index: int, generated_steps: list[ToolOperationStep], ) -> ParamToolModel: @@ -51,7 +52,7 @@ class PdfEditParameterSelector: self._build_parameter_prompt(request, operation_plan, operation_index, generated_steps), output_type=NativeOutput(parameter_model), instructions=( - f"Generate only the parameters for the PDF operation `{operation_id.value}`. " + f"Generate only the parameters for the PDF operation `{operation_id.name}`. " "Do not include fields from any other operation." ), ) @@ -60,12 +61,12 @@ class PdfEditParameterSelector: def _build_parameter_prompt( self, request: PdfEditRequest, - operation_plan: list[OperationId], + operation_plan: list[ToolEndpoint], operation_index: int, generated_steps: list[ToolOperationStep], ) -> str: operation_id = operation_plan[operation_index] - operation_list = ", ".join(operation.value for operation in operation_plan) + operation_list = ", ".join(operation.name for operation in operation_plan) file_names = ", ".join(request.file_names) if request.file_names else "No file names were provided." generated_steps_text = ( "\n".join( @@ -79,7 +80,7 @@ class PdfEditParameterSelector: f"Files: {file_names}\n" f"Operation plan: {operation_list}\n" f"Selected operation index: {operation_index + 1} of {len(operation_plan)}\n" - f"Selected operation: {operation_id.value}\n" + f"Selected operation: {operation_id.name}\n" f"Already generated steps:\n{generated_steps_text}\n" "Return only the parameter object for the selected operation." ) @@ -146,6 +147,7 @@ class PdfEditAgent: def _build_selection_prompt(self, request: PdfEditRequest) -> str: file_names = ", ".join(request.file_names) if request.file_names else "No file names were provided." return ( + f"Conversation history:\n{format_conversation_history(request.conversation_history)}\n" f"User request: {request.user_message}\n" f"Files: {file_names}\n" f"Supported operations: {self._supported_operations_prompt()}\n" @@ -153,4 +155,4 @@ class PdfEditAgent: ) def _supported_operations_prompt(self) -> str: - return ", ".join(operation_id.value for operation_id in self.supported_operations) + return ", ".join(f"{op.name} ({op.value})" for op in self.supported_operations) diff --git a/engine/src/stirling/agents/pdf_questions.py b/engine/src/stirling/agents/pdf_questions.py index b7ca33ac9a..c646159da8 100644 --- a/engine/src/stirling/agents/pdf_questions.py +++ b/engine/src/stirling/agents/pdf_questions.py @@ -4,18 +4,26 @@ from pydantic_ai import Agent from pydantic_ai.output import NativeOutput from stirling.contracts import ( + ExtractedFileText, + NeedContentFileRequest, + PdfContentType, PdfQuestionAnswerResponse, - PdfQuestionNeedTextResponse, + PdfQuestionNeedContentResponse, PdfQuestionNotFoundResponse, PdfQuestionRequest, PdfQuestionResponse, + format_conversation_history, ) from stirling.services import AppRuntime class PdfQuestionAgent: + DEFAULT_MAX_PAGES = 12 + DEFAULT_MAX_CHARACTERS = 24_000 + def __init__(self, runtime: AppRuntime) -> None: self.runtime = runtime + rag = runtime.rag_capability self.agent = Agent( model=runtime.smart_model, output_type=NativeOutput( @@ -25,18 +33,29 @@ class PdfQuestionAgent: ] ), system_prompt=( - "Answer questions about a PDF using only the extracted text provided in the prompt. " + "Answer questions about PDFs using only the extracted page text provided in the prompt. " "Do not guess or use outside knowledge. " "If the answer is not supported by the provided text, return not_found. " - "When answering, include a short list of evidence snippets copied from the provided text." + "When answering, include a short list of evidence snippets with their page numbers." ), + instructions=rag.instructions, + toolsets=[rag.toolset], model_settings=runtime.smart_model_settings, ) async def handle(self, request: PdfQuestionRequest) -> PdfQuestionResponse: - if not request.extracted_text.strip(): - return PdfQuestionNeedTextResponse( - reason="No extracted PDF text was provided, so the question cannot be answered yet." + if not self._has_page_text(request.page_text): + return PdfQuestionNeedContentResponse( + reason="No extracted PDF page text was provided, so the question cannot be answered yet.", + files=[ + NeedContentFileRequest( + file_name=file_name, + content_types=[PdfContentType.PAGE_TEXT], + ) + for file_name in request.file_names + ], + max_pages=self.DEFAULT_MAX_PAGES, + max_characters=self.DEFAULT_MAX_CHARACTERS, ) return await self._run_answer_agent(request) @@ -45,5 +64,20 @@ class PdfQuestionAgent: return result.output def _build_prompt(self, request: PdfQuestionRequest) -> str: - file_name = request.file_name or "Unknown file" - return f"File: {file_name}\nQuestion: {request.question}\nExtracted text:\n{request.extracted_text}" + file_names = ", ".join(request.file_names) if request.file_names else "Unknown files" + sections = [ + f"[File: {file_text.file_name}, Page {selection.page_number or '?'}]\n{selection.text}" + for file_text in request.page_text + for selection in file_text.pages + ] + pages = "\n\n".join(sections) + history = format_conversation_history(request.conversation_history) + return ( + f"Conversation history:\n{history}\n" + f"Files: {file_names}\n" + f"Question: {request.question}\n" + f"Extracted page text:\n{pages}" + ) + + def _has_page_text(self, page_text: list[ExtractedFileText]) -> bool: + return any(selection.text.strip() for file_text in page_text for selection in file_text.pages) diff --git a/engine/src/stirling/agents/user_spec.py b/engine/src/stirling/agents/user_spec.py index d30b5e53d5..40a028aa1b 100644 --- a/engine/src/stirling/agents/user_spec.py +++ b/engine/src/stirling/agents/user_spec.py @@ -18,6 +18,7 @@ from stirling.contracts import ( EditClarificationRequest, EditPlanResponse, PdfEditRequest, + format_conversation_history, ) from stirling.models import ApiModel from stirling.services import AppRuntime @@ -45,14 +46,15 @@ class UserSpecAgent: ) async def draft(self, request: AgentDraftRequest) -> AgentDraftWorkflowResponse: - edit_plan = await self._build_edit_plan(request.user_message) + edit_plan = await self._build_edit_plan(request.user_message, request.conversation_history) if not isinstance(edit_plan, EditPlanResponse): return edit_plan return AgentDraftResponse(draft=await self._run_draft_agent(request, edit_plan)) async def revise(self, request: AgentRevisionRequest) -> AgentRevisionWorkflowResponse: edit_plan = await self._build_edit_plan( - f"Current objective: {request.current_draft.objective}\nRevision request: {request.user_message}" + f"Current objective: {request.current_draft.objective}\nRevision request: {request.user_message}", + request.conversation_history, ) if not isinstance(edit_plan, EditPlanResponse): return edit_plan @@ -80,7 +82,7 @@ class UserSpecAgent: def _build_draft_prompt(self, request: AgentDraftRequest, edit_plan: EditPlanResponse) -> str: return ( f"User request:\n{request.user_message}\n\n" - f"Conversation history:\n{self._format_conversation_history(request.conversation_history)}\n\n" + f"Conversation history:\n{format_conversation_history(request.conversation_history)}\n\n" f"Edit plan summary:\n{edit_plan.summary}\n\n" f"Edit plan rationale:\n{edit_plan.rationale or 'None'}\n\n" f"Edit plan steps:\n{edit_plan.model_dump_json(indent=2)}" @@ -89,20 +91,18 @@ class UserSpecAgent: def _build_revision_prompt(self, request: AgentRevisionRequest, edit_plan: EditPlanResponse) -> str: return ( f"Revision request:\n{request.user_message}\n\n" - f"Conversation history:\n{self._format_conversation_history(request.conversation_history)}\n\n" + f"Conversation history:\n{format_conversation_history(request.conversation_history)}\n\n" f"Current draft:\n{request.current_draft.model_dump_json(indent=2)}\n\n" f"Edit plan summary:\n{edit_plan.summary}\n\n" f"Edit plan rationale:\n{edit_plan.rationale or 'None'}\n\n" f"Edit plan steps:\n{edit_plan.model_dump_json(indent=2)}" ) - def _format_conversation_history(self, conversation_history: list[ConversationMessage]) -> str: - if not conversation_history: - return "None" - return "\n".join(f"- {message.role}: {message.content}" for message in conversation_history) - async def _build_edit_plan( self, user_message: str, + conversation_history: list[ConversationMessage], ) -> EditPlanResponse | EditClarificationRequest | EditCannotDoResponse: - return await self.pdf_edit_agent.handle(PdfEditRequest(user_message=user_message)) + return await self.pdf_edit_agent.handle( + PdfEditRequest(user_message=user_message, conversation_history=conversation_history) + ) diff --git a/engine/src/stirling/api/app.py b/engine/src/stirling/api/app.py index 5273403c2f..ed1ccfc246 100644 --- a/engine/src/stirling/api/app.py +++ b/engine/src/stirling/api/app.py @@ -4,6 +4,8 @@ from contextlib import asynccontextmanager from typing import Annotated from fastapi import Depends, FastAPI +from pydantic_ai import Agent +from pydantic_ai.models.instrumented import InstrumentationSettings from stirling.agents import ( DocumentExtractorAgent, @@ -15,17 +17,21 @@ from stirling.agents import ( PdfQuestionAgent, UserSpecAgent, ) +from stirling.agents.ledger import MathAuditorAgent +from stirling.api.middleware import UserIdMiddleware from stirling.api.routes import ( agent_draft_router, execution_router, form_fill_router, + ledger_router, orchestrator_router, pdf_edit_router, pdf_question_router, + rag_router, ) from stirling.config import AppSettings, load_settings from stirling.contracts import HealthResponse -from stirling.services import build_runtime +from stirling.services import build_runtime, setup_posthog_tracking def _load_startup_settings(fast_api: FastAPI) -> AppSettings: @@ -46,18 +52,27 @@ async def lifespan(fast_api: FastAPI): fast_api.state.pdf_question_agent = PdfQuestionAgent(runtime) fast_api.state.user_spec_agent = UserSpecAgent(runtime) fast_api.state.execution_planning_agent = ExecutionPlanningAgent(runtime) + fast_api.state.math_auditor_agent = MathAuditorAgent(runtime) fast_api.state.form_analyser_agent = FormAnalyserAgent(runtime) fast_api.state.form_filler_agent = FormFillerAgent(runtime) fast_api.state.document_extractor_agent = DocumentExtractorAgent(runtime) + tracer_provider = setup_posthog_tracking(settings) + if tracer_provider: + Agent.instrument_all(InstrumentationSettings(tracer_provider=tracer_provider)) yield + if tracer_provider: + tracer_provider.shutdown() app = FastAPI(title="Stirling AI Engine", lifespan=lifespan, version="0.1.0") +app.add_middleware(UserIdMiddleware) app.include_router(orchestrator_router) app.include_router(pdf_edit_router) app.include_router(pdf_question_router) app.include_router(agent_draft_router) app.include_router(execution_router) +app.include_router(rag_router) +app.include_router(ledger_router) app.include_router(form_fill_router) diff --git a/engine/src/stirling/api/dependencies.py b/engine/src/stirling/api/dependencies.py index e4a5fc2fe1..a500caaae2 100644 --- a/engine/src/stirling/api/dependencies.py +++ b/engine/src/stirling/api/dependencies.py @@ -12,6 +12,8 @@ from stirling.agents import ( PdfQuestionAgent, UserSpecAgent, ) +from stirling.agents.ledger import MathAuditorAgent +from stirling.rag import RagService from stirling.services import AppRuntime @@ -39,6 +41,18 @@ def get_execution_planning_agent(request: Request) -> ExecutionPlanningAgent: return request.app.state.execution_planning_agent +def get_rag_service(request: Request) -> RagService: + return request.app.state.runtime.rag_service + + +def get_rag_embedding_model(request: Request) -> str: + return request.app.state.runtime.settings.rag_embedding_model + + +def get_math_auditor_agent(request: Request) -> MathAuditorAgent: + return request.app.state.math_auditor_agent + + def get_form_analyser_agent(request: Request) -> FormAnalyserAgent: return request.app.state.form_analyser_agent diff --git a/engine/src/stirling/api/middleware.py b/engine/src/stirling/api/middleware.py new file mode 100644 index 0000000000..8d5be8b036 --- /dev/null +++ b/engine/src/stirling/api/middleware.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.requests import Request +from starlette.responses import Response + +from stirling.services.tracking import current_user_id + +_USER_ID_HEADER = "X-User-Id" + + +class UserIdMiddleware(BaseHTTPMiddleware): + """Extract X-User-Id header and set it as the current user for PostHog tracking.""" + + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: + user_id = request.headers.get(_USER_ID_HEADER) + if user_id: + token = current_user_id.set(user_id) + try: + return await call_next(request) + finally: + current_user_id.reset(token) + return await call_next(request) diff --git a/engine/src/stirling/api/routes/__init__.py b/engine/src/stirling/api/routes/__init__.py index bdc50e7128..c751901cac 100644 --- a/engine/src/stirling/api/routes/__init__.py +++ b/engine/src/stirling/api/routes/__init__.py @@ -1,15 +1,19 @@ from .agent_drafts import router as agent_draft_router from .execution import router as execution_router from .form_fill import router as form_fill_router +from .ledger import router as ledger_router from .orchestrator import router as orchestrator_router from .pdf_edit import router as pdf_edit_router from .pdf_questions import router as pdf_question_router +from .rag import router as rag_router __all__ = [ "agent_draft_router", "execution_router", "form_fill_router", + "ledger_router", "orchestrator_router", "pdf_edit_router", "pdf_question_router", + "rag_router", ] diff --git a/engine/src/stirling/api/routes/ledger.py b/engine/src/stirling/api/routes/ledger.py new file mode 100644 index 0000000000..82e220fbf6 --- /dev/null +++ b/engine/src/stirling/api/routes/ledger.py @@ -0,0 +1,60 @@ +""" +Math Auditor Agent (mathAuditorAgent) — FastAPI routes. + +Two internal endpoints, called only by the Java MathAuditorOrchestrator: + + POST /api/v1/ai/math-auditor-agent/examine + Java sends a FolioManifest (cheap page classification). + Python returns a Requisition (what Java must extract). + + POST /api/v1/ai/math-auditor-agent/deliberate + Java sends Evidence (fulfilled extraction results). + Python returns a Verdict directly. +""" + +from __future__ import annotations + +import logging +from decimal import Decimal, InvalidOperation +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, Query + +from stirling.agents.ledger import MathAuditorAgent +from stirling.api.dependencies import get_math_auditor_agent +from stirling.contracts.ledger import ( + Evidence, + FolioManifest, + Requisition, + Verdict, +) + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/v1/ai/math-auditor-agent", tags=["math-auditor-agent"]) + + +@router.post("/examine", response_model=Requisition) +async def examine_endpoint( + manifest: FolioManifest, + agent: Annotated[MathAuditorAgent, Depends(get_math_auditor_agent)], +) -> Requisition: + """Round 1: Java presents a FolioManifest; Python declares its Requisition.""" + return await agent.examine(manifest) + + +@router.post("/deliberate", response_model=Verdict) +async def deliberate_endpoint( + evidence: Evidence, + agent: Annotated[MathAuditorAgent, Depends(get_math_auditor_agent)], + tolerance: str = Query(default="0.01"), +) -> Verdict: + """Round 2: Java presents fulfilled Evidence; Python returns a Verdict.""" + try: + tol = Decimal(tolerance) + if tol < 0: + raise HTTPException(status_code=400, detail="tolerance must be non-negative") + except InvalidOperation: + raise HTTPException(status_code=400, detail=f"Invalid tolerance value: {tolerance!r}") + + return await agent.audit(evidence, tol) diff --git a/engine/src/stirling/api/routes/rag.py b/engine/src/stirling/api/routes/rag.py new file mode 100644 index 0000000000..2eb9acfcc2 --- /dev/null +++ b/engine/src/stirling/api/routes/rag.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from typing import Annotated + +from fastapi import APIRouter, Depends + +from stirling.api.dependencies import get_rag_embedding_model, get_rag_service +from stirling.contracts import ( + RagCollectionsResponse, + RagDeleteCollectionResponse, + RagIndexRequest, + RagIndexResponse, + RagSearchRequest, + RagSearchResponse, + RagSearchResultItem, + RagStatusResponse, +) +from stirling.rag import RagService + +router = APIRouter(prefix="/api/v1/rag", tags=["rag"]) + + +@router.get("/status", response_model=RagStatusResponse) +async def rag_status( + rag: Annotated[RagService, Depends(get_rag_service)], + embedding_model: Annotated[str, Depends(get_rag_embedding_model)], +) -> RagStatusResponse: + collections = await rag.list_collections() + return RagStatusResponse(embedding_model=embedding_model, collections=collections) + + +@router.post("/index", response_model=RagIndexResponse) +async def rag_index( + request: RagIndexRequest, + rag: Annotated[RagService, Depends(get_rag_service)], +) -> RagIndexResponse: + count = await rag.index_text( + collection=request.collection, + text=request.text, + source=request.source, + metadata=request.metadata, + ) + return RagIndexResponse(collection=request.collection, chunks_indexed=count) + + +@router.post("/search", response_model=RagSearchResponse) +async def rag_search( + request: RagSearchRequest, + rag: Annotated[RagService, Depends(get_rag_service)], +) -> RagSearchResponse: + results = await rag.search(query=request.query, collection=request.collection, top_k=request.top_k) + items = [ + RagSearchResultItem( + text=r.document.text, + source=r.document.metadata.get("source", ""), + chunk_id=r.document.metadata.get("chunk_index", ""), + score=r.score, + ) + for r in results + ] + return RagSearchResponse(query=request.query, results=items) + + +@router.get("/collections", response_model=RagCollectionsResponse) +async def rag_collections( + rag: Annotated[RagService, Depends(get_rag_service)], +) -> RagCollectionsResponse: + collections = await rag.list_collections() + return RagCollectionsResponse(collections=collections) + + +@router.delete("/collections/{name}", response_model=RagDeleteCollectionResponse) +async def rag_delete_collection( + name: str, + rag: Annotated[RagService, Depends(get_rag_service)], +) -> RagDeleteCollectionResponse: + await rag.delete_collection(name) + return RagDeleteCollectionResponse(status="deleted", collection=name) diff --git a/engine/src/stirling/config/__init__.py b/engine/src/stirling/config/__init__.py index cb0326172c..c5044a2148 100644 --- a/engine/src/stirling/config/__init__.py +++ b/engine/src/stirling/config/__init__.py @@ -1,8 +1,10 @@ """Configuration models and loaders for the Stirling AI service.""" -from .settings import AppSettings, load_settings +from .settings import ENGINE_ROOT, AppSettings, RagBackend, load_settings __all__ = [ + "ENGINE_ROOT", "AppSettings", + "RagBackend", "load_settings", ] diff --git a/engine/src/stirling/config/settings.py b/engine/src/stirling/config/settings.py index e4ddc81d17..d4e6212283 100644 --- a/engine/src/stirling/config/settings.py +++ b/engine/src/stirling/config/settings.py @@ -1,5 +1,8 @@ from __future__ import annotations +import logging +import logging.handlers +from enum import StrEnum from functools import lru_cache from pathlib import Path @@ -9,18 +12,70 @@ from pydantic_settings import BaseSettings, SettingsConfigDict ENGINE_ROOT = Path(__file__).resolve().parents[3] ENV_FILE = ENGINE_ROOT / ".env" +ENV_LOCAL_FILE = ENGINE_ROOT / ".env.local" + + +class RagBackend(StrEnum): + SQLITE = "sqlite" + PGVECTOR = "pgvector" class AppSettings(BaseSettings): - model_config = SettingsConfigDict(env_file=ENV_FILE, extra="ignore", populate_by_name=True) + model_config = SettingsConfigDict(env_file=(ENV_FILE, ENV_LOCAL_FILE), extra="ignore", populate_by_name=True) smart_model_name: str = Field(validation_alias="STIRLING_SMART_MODEL") fast_model_name: str = Field(validation_alias="STIRLING_FAST_MODEL") smart_model_max_tokens: int = Field(validation_alias="STIRLING_SMART_MODEL_MAX_TOKENS") fast_model_max_tokens: int = Field(validation_alias="STIRLING_FAST_MODEL_MAX_TOKENS") + # RAG settings — always on; the backend picks between embedded sqlite-vec and external pgvector. + rag_backend: RagBackend = Field(validation_alias="STIRLING_RAG_BACKEND") + rag_embedding_model: str = Field(validation_alias="STIRLING_RAG_EMBEDDING_MODEL") + rag_store_path: Path = Field(validation_alias="STIRLING_RAG_STORE_PATH") + rag_pgvector_dsn: str = Field(validation_alias="STIRLING_RAG_PGVECTOR_DSN") + rag_chunk_size: int = Field(validation_alias="STIRLING_RAG_CHUNK_SIZE") + rag_chunk_overlap: int = Field(validation_alias="STIRLING_RAG_CHUNK_OVERLAP") + rag_default_top_k: int = Field(validation_alias="STIRLING_RAG_TOP_K") + + log_level: str = Field(default="INFO", validation_alias="STIRLING_LOG_LEVEL") + log_file: str = Field(default="", validation_alias="STIRLING_LOG_FILE") + + posthog_enabled: bool = Field(validation_alias="STIRLING_POSTHOG_ENABLED") + posthog_api_key: str = Field(validation_alias="STIRLING_POSTHOG_API_KEY") + posthog_host: str = Field(validation_alias="STIRLING_POSTHOG_HOST") + + +def _configure_logging(level_name: str, log_file: str) -> None: + """Configure the ``stirling`` logger hierarchy.""" + level = logging.getLevelNamesMapping().get(level_name.upper()) + if level is None: + logging.getLogger("stirling").warning( + "Unknown STIRLING_LOG_LEVEL %r, defaulting to INFO", + level_name, + ) + level = logging.INFO + + root = logging.getLogger("stirling") + root.setLevel(level) + + if log_file: + log_path = Path(log_file) + log_path.parent.mkdir(parents=True, exist_ok=True) + fh = logging.handlers.TimedRotatingFileHandler( + log_path, + when="midnight", + backupCount=1, + encoding="utf-8", + ) + fh.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s [%(funcName)s] %(message)s")) + fh.setLevel(level) + root.addHandler(fh) + @lru_cache(maxsize=1) def load_settings() -> AppSettings: load_dotenv(ENV_FILE) - return AppSettings.model_validate({}) + load_dotenv(ENV_LOCAL_FILE, override=True) + settings = AppSettings.model_validate({}) + _configure_logging(settings.log_level, settings.log_file) + return settings diff --git a/engine/src/stirling/contracts/__init__.py b/engine/src/stirling/contracts/__init__.py index ba18995d44..aaaf5a173c 100644 --- a/engine/src/stirling/contracts/__init__.py +++ b/engine/src/stirling/contracts/__init__.py @@ -8,7 +8,18 @@ from .agent_drafts import ( AgentRevisionWorkflowResponse, ) from .agent_specs import AgentSpec, AgentSpecStep, AiToolAgentStep -from .common import ConversationMessage, PdfTextSelection, ToolOperationStep +from .common import ( + ArtifactKind, + ConversationMessage, + ExtractedFileText, + PdfContentType, + PdfTextSelection, + StepKind, + SupportedCapability, + ToolOperationStep, + WorkflowOutcome, + format_conversation_history, +) from .execution import ( AgentExecutionRequest, CannotContinueExecutionAction, @@ -41,7 +52,24 @@ from .form_fill import ( ProposedProfile, ) from .health import HealthResponse -from .orchestrator import OrchestratorRequest, OrchestratorResponse, SupportedCapability, UnsupportedCapabilityResponse +from .ledger import ( + Discrepancy, + DiscrepancyKind, + Evidence, + Folio, + FolioManifest, + FolioType, + Requisition, + Severity, + Verdict, +) +from .orchestrator import ( + ExtractedTextArtifact, + OrchestratorRequest, + OrchestratorResponse, + UnsupportedCapabilityResponse, + WorkflowArtifact, +) from .pdf_edit import ( EditCannotDoResponse, EditClarificationRequest, @@ -50,14 +78,27 @@ from .pdf_edit import ( PdfEditResponse, ) from .pdf_questions import ( + NeedContentFileRequest, PdfQuestionAnswerResponse, - PdfQuestionNeedTextResponse, + PdfQuestionNeedContentResponse, PdfQuestionNotFoundResponse, PdfQuestionRequest, PdfQuestionResponse, ) +from .rag import ( + MAX_INDEX_TEXT_LENGTH, + RagCollectionsResponse, + RagDeleteCollectionResponse, + RagIndexRequest, + RagIndexResponse, + RagSearchRequest, + RagSearchResponse, + RagSearchResultItem, + RagStatusResponse, +) __all__ = [ + "MAX_INDEX_TEXT_LENGTH", "AgentDraft", "AgentDraftRequest", "AgentDraftResponse", @@ -69,48 +110,74 @@ __all__ = [ "AgentSpec", "AgentSpecStep", "AiToolAgentStep", + "AnalysedFileResult", + "ArtifactKind", "CannotContinueExecutionAction", "CleanedLabel", + "CompletedExecutionAction", "ConversationMessage", + "CrossFileRole", "DetectedRole", + "Discrepancy", + "DiscrepancyKind", "DocumentExtractionRequest", "DocumentExtractionResponse", "DocumentText", - "FileFieldSet", - "FileFillRequest", - "FileFillResult", - "FormAnalysisRequest", - "FormAnalysisResponse", - "AnalysedFileResult", - "CrossFileRole", - "FormFillBatchRequest", - "FormFillBatchResponse", - "CompletedExecutionAction", - "FieldMapping", - "FormField", "EditCannotDoResponse", "EditClarificationRequest", "EditPlanResponse", + "Evidence", "ExecutionContext", "ExecutionStepResult", + "ExtractedFileText", + "ExtractedTextArtifact", + "FieldMapping", + "FileFieldSet", + "FileFillRequest", + "FileFillResult", + "Folio", + "FolioManifest", + "FolioType", + "FormAnalysisRequest", + "FormAnalysisResponse", + "FormField", + "FormFillBatchRequest", + "FormFillBatchResponse", + "format_conversation_history", "HealthResponse", "KnowledgeEntry", "KnowledgeUpdateResponse", "MultiProfileExtractionResponse", + "NeedContentFileRequest", + "ProposedProfile", "NextExecutionAction", "OrchestratorRequest", "OrchestratorResponse", - "ProposedProfile", + "PdfContentType", "PdfEditRequest", "PdfEditResponse", "PdfQuestionAnswerResponse", + "PdfQuestionNeedContentResponse", "PdfQuestionNotFoundResponse", - "PdfQuestionNeedTextResponse", "PdfQuestionRequest", "PdfQuestionResponse", "PdfTextSelection", + "RagCollectionsResponse", + "RagDeleteCollectionResponse", + "RagIndexRequest", + "RagIndexResponse", + "RagSearchRequest", + "RagSearchResponse", + "RagSearchResultItem", + "RagStatusResponse", + "Requisition", + "Severity", + "StepKind", "SupportedCapability", - "ToolOperationStep", "ToolCallExecutionAction", + "ToolOperationStep", "UnsupportedCapabilityResponse", + "Verdict", + "WorkflowArtifact", + "WorkflowOutcome", ] diff --git a/engine/src/stirling/contracts/agent_drafts.py b/engine/src/stirling/contracts/agent_drafts.py index a59d279f21..752019d348 100644 --- a/engine/src/stirling/contracts/agent_drafts.py +++ b/engine/src/stirling/contracts/agent_drafts.py @@ -7,12 +7,12 @@ from pydantic import Field from stirling.models import ApiModel from .agent_specs import AgentSpecStep -from .common import ConversationMessage +from .common import ConversationMessage, StepKind, WorkflowOutcome from .pdf_edit import EditCannotDoResponse, EditClarificationRequest class AgentDraftStep(ApiModel): - kind: Literal["tool", "ai_tool"] + kind: Literal[StepKind.TOOL, StepKind.AI_TOOL] title: str description: str @@ -30,7 +30,7 @@ class AgentDraftRequest(ApiModel): class AgentDraftResponse(ApiModel): - outcome: Literal["draft"] = "draft" + outcome: Literal[WorkflowOutcome.DRAFT] = WorkflowOutcome.DRAFT draft: AgentDraft @@ -41,7 +41,7 @@ class AgentRevisionRequest(ApiModel): class AgentRevisionResponse(ApiModel): - outcome: Literal["draft"] = "draft" + outcome: Literal[WorkflowOutcome.DRAFT] = WorkflowOutcome.DRAFT draft: AgentDraft diff --git a/engine/src/stirling/contracts/agent_specs.py b/engine/src/stirling/contracts/agent_specs.py index 403af17807..933d2e6856 100644 --- a/engine/src/stirling/contracts/agent_specs.py +++ b/engine/src/stirling/contracts/agent_specs.py @@ -4,16 +4,16 @@ from typing import Annotated, Literal from pydantic import Field -from stirling.models import ApiModel, OperationId +from stirling.models import ApiModel, ToolEndpoint -from .common import ToolOperationStep +from .common import StepKind, ToolOperationStep class AiToolAgentStep(ApiModel): - kind: Literal["ai_tool"] = "ai_tool" + kind: Literal[StepKind.AI_TOOL] = StepKind.AI_TOOL title: str description: str - tool: OperationId + tool: ToolEndpoint instruction: str diff --git a/engine/src/stirling/contracts/common.py b/engine/src/stirling/contracts/common.py index 65bb224004..50038d1503 100644 --- a/engine/src/stirling/contracts/common.py +++ b/engine/src/stirling/contracts/common.py @@ -1,10 +1,89 @@ from __future__ import annotations -from typing import Literal +from enum import StrEnum +from typing import Literal, assert_never -from pydantic import model_validator +from pydantic import Field, model_validator -from stirling.models import OPERATIONS, ApiModel, OperationId, ParamToolModel +from stirling.models import OPERATIONS, ApiModel, ToolEndpoint +from stirling.models.agent_tool_models import AGENT_OPERATIONS, AgentToolId, AnyParamModel, AnyToolId + + +class PdfContentType(StrEnum): + """Types of content that can be extracted from a PDF and sent to the AI. + + Java counterpart: AiPdfContentType.java - values must stay in sync. + """ + + # Document-level structured data + PAGE_LAYOUT = "page_layout" + DOCUMENT_METADATA = "document_metadata" + ENCRYPTION_INFO = "encryption_info" + BOOKMARKS = "bookmarks" + LAYERS = "layers" + EMBEDDED_FILES = "embedded_files" + JAVASCRIPT = "javascript" + LINKS = "links" + IMAGE_INFO = "image_info" + FONTS = "fonts" + + # Text and content + PAGE_TEXT = "page_text" + FULL_TEXT = "full_text" + FORM_FIELDS = "form_fields" + ANNOTATIONS = "annotations" + SIGNATURES = "signatures" + STRUCTURE_TREE = "structure_tree" + XMP_METADATA = "xmp_metadata" + + # Heavy content + COMPLIANCE = "compliance" + IMAGES = "images" + + +class WorkflowOutcome(StrEnum): + """Discriminator values for all workflow response unions (outcome field). + + Java counterpart: AiWorkflowOutcome.java - values must stay in sync. + """ + + ANSWER = "answer" + NEED_CONTENT = "need_content" + NOT_FOUND = "not_found" + PLAN = "plan" + NEED_CLARIFICATION = "need_clarification" + CANNOT_DO = "cannot_do" + DRAFT = "draft" + TOOL_CALL = "tool_call" + COMPLETED = "completed" + CANNOT_CONTINUE = "cannot_continue" + UNSUPPORTED_CAPABILITY = "unsupported_capability" + + +class ArtifactKind(StrEnum): + """Discriminator values for WorkflowArtifact unions (kind field). + + Java counterpart: PdfContentExtractor.ArtifactKind - values must stay in sync. + """ + + EXTRACTED_TEXT = "extracted_text" + + +class StepKind(StrEnum): + """Discriminator values for AgentSpecStep unions (kind field).""" + + TOOL = "tool" + AI_TOOL = "ai_tool" + + +class SupportedCapability(StrEnum): + ORCHESTRATE = "orchestrate" + PDF_EDIT = "pdf_edit" + PDF_QUESTION = "pdf_question" + AGENT_DRAFT = "agent_draft" + AGENT_REVISE = "agent_revise" + AGENT_NEXT_ACTION = "agent_next_action" + MATH_AUDITOR_AGENT = "math_auditor_agent" class ConversationMessage(ApiModel): @@ -12,21 +91,37 @@ class ConversationMessage(ApiModel): content: str +def format_conversation_history(conversation_history: list[ConversationMessage]) -> str: + if not conversation_history: + return "None" + return "\n".join(f"- {message.role}: {message.content}" for message in conversation_history) + + class PdfTextSelection(ApiModel): page_number: int | None = None text: str +class ExtractedFileText(ApiModel): + file_name: str + pages: list[PdfTextSelection] = Field(default_factory=list) + + class ToolOperationStep(ApiModel): - kind: Literal["tool"] = "tool" - tool: OperationId - parameters: ParamToolModel + kind: Literal[StepKind.TOOL] = StepKind.TOOL + tool: AnyToolId + parameters: AnyParamModel @model_validator(mode="after") def validate_tool_parameter_pairing(self) -> ToolOperationStep: - expected_type = OPERATIONS[self.tool] + if isinstance(self.tool, AgentToolId): + expected_type = AGENT_OPERATIONS[self.tool] + elif isinstance(self.tool, ToolEndpoint): + expected_type = OPERATIONS[self.tool] + else: + assert_never(self.tool) + if not isinstance(self.parameters, expected_type): actual_type = type(self.parameters).__name__ - expected_type_name = expected_type.__name__ - raise ValueError(f"Parameters for tool {self.tool.value} must be {expected_type_name}, got {actual_type}.") + raise ValueError(f"Parameters for tool {self.tool} must be {expected_type.__name__}, got {actual_type}.") return self diff --git a/engine/src/stirling/contracts/execution.py b/engine/src/stirling/contracts/execution.py index 64e70d682a..a5a227c70e 100644 --- a/engine/src/stirling/contracts/execution.py +++ b/engine/src/stirling/contracts/execution.py @@ -4,14 +4,15 @@ from typing import Annotated, Any, Literal from pydantic import Field -from stirling.models import ApiModel, OperationId, ParamToolModel +from stirling.models import ApiModel, ParamToolModel, ToolEndpoint from .agent_specs import AgentSpec +from .common import WorkflowOutcome class ExecutionStepResult(ApiModel): step_index: int - tool: OperationId | None = None + tool: ToolEndpoint | None = None success: bool output_summary: str | None = None output_data: dict[str, Any] = Field(default_factory=dict) @@ -31,19 +32,19 @@ class AgentExecutionRequest(ApiModel): class ToolCallExecutionAction(ApiModel): - outcome: Literal["tool_call"] = "tool_call" - tool: OperationId + outcome: Literal[WorkflowOutcome.TOOL_CALL] = WorkflowOutcome.TOOL_CALL + tool: ToolEndpoint parameters: ParamToolModel rationale: str | None = None class CompletedExecutionAction(ApiModel): - outcome: Literal["completed"] = "completed" + outcome: Literal[WorkflowOutcome.COMPLETED] = WorkflowOutcome.COMPLETED summary: str class CannotContinueExecutionAction(ApiModel): - outcome: Literal["cannot_continue"] = "cannot_continue" + outcome: Literal[WorkflowOutcome.CANNOT_CONTINUE] = WorkflowOutcome.CANNOT_CONTINUE reason: str diff --git a/engine/src/stirling/contracts/ledger.py b/engine/src/stirling/contracts/ledger.py new file mode 100644 index 0000000000..bc31b960e1 --- /dev/null +++ b/engine/src/stirling/contracts/ledger.py @@ -0,0 +1,196 @@ +""" +Ledger Auditor — shared models for the Java-Python protocol. + +Every struct that crosses the wire lives here so the contract is +impossible to miss or partially implement. +""" + +from __future__ import annotations + +from enum import StrEnum +from typing import Literal + +from pydantic import Field + +from stirling.models import ApiModel + +# --------------------------------------------------------------------------- +# Page classification — Java's side of the conversation +# --------------------------------------------------------------------------- + + +class FolioType(StrEnum): + """How Java classifies each page after a cheap PDFBox scan. + + Java counterpart: FolioType.java - values must stay in sync. + """ + + TEXT = "text" # selectable text layer present + IMAGE = "image" # image-only, will need OCR + MIXED = "mixed" # partial text layer + embedded images + + +class FolioManifest(ApiModel): + """ + Java's opening move: a fast, cheap page classification with no OCR or + table extraction — just PDFBox character counts and image detection. + + Python inspects this and returns a Requisition declaring what it needs. + """ + + session_id: str = Field(description="Opaque handle Java uses to find the PDF on disk.") + page_count: int = Field(ge=1) + folio_types: list[FolioType] = Field(description="One entry per page (0-indexed). len(folio_types) == page_count.") + round: int = Field(default=1, ge=1, le=3, description="Which negotiation round this is.") + + +# --------------------------------------------------------------------------- +# Requisition — Python's declaration of what it needs +# --------------------------------------------------------------------------- + + +class Requisition(ApiModel): + """ + Python's reply to a FolioManifest: a precise shopping list of what Java + must extract before the auditor can form an opinion. + + Java fulfils this and sends back an Evidence payload. + """ + + type: Literal["requisition"] = "requisition" + need_text: list[int] = Field( + default_factory=list, + description="0-indexed page numbers. Java runs PDFBox text extraction on these.", + ) + need_tables: list[int] = Field( + default_factory=list, + description="0-indexed page numbers. Java runs Tabula CSV extraction on these.", + ) + need_ocr: list[int] = Field( + default_factory=list, + description="0-indexed page numbers. Java runs OCRmyPDF on these.", + ) + rationale: str = Field(description="Plain-language reason, written for log readability, not the client.") + + +# --------------------------------------------------------------------------- +# Evidence — Java's fulfilment of a Requisition +# --------------------------------------------------------------------------- + + +class Folio(ApiModel): + """ + One page's worth of extracted content — whatever Java was able to provide + in response to the Requisition for that page. + """ + + page: int = Field(ge=0, description="0-indexed page number.") + text: str | None = Field(default=None, description="PDFBox plain-text extraction.") + tables: list[str] | None = Field(default=None, description="Tabula CSV strings, one per table found on the page.") + ocr_text: str | None = Field(default=None, description="OCRmyPDF output text.") + ocr_confidence: float | None = Field( + default=None, ge=0.0, le=1.0, description="Mean character confidence from OCRmyPDF." + ) + + @property + def readable_text(self) -> str: + """Best available text for this folio — OCR wins over digital when present.""" + return self.ocr_text or self.text or "" + + +class Evidence(ApiModel): + """ + Java's fulfilment package: the extracted content Python asked for. + Java may also set final_round=True on the last allowable round to signal + that the auditor must return a Verdict regardless of remaining questions. + """ + + session_id: str + folios: list[Folio] + round: int = Field(ge=1, le=3) + final_round: bool = Field( + default=False, + description="When True, Java will not honour further Requisitions. " + "The auditor must return a Verdict this round.", + ) + unauditable_pages: list[int] = Field( + default_factory=list, + description=( + "Pages that were requested in the Requisition but could not be fulfilled — " + "e.g. OCR was asked for but is not wired. The Auditor echoes these into " + "Verdict.unauditable_pages so the client knows coverage is incomplete." + ), + ) + + +# --------------------------------------------------------------------------- +# Findings — what the auditor discovers +# --------------------------------------------------------------------------- + + +class DiscrepancyKind(StrEnum): + """Java counterpart: DiscrepancyKind.java - values must stay in sync.""" + + TALLY = "tally" # a row/column sum is wrong + ARITHMETIC = "arithmetic" # an inline calculation is wrong + CONSISTENCY = "consistency" # the same figure is stated differently elsewhere + STATEMENT = "statement" # a prose claim contradicts the numbers + + +class Severity(StrEnum): + """Java counterpart: AuditSeverity.java - values must stay in sync.""" + + ERROR = "error" # definite arithmetic mistake + WARNING = "warning" # possible rounding or ambiguity + + +class Discrepancy(ApiModel): + """A single mathematical error found in the document.""" + + page: int = Field(ge=0) + kind: DiscrepancyKind + severity: Severity + description: str = Field(description="Human-readable explanation of the error.") + stated: str = Field(description="The value as it appears in the document.") + expected: str = Field(description="The value the auditor calculated.") + context: str = Field( + default="", + description="Surrounding text or table fragment for traceability.", + ) + + +# --------------------------------------------------------------------------- +# Verdict — the final report +# --------------------------------------------------------------------------- + + +class Verdict(ApiModel): + """ + The auditor's final opinion on the document's mathematical integrity. + Returned to Java as the terminal message in the negotiation. + """ + + type: Literal["verdict"] = "verdict" + session_id: str + discrepancies: list[Discrepancy] = Field(default_factory=list) + pages_examined: list[int] = Field(description="0-indexed page numbers the auditor actually inspected.") + rounds_taken: int = Field(ge=1, le=3) + summary: str = Field(description="One or two sentences summarising the audit outcome for the client.") + clean: bool = Field(description="True iff no errors were found (warnings are tolerated).") + unauditable_pages: list[int] = Field( + default_factory=list, + description=( + "0-indexed pages that could not be audited — typically because OCR was " + "requested but is not yet wired. Java populates this by omitting the folio " + "and the Auditor echoes the page number here so the client knows coverage " + "is incomplete." + ), + ) + + @property + def error_count(self) -> int: + return sum(1 for d in self.discrepancies if d.severity == Severity.ERROR) + + @property + def warning_count(self) -> int: + return sum(1 for d in self.discrepancies if d.severity == Severity.WARNING) diff --git a/engine/src/stirling/contracts/orchestrator.py b/engine/src/stirling/contracts/orchestrator.py index 823b0f9d12..f38a2f8379 100644 --- a/engine/src/stirling/contracts/orchestrator.py +++ b/engine/src/stirling/contracts/orchestrator.py @@ -1,6 +1,5 @@ from __future__ import annotations -from enum import StrEnum from typing import Annotated, Literal from pydantic import Field @@ -8,29 +7,37 @@ from pydantic import Field from stirling.models import ApiModel from .agent_drafts import AgentDraftResponse +from .common import ( + ArtifactKind, + ConversationMessage, + ExtractedFileText, + SupportedCapability, + WorkflowOutcome, +) from .execution import NextExecutionAction from .form_fill import KnowledgeUpdateResponse from .pdf_edit import PdfEditResponse from .pdf_questions import PdfQuestionResponse -class SupportedCapability(StrEnum): - ORCHESTRATE = "orchestrate" - PDF_EDIT = "pdf_edit" - PDF_QUESTION = "pdf_question" - AGENT_DRAFT = "agent_draft" - AGENT_REVISE = "agent_revise" - AGENT_NEXT_ACTION = "agent_next_action" - FORM_FILL = "form_fill" +class ExtractedTextArtifact(ApiModel): + kind: Literal[ArtifactKind.EXTRACTED_TEXT] = ArtifactKind.EXTRACTED_TEXT + files: list[ExtractedFileText] = Field(default_factory=list) + + +WorkflowArtifact = Annotated[ExtractedTextArtifact, Field(discriminator="kind")] class OrchestratorRequest(ApiModel): user_message: str - conversation_id: str | None = None + file_names: list[str] + conversation_history: list[ConversationMessage] = Field(default_factory=list) + artifacts: list[WorkflowArtifact] = Field(default_factory=list) + resume_with: SupportedCapability | None = None class UnsupportedCapabilityResponse(ApiModel): - outcome: Literal["unsupported_capability"] = "unsupported_capability" + outcome: Literal[WorkflowOutcome.UNSUPPORTED_CAPABILITY] = WorkflowOutcome.UNSUPPORTED_CAPABILITY capability: str message: str diff --git a/engine/src/stirling/contracts/pdf_edit.py b/engine/src/stirling/contracts/pdf_edit.py index e3e1e60579..a7bcedfa9d 100644 --- a/engine/src/stirling/contracts/pdf_edit.py +++ b/engine/src/stirling/contracts/pdf_edit.py @@ -6,30 +6,30 @@ from pydantic import Field from stirling.models import ApiModel -from .common import ToolOperationStep +from .common import ConversationMessage, ToolOperationStep, WorkflowOutcome class PdfEditRequest(ApiModel): user_message: str - conversation_id: str | None = None file_names: list[str] = Field(default_factory=list) + conversation_history: list[ConversationMessage] = Field(default_factory=list) class EditPlanResponse(ApiModel): - outcome: Literal["plan"] = "plan" + outcome: Literal[WorkflowOutcome.PLAN] = WorkflowOutcome.PLAN summary: str rationale: str | None = None steps: list[ToolOperationStep] class EditClarificationRequest(ApiModel): - outcome: Literal["need_clarification"] = "need_clarification" + outcome: Literal[WorkflowOutcome.NEED_CLARIFICATION] = WorkflowOutcome.NEED_CLARIFICATION question: str reason: str class EditCannotDoResponse(ApiModel): - outcome: Literal["cannot_do"] = "cannot_do" + outcome: Literal[WorkflowOutcome.CANNOT_DO] = WorkflowOutcome.CANNOT_DO reason: str diff --git a/engine/src/stirling/contracts/pdf_questions.py b/engine/src/stirling/contracts/pdf_questions.py index 3ac12bdfe8..4ee0d25966 100644 --- a/engine/src/stirling/contracts/pdf_questions.py +++ b/engine/src/stirling/contracts/pdf_questions.py @@ -6,31 +6,49 @@ from pydantic import Field from stirling.models import ApiModel +from .common import ( + ConversationMessage, + ExtractedFileText, + PdfContentType, + SupportedCapability, + WorkflowOutcome, +) + class PdfQuestionRequest(ApiModel): question: str - conversation_id: str | None = None - extracted_text: str = "" - file_name: str | None = None + page_text: list[ExtractedFileText] = Field(default_factory=list) + file_names: list[str] + conversation_history: list[ConversationMessage] = Field(default_factory=list) class PdfQuestionAnswerResponse(ApiModel): - outcome: Literal["answer"] = "answer" + outcome: Literal[WorkflowOutcome.ANSWER] = WorkflowOutcome.ANSWER answer: str - evidence: list[str] = Field(default_factory=list) + evidence: list[ExtractedFileText] = Field(default_factory=list) -class PdfQuestionNeedTextResponse(ApiModel): - outcome: Literal["need_text"] = "need_text" +class NeedContentFileRequest(ApiModel): + file_name: str + page_numbers: list[int] = Field(default_factory=list) + content_types: list[PdfContentType] + + +class PdfQuestionNeedContentResponse(ApiModel): + outcome: Literal[WorkflowOutcome.NEED_CONTENT] = WorkflowOutcome.NEED_CONTENT + resume_with: SupportedCapability = SupportedCapability.PDF_QUESTION reason: str + files: list[NeedContentFileRequest] = Field(default_factory=list) + max_pages: int + max_characters: int class PdfQuestionNotFoundResponse(ApiModel): - outcome: Literal["not_found"] = "not_found" + outcome: Literal[WorkflowOutcome.NOT_FOUND] = WorkflowOutcome.NOT_FOUND reason: str PdfQuestionResponse = Annotated[ - PdfQuestionAnswerResponse | PdfQuestionNeedTextResponse | PdfQuestionNotFoundResponse, + PdfQuestionAnswerResponse | PdfQuestionNeedContentResponse | PdfQuestionNotFoundResponse, Field(discriminator="outcome"), ] diff --git a/engine/src/stirling/contracts/rag.py b/engine/src/stirling/contracts/rag.py new file mode 100644 index 0000000000..c4cea35af1 --- /dev/null +++ b/engine/src/stirling/contracts/rag.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from pydantic import Field + +from stirling.models import ApiModel + +MAX_INDEX_TEXT_LENGTH = 1_000_000 # 1MB text limit per index request + + +class RagStatusResponse(ApiModel): + embedding_model: str + collections: list[str] + + +class RagIndexRequest(ApiModel): + collection: str = Field(min_length=1) + text: str = Field(max_length=MAX_INDEX_TEXT_LENGTH) + source: str = "" + metadata: dict[str, str] = Field(default_factory=dict) + + +class RagIndexResponse(ApiModel): + collection: str + chunks_indexed: int + + +class RagSearchRequest(ApiModel): + query: str + collection: str | None = Field(default=None, min_length=1) + top_k: int = 5 + + +class RagSearchResultItem(ApiModel): + text: str + source: str + chunk_id: str + score: float + + +class RagSearchResponse(ApiModel): + query: str + results: list[RagSearchResultItem] + + +class RagCollectionsResponse(ApiModel): + collections: list[str] + + +class RagDeleteCollectionResponse(ApiModel): + status: str + collection: str diff --git a/engine/src/stirling/logging.py b/engine/src/stirling/logging.py new file mode 100644 index 0000000000..a0a6fec20b --- /dev/null +++ b/engine/src/stirling/logging.py @@ -0,0 +1,22 @@ +"""Shared logging utilities for the Stirling AI engine.""" + +from __future__ import annotations + +import json + + +class Pretty: + """Lazy JSON formatter — only serialises when ``str()`` is called. + + Designed for use with ``logging``'s ``%s`` formatting so that the + JSON serialisation is skipped entirely when the log message is + never emitted. + """ + + __slots__ = ("_obj",) + + def __init__(self, obj: object) -> None: + self._obj = obj + + def __str__(self) -> str: + return json.dumps(self._obj, indent=2, default=str, ensure_ascii=True) diff --git a/engine/src/stirling/models/__init__.py b/engine/src/stirling/models/__init__.py index 7877141aaf..0d4c774d0e 100644 --- a/engine/src/stirling/models/__init__.py +++ b/engine/src/stirling/models/__init__.py @@ -1,11 +1,11 @@ from . import tool_models from .base import ApiModel -from .tool_models import OPERATIONS, OperationId, ParamToolModel +from .tool_models import OPERATIONS, ParamToolModel, ToolEndpoint __all__ = [ "ApiModel", "OPERATIONS", - "OperationId", "ParamToolModel", + "ToolEndpoint", "tool_models", ] diff --git a/engine/src/stirling/models/agent_tool_models.py b/engine/src/stirling/models/agent_tool_models.py new file mode 100644 index 0000000000..ecbcde78d7 --- /dev/null +++ b/engine/src/stirling/models/agent_tool_models.py @@ -0,0 +1,30 @@ +"""Agent tool IDs, parameter models, and registry. + +tool_models.py is auto-generated from the Java OpenAPI spec. This file is its +manually-maintained counterpart for tools backed by AI agent pipelines. +""" + +from __future__ import annotations + +from enum import StrEnum + +from stirling.models.base import ApiModel +from stirling.models.tool_models import ParamToolModel, ToolEndpoint + + +class AgentToolId(StrEnum): + MATH_AUDITOR_AGENT = "mathAuditorAgent" + + +class MathAuditorAgentParams(ApiModel): + tolerance: str = "0.01" + + +type AgentParamModel = MathAuditorAgentParams + +type AnyToolId = ToolEndpoint | AgentToolId +type AnyParamModel = ParamToolModel | AgentParamModel + +AGENT_OPERATIONS: dict[AgentToolId, type[AgentParamModel]] = { + AgentToolId.MATH_AUDITOR_AGENT: MathAuditorAgentParams, +} diff --git a/engine/src/stirling/models/tool_models.py b/engine/src/stirling/models/tool_models.py index 334d88442d..d671441141 100644 --- a/engine/src/stirling/models/tool_models.py +++ b/engine/src/stirling/models/tool_models.py @@ -1,439 +1,1380 @@ # AUTO-GENERATED FILE. DO NOT EDIT. -# Generated by scripts/generate_tool_models.py from frontend TypeScript sources. +# Generated by scripts/generate_tool_models.py from Java OpenAPI spec (SwaggerDoc.json). +# ruff: noqa: E501 + from __future__ import annotations -from enum import StrEnum -from typing import Any, Literal +from enum import Enum, IntEnum, StrEnum +from typing import Any + +from pydantic import Field, RootModel, SecretStr from stirling.models.base import ApiModel class AddAttachmentsParams(ApiModel): - attachments: list[dict[str, Any]] = [] - - -class AdjustContrastParams(ApiModel): - blue: float = 100 - brightness: float = 100 - contrast: float = 100 - green: float = 100 - red: float = 100 - saturation: float = 100 - - -class AutoRenameParams(ApiModel): - use_first_text_as_fallback: bool = False - - -class AutomateParams(ApiModel): - pass - - -class BookletImpositionParams(ApiModel): - add_border: bool = False - add_gutter: bool = False - double_sided: bool = True - duplex_pass: Literal["BOTH", "FIRST", "SECOND"] = "BOTH" - flip_on_short_edge: bool = False - gutter_size: float = 12 - pages_per_sheet: float = 2 - spine_location: Literal["LEFT", "RIGHT"] = "LEFT" - - -class CertSignParams(ApiModel): - cert_file: dict[str, Any] | None = None - cert_type: Literal["", "PEM", "PKCS12", "PFX", "JKS"] = "" - jks_file: dict[str, Any] | None = None - location: str = "" - name: str = "" - p12_file: dict[str, Any] | None = None - page_number: float = 1 - password: str = "" - private_key_file: dict[str, Any] | None = None - reason: str = "" - show_logo: bool = True - show_signature: bool = False - sign_mode: Literal["MANUAL", "AUTO"] = "MANUAL" - - -class ChangeMetadataParams(ApiModel): - author: str = "" - creation_date: str | None = None - creator: str = "" - custom_metadata: list[dict[str, str]] = [] - delete_all: bool = False - keywords: str = "" - modification_date: str | None = None - producer: str = "" - subject: str = "" - title: str = "" - trapped: Literal["True", "False", "Unknown"] | None = None - - -class ChangePermissionsParams(ApiModel): - prevent_assembly: bool = False - prevent_extract_content: bool = False - prevent_extract_for_accessibility: bool = False - prevent_fill_in_form: bool = False - prevent_modify: bool = False - prevent_modify_annotations: bool = False - prevent_printing: bool = False - prevent_printing_faithful: bool = False - - -class AddPasswordParams(ApiModel): - key_length: float = 128 - owner_password: str = "" - password: str = "" - permissions: ChangePermissionsParams = ChangePermissionsParams.model_validate( - { - "preventAssembly": False, - "preventExtractContent": False, - "preventExtractForAccessibility": False, - "preventFillInForm": False, - "preventModify": False, - "preventModifyAnnotations": False, - "preventPrinting": False, - "preventPrintingFaithful": False, - } + attachments: list[bytes] | None = Field(None, description="The image file to be overlaid onto the PDF.") + convert_to_pdf_a3b: bool | None = Field( + False, description="Convert the resulting PDF to PDF/A-3b format after adding attachments" ) -class CompressParams(ApiModel): - compression_level: float = 5 - compression_method: Literal["quality", "filesize"] = "quality" - expected_size: str = "" - file_size_unit: Literal["KB", "MB"] = "MB" - file_size_value: str = "" - grayscale: bool = False +class AddImageParams(ApiModel): + every_page: bool | None = Field(False, description="Whether to overlay the image onto every page of the PDF.") + x: float | None = Field(0, description="The x-coordinate at which to place the top-left corner of the image.") + y: float | None = Field(0, description="The y-coordinate at which to place the top-left corner of the image.") -class ConvertParams(ApiModel): - cbz_options: dict[str, bool] = {"optimizeForEbook": False} - cbz_output_options: dict[str, float] = {"dpi": 150} - email_options: dict[str, Any] = { - "includeAttachments": True, - "maxAttachmentSizeMB": 10, - "downloadHtml": False, - "includeAllRecipients": False, - } - from_extension: str = "" - html_options: dict[str, float] = {"zoomLevel": 1} - image_options: dict[str, Any] = { - "colorType": "color", - "dpi": 300, - "singleOrMultiple": "multiple", - "fitOption": "maintainAspectRatio", - "autoRotate": True, - "combineImages": True, - } - is_smart_detection: bool = False - pdfa_options: dict[str, str] = {"outputFormat": "pdfa-1"} - smart_detection_type: Literal["mixed", "images", "web", "none"] = "none" - to_extension: str = "" +class CustomMargin(StrEnum): + small = "small" + medium = "medium" + large = "large" + x_large = "x-large" + + +class FontType(StrEnum): + helvetica = "helvetica" + courier = "courier" + times = "times" + + +class Position(IntEnum): + integer_1 = 1 + integer_2 = 2 + integer_3 = 3 + integer_4 = 4 + integer_5 = 5 + integer_6 = 6 + integer_7 = 7 + integer_8 = 8 + integer_9 = 9 + + +class AddPageNumbersParams(ApiModel): + custom_margin: CustomMargin | None = Field( + CustomMargin.medium, description="Custom margin: small/medium/large/x-large" + ) + custom_text: str | None = Field( + "{n}", + description="Custom text pattern. Available variables: {n}=current page number, {total}=total pages, {filename}=original filename", + examples=["Page {n} of {total}"], + ) + font_color: str | None = Field( + "#000000", description="Hex colour for page numbers (e.g. #FF0000)", examples=["#000000"] + ) + font_size: float | None = Field(12, description="Font size for page numbers", ge=1.0) + font_type: FontType | None = Field(None, description="Font type for page numbers") + page_numbers: str | None = Field( + "all", + description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", + ) + pages_to_number: str | None = Field("all", description="Which pages to number (e.g. '1,3-5,7' or 'all')") + position: Position | None = Field( + None, + description="Position: 1-9 representing positions on the page (1=top-left, 2=top-center, 3=top-right, 4=middle-left, 5=middle-center, 6=middle-right, 7=bottom-left, 8=bottom-center, 9=bottom-right)", + ) + starting_number: int | None = Field(1, description="Starting number for page numbering", ge=1) + zero_pad: int | None = Field( + 0, description="Zero-padding width for page numbers (Bates Stamping). Set to 0 to disable padding", ge=0 + ) + + +class KeyLength(IntEnum): + integer_40 = 40 + integer_128 = 128 + integer_256 = 256 + + +class AddPasswordParams(ApiModel): + key_length: KeyLength | None = Field(None, description="The length of the encryption key") + owner_password: SecretStr | None = Field( + None, + description="The owner password to be added to the PDF file (Restricts what can be done with the document once it is opened)", + ) + password: SecretStr | None = Field( + None, description="The password to be added to the PDF file (Restricts the opening of the document itself.)" + ) + prevent_assembly: bool | None = Field(False, description="Whether document assembly is prevented") + prevent_extract_content: bool | None = Field(False, description="Whether content extraction is prevented") + prevent_extract_for_accessibility: bool | None = Field( + False, description="Whether content extraction for accessibility is prevented" + ) + prevent_fill_in_form: bool | None = Field(False, description="Whether form filling is prevented") + prevent_modify: bool | None = Field(False, description="Whether document modification is prevented") + prevent_modify_annotations: bool | None = Field( + False, description="Whether modification of annotations is prevented" + ) + prevent_printing: bool | None = Field(False, description="Whether printing of the document is prevented") + prevent_printing_faithful: bool | None = Field(False, description="Whether faithful printing is prevented") + + +class Alphabet(StrEnum): + roman = "roman" + arabic = "arabic" + japanese = "japanese" + korean = "korean" + chinese = "chinese" + thai = "thai" + + +class StampType(StrEnum): + text = "text" + image = "image" + + +class AddStampParams(ApiModel): + alphabet: Alphabet | None = Field(Alphabet.roman, description="The selected alphabet of the stamp text") + custom_color: str | None = Field("#d3d3d3", description="The color of the stamp text") + custom_margin: CustomMargin | None = Field( + CustomMargin.medium, description="Specifies the margin size for the stamp." + ) + font_size: float | None = Field(40, description="The font size of the stamp text and image in points.") + opacity: float | None = Field(0.5, description="The opacity of the stamp (0.0 - 1.0)") + override_x: float | None = Field( + -1, + description="Override X coordinate for stamp placement. If set, it will override the position-based calculation. Negative value means no override.", + ) + override_y: float | None = Field( + -1, + description="Override Y coordinate for stamp placement. If set, it will override the position-based calculation. Negative value means no override.", + ) + page_numbers: str | None = Field( + "all", + description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", + ) + position: Position | None = Field( + Position.integer_8, + description="Position for stamp placement based on a 1-9 grid (1: bottom-left, 2: bottom-center, 3: bottom-right, 4: middle-left, 5: middle-center, 6: middle-right, 7: top-left, 8: top-center, 9: top-right)", + ) + rotation: float | None = Field(0, description="The rotation of the stamp in degrees") + stamp_text: str | None = Field("Stirling Software", description="The stamp text") + stamp_type: StampType | None = Field(None, description="The stamp type (text or image)") + + +class WatermarkType(StrEnum): + text = "text" + image = "image" + + +class AddWatermarkParams(ApiModel): + alphabet: Alphabet | None = Field(Alphabet.roman, description="The selected alphabet") + convert_pdf_to_image: bool | None = Field(False, description="Convert the redacted PDF to an image") + custom_color: str | None = Field("#d3d3d3", description="The color for watermark") + font_size: float | None = Field(30, description="The font size of the watermark text", ge=1.0) + height_spacer: int | None = Field(50, description="The height spacer between watermark elements", ge=0) + opacity: float | None = Field(0.5, description="The opacity of the watermark (0.0 - 1.0)") + rotation: float | None = Field(0, description="The rotation of the watermark in degrees") + watermark_text: str | None = Field("Stirling Software", description="The watermark text") + watermark_type: WatermarkType | None = Field(None, description="The watermark type (text or image)") + width_spacer: int | None = Field(50, description="The width spacer between watermark elements", ge=0) + + +class AutoRedactParams(ApiModel): + convert_pdf_to_image: bool | None = Field(False, description="Convert the redacted PDF to an image") + custom_padding: float | None = Field(None, description="Custom padding for redaction") + list_of_text: str | None = Field("text,text2", description="List of text to redact from the PDF") + redact_color: str | None = Field("#000000", description="The color for redaction") + use_regex: bool | None = Field(False, description="Whether to use regex for the listOfText") + whole_word_search: bool | None = Field(False, description="Whether to use whole word search") + + +class AutoRenameParams(ApiModel): + use_first_text_as_fallback: bool | None = Field( + False, + description="Flag indicating whether to use the first text as a fallback if no suitable title is found. Defaults to false.", + ) + + +class AutoSplitPdfParams(ApiModel): + duplex_mode: bool | None = Field( + False, + description="Flag indicating if the duplex mode is active, where the page after the divider also gets removed.", + ) + + +class DuplexPass(StrEnum): + both = "BOTH" + first = "FIRST" + second = "SECOND" + + +class PagesPerSheet(Enum): + number_2 = 2 + + +class SpineLocation(StrEnum): + left = "LEFT" + right = "RIGHT" + + +class BookletImpositionParams(ApiModel): + add_border: bool | None = Field(None, description="Boolean for if you wish to add border around the pages") + add_gutter: bool | None = Field(None, description="Add gutter margin (inner margin for binding)") + double_sided: bool | None = Field(None, description="Generate both front and back sides (double-sided printing)") + duplex_pass: DuplexPass | None = Field(DuplexPass.both, description="For manual duplex: which pass to generate") + flip_on_short_edge: bool | None = Field( + None, description="Flip back sides for short-edge duplex printing (default is long-edge)" + ) + gutter_size: float | None = Field(12, description="Gutter margin size in points (used when addGutter is true)") + pages_per_sheet: PagesPerSheet | None = Field( + PagesPerSheet.number_2, + description="The number of pages per side for booklet printing (always 2 for proper booklet).", + ) + spine_location: SpineLocation | None = Field(SpineLocation.left, description="The spine location for the booklet.") + + +class CbrToPdfParams(ApiModel): + optimize_for_ebook: bool | None = Field( + False, description="Optimize the output PDF for ebook reading using Ghostscript" + ) + + +class CbzToPdfParams(ApiModel): + optimize_for_ebook: bool | None = Field( + False, description="Optimize the output PDF for ebook reading using Ghostscript" + ) + + +class CertType(StrEnum): + pem = "PEM" + pkcs12 = "PKCS12" + pfx = "PFX" + jks = "JKS" + server = "SERVER" + + +class CertSignParams(ApiModel): + cert_type: CertType | None = Field(None, description="The type of the digital certificate") + location: str | None = Field("SPDF", description="The location where the PDF is signed") + name: str | None = Field("SPDF", description="The name of the signer") + page_number: int | None = Field( + 1, + description="The page number where the signature should be visible. This is required if showSignature is set to true", + ) + password: SecretStr | None = Field(None, description="The password for the keystore or the private key") + reason: str | None = Field("Signed by SPDF", description="The reason for signing the PDF") + show_logo: bool | None = Field( + True, description="Whether to visually show a signature logo along with the signature" + ) + show_signature: bool | None = Field(False, description="Whether to visually show the signature in the PDF file") + + +class LineArtEdgeLevel(IntEnum): + integer_1 = 1 + integer_2 = 2 + integer_3 = 3 + + +class OptimizeLevel(IntEnum): + integer_1 = 1 + integer_2 = 2 + integer_3 = 3 + integer_4 = 4 + integer_5 = 5 + integer_6 = 6 + integer_7 = 7 + integer_8 = 8 + integer_9 = 9 + + +class CompressPdfParams(ApiModel): + expected_output_size: str | None = Field("25KB", description="The expected output size, e.g. '100MB', '25KB', etc.") + grayscale: bool | None = Field(False, description="Whether to convert the PDF to grayscale. Default is false.") + line_art: bool | None = Field( + False, description="Whether to convert images to high-contrast line art using ImageMagick. Default is false." + ) + line_art_edge_level: LineArtEdgeLevel | None = Field( + LineArtEdgeLevel.integer_1, + description="Edge detection strength to use for line art conversion (1-3). This maps to ImageMagick's -edge radius.", + ) + line_art_threshold: float | None = Field(55, description="Threshold to use for line art conversion (0-100).") + linearize: bool | None = Field( + False, description="Whether to linearize the PDF for faster web viewing. Default is false." + ) + normalize: bool | None = Field( + False, description="Whether to normalize the PDF content for better compatibility. Default is false." + ) + optimize_level: OptimizeLevel | None = Field( + None, + description="The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.", + ) class CropParams(ApiModel): - crop_area: dict[str, float] = {"x": 0, "y": 0, "width": 595, "height": 842} + auto_crop: bool | None = Field(None, description="Enable auto-crop to detect and remove white space") + height: float | None = Field(None, description="The height of the crop area") + remove_data_outside_crop: bool | None = Field( + None, description="Whether to remove text outside the crop area (keeps images)" + ) + width: float | None = Field(None, description="The width of the crop area") + x: float | None = Field(None, description="The x-coordinate of the top-left corner of the crop area") + y: float | None = Field(None, description="The y-coordinate of the top-left corner of the crop area") + + +class DeleteAttachmentParams(ApiModel): + attachment_name: str | None = Field(None, description="The name of the attachment to delete") + + +class EmbedAllFonts(Enum): + boolean_true = True + boolean_false = False + + +class IncludePageNumbers(Enum): + boolean_true = True + boolean_false = False + + +class IncludeTableOfContents(Enum): + boolean_true = True + boolean_false = False + + +class OptimizeForEbook(Enum): + boolean_true = True + boolean_false = False + + +class EbookToPdfParams(ApiModel): + embed_all_fonts: EmbedAllFonts | None = Field( + EmbedAllFonts.boolean_false, description="Embed all fonts from the eBook into the generated PDF" + ) + include_page_numbers: IncludePageNumbers | None = Field( + IncludePageNumbers.boolean_false, description="Add page numbers to the generated PDF" + ) + include_table_of_contents: IncludeTableOfContents | None = Field( + IncludeTableOfContents.boolean_false, description="Add a generated table of contents to the resulting PDF" + ) + optimize_for_ebook: OptimizeForEbook | None = Field( + OptimizeForEbook.boolean_false, + description="Optimize the PDF for eBook reading (smaller file size, better rendering on eInk devices)", + ) class EditTableOfContentsParams(ApiModel): - bookmarks: list[dict[str, Any]] = [] - replace_existing: bool = True + bookmark_data: str | None = Field( + None, + description="Bookmark structure in JSON format", + examples=[ + '[{\\"title\\":\\"Chapter 1\\",\\"pageNumber\\":1,\\"children\\":[{\\"title\\":\\"Section 1.1\\",\\"pageNumber\\":2}]}]' + ], + ) + replace_existing: bool | None = Field( + None, description="Whether to replace existing bookmarks or append to them", examples=[True] + ) + + +class EmlToPdfParams(ApiModel): + download_html: bool | None = Field( + None, description="Download HTML intermediate file instead of PDF", examples=[False] + ) + include_all_recipients: bool | None = Field( + None, description="Include CC and BCC recipients in header (if available)", examples=[True] + ) + include_attachments: bool | None = Field( + None, description="Include email attachments in the PDF output", examples=[False] + ) + max_attachment_size_mb: int | None = Field( + None, + description="Maximum attachment size in MB to include (default 10MB, range: 1-100)", + examples=[10], + ge=1, + le=100, + ) + + +class ExtractImageScansParams(ApiModel): + angle_threshold: int | None = Field(5, description="The angle threshold for the image scan extraction") + border_size: int | None = Field(1, description="The border size for the image scan extraction") + min_area: int | None = Field(8000, description="The minimum area for the image scan extraction") + min_contour_area: int | None = Field(500, description="The minimum contour area for the image scan extraction") + tolerance: int | None = Field(20, description="The tolerance for the image scan extraction") + + +class Format(StrEnum): + png = "png" + jpeg = "jpeg" + gif = "gif" class ExtractImagesParams(ApiModel): - allow_duplicates: bool = False - format: Literal["png", "jpg", "gif"] = "png" - - -class ExtractPagesParams(ApiModel): - page_numbers: str = "" + format: Format | None = Field(Format.png, description="The output image format e.g., 'png', 'jpeg', or 'gif'") class FlattenParams(ApiModel): - flatten_only_forms: bool = False + flatten_only_forms: bool | None = Field( + False, description="True to flatten only the forms, false to flatten full PDF (Convert page to image)" + ) + render_dpi: int | None = Field( + None, description="Optional DPI for page rendering when flattening the full document.", ge=72 + ) -class MergeParams(ApiModel): - generate_table_of_contents: bool = False - remove_digital_signature: bool = False +class HtmlToPdfParams(ApiModel): + zoom: float | None = Field(1, description="Zoom level for displaying the website. Default is '1'.") -class OcrParams(ApiModel): - additional_options: list[str] = [] - languages: list[str] = [] - ocr_render_type: str = "hocr" - ocr_type: str = "skip-text" +class ColorType(StrEnum): + color = "color" + greyscale = "greyscale" + blackwhite = "blackwhite" + + +class FitOption(StrEnum): + fill_page = "fillPage" + fit_document_to_image = "fitDocumentToImage" + maintain_aspect_ratio = "maintainAspectRatio" + + +class ImgToPdfParams(ApiModel): + auto_rotate: bool | None = Field( + False, description="Whether to automatically rotate the images to better fit the PDF page" + ) + color_type: ColorType | None = Field(ColorType.color, description="The color type of the output image(s)") + fit_option: FitOption | None = Field( + FitOption.fill_page, description="Option to determine how the image will fit onto the page" + ) + + +class SortType(StrEnum): + order_provided = "orderProvided" + by_file_name = "byFileName" + by_date_modified = "byDateModified" + by_date_created = "byDateCreated" + by_pdf_title = "byPDFTitle" + + +class MergePdfsParams(ApiModel): + client_file_ids: str | None = Field( + None, description="JSON array of client-provided IDs for each uploaded file (same order as fileInput)" + ) + generate_toc: bool | None = Field( + False, + description="Flag indicating whether to generate a table of contents for the merged PDF. If true, a table of contents will be created using the input filenames as chapter names.", + ) + remove_cert_sign: bool | None = Field( + True, + description="Flag indicating whether to remove certification signatures from the merged PDF. If true, all certification signatures will be removed from the final merged document.", + ) + sort_type: SortType | None = Field( + SortType.order_provided, description="The type of sorting to be applied on the input files before merging." + ) + + +class Arrangement(StrEnum): + by_rows = "BY_ROWS" + by_columns = "BY_COLUMNS" + + +class Mode(StrEnum): + default = "DEFAULT" + custom = "CUSTOM" + + +class Orientation(StrEnum): + portrait = "PORTRAIT" + landscape = "LANDSCAPE" + + +class PagesPerSheet1(IntEnum): + integer_2 = 2 + integer_4 = 4 + integer_9 = 9 + integer_16 = 16 + + +class ReadingDirection(StrEnum): + ltr = "LTR" + rtl = "RTL" + + +class MultiPageLayoutParams(ApiModel): + add_border: bool | None = Field(None, description="Boolean for if you wish to add border around the pages") + arrangement: Arrangement | None = Field( + Arrangement.by_rows, + description="The arrangement of pages on the sheet: BY_ROWS fills pages row by row, while BY_COLUMNS fills pages column by column.", + ) + border_width: float | None = Field( + 1, description="Border width (in points) to apply around each page when merging", examples=[2], ge=0.0 + ) + bottom_margin: float | None = Field( + 0, description="Bottom margin (in points) to apply to the output pages when merging", examples=[200], ge=0.0 + ) + cols: float | None = Field(2, description="Number of columns", examples=[2], ge=1.0, le=300.0) + inner_margin: float | None = Field( + 0, description="Inner margin (in points) to apply around each page when merging", examples=[200], ge=0.0 + ) + left_margin: float | None = Field( + 0, description="Left margin (in points) to apply to the output pages when merging", examples=[200], ge=0.0 + ) + mode: Mode | None = Field( + Mode.default, description="Input mode: DEFAULT uses pagesPerSheet; CUSTOM uses explicit cols x rows." + ) + orientation: Orientation | None = Field(Orientation.portrait, description="The orientation of the output PDF pages") + pages_per_sheet: PagesPerSheet1 | None = Field( + None, description="The number of pages to fit onto a single sheet in the output PDF." + ) + reading_direction: ReadingDirection | None = Field( + ReadingDirection.ltr, + description="The direction in which pages are arranged on the sheet: LTR (left-to-right) or RTL (right-to-left).", + ) + right_margin: float | None = Field( + 0, description="Right margin (in points) to apply to the output pages when merging", examples=[200], ge=0.0 + ) + rows: float | None = Field(1, description="Number of rows", examples=[3], ge=1.0, le=300.0) + top_margin: float | None = Field( + 0, description="Top margin (in points) to apply to the output pages when merging", examples=[200], ge=0.0 + ) + + +class OcrRenderType(StrEnum): + hocr = "hocr" + sandwich = "sandwich" + + +class OcrType(StrEnum): + skip_text = "skip-text" + force_ocr = "force-ocr" + normal = "Normal" + + +class OcrPdfParams(ApiModel): + clean: bool | None = Field(None, description="Clean the input file if set to true") + clean_final: bool | None = Field(None, description="Clean the final output if set to true") + deskew: bool | None = Field(None, description="Deskew the input file if set to true") + languages: list[str] | None = Field( + ["eng"], description="List of languages to use in OCR processing, e.g., 'eng', 'deu'" + ) + ocr_render_type: OcrRenderType | None = Field( + OcrRenderType.hocr, description="Specify the OCR render type, either 'hocr' or 'sandwich'" + ) + ocr_type: OcrType | None = Field( + None, description="Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'" + ) + remove_images_after: bool | None = Field(None, description="Remove images from the output PDF if set to true") + sidecar: bool | None = Field(None, description="Include OCR text in a sidecar text file if set to true") + + +class OverlayMode(StrEnum): + sequential_overlay = "SequentialOverlay" + interleaved_overlay = "InterleavedOverlay" + fixed_repeat_overlay = "FixedRepeatOverlay" + + +class OverlayPosition(Enum): + number_0 = 0 + number_1 = 1 class OverlayPdfsParams(ApiModel): - counts: list[float] = [] - overlay_files: list[dict[str, Any]] = [] - overlay_mode: Literal["SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay"] = "SequentialOverlay" - overlay_position: Literal[0, 1] = 0 + counts: list[int] | None = Field( + None, + description="An array of integers specifying the number of times each corresponding overlay file should be applied in the 'FixedRepeatOverlay' mode. This should match the length of the overlayFiles array.", + ) + overlay_files: list[bytes] | None = Field( + None, + description="An array of PDF files to be used as overlays on the base PDF. The order in these files is applied based on the selected mode.", + ) + overlay_mode: OverlayMode | None = Field( + None, + description="The mode of overlaying: 'SequentialOverlay' for sequential application, 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay' for fixed repetition based on provided counts", + ) + overlay_position: OverlayPosition | None = Field( + None, description="Overlay position 0 is Foregound, 1 is Background" + ) -class PageLayoutParams(ApiModel): - add_border: bool = False - pages_per_sheet: float = 4 +class PdfToCbrParams(ApiModel): + dpi: int | None = Field( + None, description="The DPI (Dots Per Inch) for rendering PDF pages as images", examples=[150] + ) -class PdfToSinglePageParams(ApiModel): - pass +class PdfToCbzParams(ApiModel): + dpi: int | None = Field( + None, description="The DPI (Dots Per Inch) for rendering PDF pages as images", examples=[150] + ) -class RedactParams(ApiModel): - convert_pdfto_image: bool = True - custom_padding: float = 0.1 - mode: Literal["automatic", "manual"] = "automatic" - redact_color: str = "#000000" - use_regex: bool = False - whole_word_search: bool = False - words_to_redact: list[str] = [] +class PdfToCsvParams(ApiModel): + page_numbers: str | None = Field( + "all", + description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", + ) -class RemoveAnnotationsParams(ApiModel): - pass +class DetectChapters(Enum): + boolean_true = True + boolean_false = False + + +class OutputFormat(StrEnum): + epub = "EPUB" + azw3 = "AZW3" + epub_1 = "EPUB" + azw3_1 = "AZW3" + + +class TargetDevice(StrEnum): + tablet_phone_images = "TABLET_PHONE_IMAGES" + kindle_eink_text = "KINDLE_EINK_TEXT" + tablet_phone_images_1 = "TABLET_PHONE_IMAGES" + kindle_eink_text_1 = "KINDLE_EINK_TEXT" + + +class PdfToEpubParams(ApiModel): + detect_chapters: DetectChapters | None = Field( + DetectChapters.boolean_true, description="Detect headings that look like chapters and insert EPUB page breaks." + ) + output_format: OutputFormat | None = Field(OutputFormat.epub, description="Choose the output format for the ebook.") + target_device: TargetDevice | None = Field( + TargetDevice.tablet_phone_images, description="Choose an output profile optimized for the reader device." + ) + + +class ImageFormat(StrEnum): + png = "png" + jpeg = "jpeg" + jpg = "jpg" + gif = "gif" + webp = "webp" + + +class SingleOrMultiple(StrEnum): + single = "single" + multiple = "multiple" + + +class PdfToImgParams(ApiModel): + color_type: ColorType | None = Field(ColorType.color, description="The color type of the output image(s)") + dpi: int | None = Field(300, description="The DPI (dots per inch) for the output image(s)") + image_format: ImageFormat | None = Field(ImageFormat.png, description="The output image format") + include_annotations: bool | None = Field( + False, description="Include annotations such as comments in the output image(s)" + ) + page_numbers: str | None = Field( + "all", + description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", + ) + single_or_multiple: SingleOrMultiple | None = Field( + SingleOrMultiple.multiple, + description="Choose between a single image containing all pages or separate images for each page", + ) + + +class OutputFormat1(StrEnum): + pdfa = "pdfa" + pdfa_1 = "pdfa-1" + pdfa_2 = "pdfa-2" + pdfa_2b = "pdfa-2b" + pdfa_3 = "pdfa-3" + pdfa_3b = "pdfa-3b" + pdfx = "pdfx" + + +class PdfToPdfaParams(ApiModel): + output_format: OutputFormat1 | None = Field(None, description="The output format type (PDF/A or PDF/X)") + strict: bool | None = Field( + None, description="If true, the conversion will fail if the output is not perfectly compliant" + ) + + +class OutputFormat2(StrEnum): + ppt = "ppt" + pptx = "pptx" + odp = "odp" + + +class PdfToPresentationParams(ApiModel): + output_format: OutputFormat2 | None = Field(None, description="The output Presentation format") + + +class OutputFormat3(StrEnum): + rtf = "rtf" + txt = "txt" + + +class PdfToTextParams(ApiModel): + output_format: OutputFormat3 | None = Field(None, description="The output Text or RTF format") + + +class OutputFormat4(StrEnum): + eps = "eps" + ps = "ps" + pcl = "pcl" + xps = "xps" + + +class Prepress(Enum): + boolean_true = True + boolean_false = False + + +class PdfToVectorParams(ApiModel): + output_format: OutputFormat4 | None = Field(OutputFormat4.eps, description="Target vector format extension") + prepress: Prepress | None = Field(Prepress.boolean_false, description="Apply Ghostscript prepress settings") + + +class OutputFormat5(StrEnum): + doc = "doc" + docx = "docx" + odt = "odt" + + +class PdfToWordParams(ApiModel): + output_format: OutputFormat5 | None = Field(None, description="The output Word document format") + + +class PdfToXlsxParams(ApiModel): + page_numbers: str | None = Field( + "all", + description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", + ) + + +class CustomMode(StrEnum): + custom = "CUSTOM" + reverse_order = "REVERSE_ORDER" + duplex_sort = "DUPLEX_SORT" + booklet_sort = "BOOKLET_SORT" + side_stitch_booklet_sort = "SIDE_STITCH_BOOKLET_SORT" + odd_even_split = "ODD_EVEN_SPLIT" + remove_first = "REMOVE_FIRST" + remove_last = "REMOVE_LAST" + remove_first_and_last = "REMOVE_FIRST_AND_LAST" + duplicate = "DUPLICATE" + + +class RearrangePagesParams(ApiModel): + custom_mode: CustomMode | None = Field( + None, + description="The custom mode for page rearrangement. Valid values are:\nCUSTOM: Uses order defined in PageNums DUPLICATE: Duplicate pages n times (if Page order defined as 4, then duplicates each page 4 times)REVERSE_ORDER: Reverses the order of all pages.\nDUPLEX_SORT: Sorts pages as if all fronts were scanned then all backs in reverse (1, n, 2, n-1, ...). BOOKLET_SORT: Arranges pages for booklet printing (last, first, second, second last, ...).\nODD_EVEN_SPLIT: Splits and arranges pages into odd and even numbered pages.\nREMOVE_FIRST: Removes the first page.\nREMOVE_LAST: Removes the last page.\nREMOVE_FIRST_AND_LAST: Removes both the first and the last pages.\n", + ) + page_numbers: str | None = Field( + "all", + description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", + ) class RemoveBlanksParams(ApiModel): - include_blank_pages: bool = False - threshold: float = 10 - white_percent: float = 99.9 - - -class RemoveCertSignParams(ApiModel): - pass - - -class RemoveImageParams(ApiModel): - pass + threshold: int | None = Field(10, description="The threshold value to determine blank pages", ge=0, le=255) + white_percent: float | None = Field( + 99.9, description="The percentage of white color on a page to consider it as blank", ge=0.1, le=100.0 + ) class RemovePagesParams(ApiModel): - page_numbers: str = "" + page_numbers: str | None = Field( + "all", + description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", + ) class RemovePasswordParams(ApiModel): - password: str = "" + password: SecretStr | None = Field(None, description="The password of the PDF file") -class ReorganizePagesParams(ApiModel): - custom_mode: str | None = None - page_numbers: str | None = None +class RenameAttachmentParams(ApiModel): + attachment_name: str | None = Field(None, description="The current name of the attachment to rename") + new_name: str | None = Field(None, description="The new name for the attachment") -class RepairParams(ApiModel): - pass +class HighContrastColorCombination(StrEnum): + white_text_on_black = "WHITE_TEXT_ON_BLACK" + black_text_on_white = "BLACK_TEXT_ON_WHITE" + yellow_text_on_black = "YELLOW_TEXT_ON_BLACK" + green_text_on_black = "GREEN_TEXT_ON_BLACK" + white_text_on_black_1 = "WHITE_TEXT_ON_BLACK" + black_text_on_white_1 = "BLACK_TEXT_ON_WHITE" + yellow_text_on_black_1 = "YELLOW_TEXT_ON_BLACK" + green_text_on_black_1 = "GREEN_TEXT_ON_BLACK" -class ReplaceColorParams(ApiModel): - back_ground_color: str = "#ffffff" - high_contrast_color_combination: Literal[ - "WHITE_TEXT_ON_BLACK", "BLACK_TEXT_ON_WHITE", "YELLOW_TEXT_ON_BLACK", "GREEN_TEXT_ON_BLACK" - ] = "WHITE_TEXT_ON_BLACK" - replace_and_invert_option: Literal[ - "HIGH_CONTRAST_COLOR", "CUSTOM_COLOR", "FULL_INVERSION", "COLOR_SPACE_CONVERSION" - ] = "HIGH_CONTRAST_COLOR" - text_color: str = "#000000" +class ReplaceAndInvertOption(StrEnum): + high_contrast_color = "HIGH_CONTRAST_COLOR" + custom_color = "CUSTOM_COLOR" + full_inversion = "FULL_INVERSION" + color_space_conversion = "COLOR_SPACE_CONVERSION" + high_contrast_color_1 = "HIGH_CONTRAST_COLOR" + custom_color_1 = "CUSTOM_COLOR" + full_inversion_1 = "FULL_INVERSION" + color_space_conversion_1 = "COLOR_SPACE_CONVERSION" -class RotateParams(ApiModel): - angle: float = 0 +class ReplaceInvertPdfParams(ApiModel): + back_ground_color: str | None = Field( + None, + description="If CUSTOM_COLOR option selected, then pick the custom color for background. Expected color value should be 24bit decimal value of a color", + ) + high_contrast_color_combination: HighContrastColorCombination | None = Field( + HighContrastColorCombination.white_text_on_black, + description="If HIGH_CONTRAST_COLOR option selected, then pick the default color option for text and background.", + ) + replace_and_invert_option: ReplaceAndInvertOption | None = Field( + ReplaceAndInvertOption.high_contrast_color, description="Replace and Invert color options of a pdf." + ) + text_color: str | None = Field( + None, + description="If CUSTOM_COLOR option selected, then pick the custom color for text. Expected color value should be 24bit decimal value of a color", + ) -class SanitizeParams(ApiModel): - remove_embedded_files: bool = True - remove_fonts: bool = False - remove_java_script: bool = True - remove_links: bool = False - remove_metadata: bool = False - remove_xmpmetadata: bool = False +class Angle(IntEnum): + integer_0 = 0 + integer_90 = 90 + integer_180 = 180 + integer_270 = 270 + + +class RotatePdfParams(ApiModel): + angle: Angle | None = Field( + None, description="The clockwise angle by which to rotate the PDF file. Must be a multiple of 90." + ) + + +class SanitizePdfParams(ApiModel): + remove_embedded_files: bool | None = Field(True, description="Remove embedded files from the PDF") + remove_fonts: bool | None = Field(False, description="Remove fonts from the PDF") + remove_java_script: bool | None = Field(True, description="Remove JavaScript actions from the PDF") + remove_links: bool | None = Field(False, description="Remove links from the PDF") + remove_metadata: bool | None = Field(False, description="Remove document info metadata from the PDF") + remove_xmp_metadata: bool | None = Field(False, description="Remove XMP metadata from the PDF") + + +class PageSize(StrEnum): + a0 = "A0" + a1 = "A1" + a2 = "A2" + a3 = "A3" + a4 = "A4" + a5 = "A5" + a6 = "A6" + letter = "LETTER" + legal = "LEGAL" + keep = "KEEP" class ScalePagesParams(ApiModel): - page_size: Literal["KEEP", "A0", "A1", "A2", "A3", "A4", "A5", "A6", "LETTER", "LEGAL"] | None = None - scale_factor: float = 1 + page_size: PageSize | None = Field( + None, description="The scale of pages in the output PDF. Acceptable values are A0-A6, LETTER, LEGAL, KEEP." + ) + scale_factor: float | None = Field( + 1, description="The scale of the content on the pages of the output PDF. Acceptable values are floats.", ge=0.0 + ) -class ScannerImageSplitParams(ApiModel): - angle_threshold: float = 10 - border_size: float = 1 - min_area: float = 10000 - min_contour_area: float = 500 - tolerance: float = 30 +class Colorspace(StrEnum): + grayscale = "grayscale" + color = "color" -class SignParams(ApiModel): - font_family: str = "Helvetica" - font_size: float = 16 - location: str = "Digital" - reason: str = "Document signing" - signature_data: str | None = None - signature_position: dict[str, float] | None = None - signature_type: Literal["text", "image", "canvas"] = "canvas" - signer_name: str = "" - text_color: str = "#000000" +class Quality(StrEnum): + low = "low" + medium = "medium" + high = "high" -class SplitParams(ApiModel): - allow_duplicates: bool = False - bookmark_level: str = "1" - duplex_mode: bool = False - h_div: str = "2" - include_metadata: bool = False - merge: bool = False - method: Literal[ - "", "byPages", "bySections", "bySize", "byPageCount", "byDocCount", "byChapters", "byPageDivider" - ] = "" - pages: str = "" - split_value: str = "" - v_div: str = "2" +class Rotation(StrEnum): + none = "none" + slight = "slight" + moderate = "moderate" + severe = "severe" -class UnlockPdfformsParams(ApiModel): - pass +class ScannerEffectParams(ApiModel): + advanced_enabled: bool | None = Field(None, description="Whether advanced settings are enabled", examples=[False]) + blur: float | None = Field(None, description="Blur amount (0 = none, higher = more blur)", examples=[1.0]) + border: int | None = Field(None, description="Border thickness in pixels", examples=[20]) + brightness: float | None = Field(None, description="Brightness multiplier (1.0 = no change)", examples=[1.0]) + colorspace: Colorspace | None = Field(None, description="Colorspace for output image", examples=["grayscale"]) + contrast: float | None = Field(None, description="Contrast multiplier (1.0 = no change)", examples=[1.0]) + noise: float | None = Field(None, description="Noise amount (0 = none, higher = more noise)", examples=[8.0]) + quality: Quality | None = Field(None, description="Scan quality preset", examples=["high"]) + resolution: int | None = Field(None, description="Rendering resolution in DPI", examples=[300]) + rotate: int | None = Field(None, description="Base rotation in degrees", examples=[0]) + rotate_variance: int | None = Field(None, description="Random rotation variance in degrees", examples=[2]) + rotation: Rotation | None = Field(None, description="Rotation preset", examples=["none"]) + rotation_value: int | None = None + yellowish: bool | None = Field(None, description="Simulate yellowed paper", examples=[False]) -class WatermarkParams(ApiModel): - alphabet: str = "roman" - convert_pdfto_image: bool = False - custom_color: str = "#d3d3d3" - font_size: float = 12 - height_spacer: float = 50 - opacity: float = 50 - rotation: float = 0 - watermark_image: dict[str, Any] | None = None - watermark_text: str = "" - watermark_type: Literal["text", "image"] | None = None - width_spacer: float = 50 +class WorkflowType(StrEnum): + signing = "SIGNING" + review = "REVIEW" + approval = "APPROVAL" + + +class Request(ApiModel): + document_name: str | None = None + due_date: str | None = None + message: str | None = None + owner_email: str | None = None + participant_emails: list[str] | None = None + participant_user_ids: list[int] | None = None + workflow_metadata: str | None = None + workflow_type: WorkflowType | None = None + + +class SessionsParams(ApiModel): + request: Request | None = None + + +class SplitBySizeOrCountParams(ApiModel): + split_type: int | None = Field( + 0, description="Determines the type of split: 0 for size, 1 for page count, 2 for document count" + ) + split_value: str | None = Field( + "10MB", description="Value for split: size in MB (e.g., '10MB') or number of pages (e.g., '5')" + ) + + +class PageSize1(StrEnum): + a4 = "A4" + letter = "Letter" + a3 = "A3" + a5 = "A5" + legal = "Legal" + tabloid = "Tabloid" + + +class SplitForPosterPrintParams(ApiModel): + page_size: PageSize1 | None = Field( + None, description="Target page size for output chunks (e.g., 'A4', 'Letter', 'A3')" + ) + right_to_left: bool | None = Field(False, description="Split right-to-left instead of left-to-right") + xfactor: int | None = None + yfactor: int | None = None + + +class SplitPagesParams(ApiModel): + page_numbers: str | None = Field( + "all", + description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", + ) + + +class SplitPdfByChaptersParams(ApiModel): + allow_duplicates: bool | None = Field(False, description="Whether to allow duplicates or not") + bookmark_level: int | None = Field(0, description="Maximum bookmark level required", ge=0) + include_metadata: bool | None = Field(False, description="Whether to include Metadata or not") + + +class SplitMode(StrEnum): + custom = "CUSTOM" + split_all_except_first_and_last = "SPLIT_ALL_EXCEPT_FIRST_AND_LAST" + split_all_except_first = "SPLIT_ALL_EXCEPT_FIRST" + split_all_except_last = "SPLIT_ALL_EXCEPT_LAST" + split_all = "SPLIT_ALL" + + +class SplitPdfBySectionsParams(ApiModel): + horizontal_divisions: int | None = Field( + 0, description="Number of horizontal divisions for each PDF page", ge=0, le=50 + ) + merge: bool | None = Field(False, description="Merge the split documents into a single PDF") + page_numbers: str | None = Field("SPLIT_ALL", description="Pages to be split by section") + split_mode: SplitMode | None = Field( + None, + description="Modes for page split. Valid values are:\nSPLIT_ALL_EXCEPT_FIRST_AND_LAST: Splits all except the first and the last pages.\nSPLIT_ALL_EXCEPT_FIRST: Splits all except the first page.\nSPLIT_ALL_EXCEPT_LAST: Splits all except the last page.\nSPLIT_ALL: Splits all pages.\nCUSTOM: Custom split.\n", + ) + vertical_divisions: int | None = Field(1, description="Number of vertical divisions for each PDF page", ge=0, le=50) + + +class SvgToPdfParams(ApiModel): + combine_into_single_pdf: bool | None = Field( + False, + description="Whether to combine all SVG files into a single PDF (each SVG as a separate page) or create separate PDF files for each SVG.", + ) + + +class TimestampPdfParams(ApiModel): + tsa_url: str | None = Field( + "http://timestamp.digicert.com", + description="URL of the RFC 3161 Time Stamp Authority (TSA) server. Must be one of the built-in presets (DigiCert, Sectigo, SSL.com, FreeTSA, MeSign) or an admin-configured URL in settings.yml (security.timestamp.customTsaUrls). If omitted, the server default is used.", + ) + + +class Trapped(StrEnum): + true = "True" + false = "False" + unknown = "Unknown" + + +class UpdateMetadataParams(ApiModel): + all_request_params: dict[str, str] | None = Field( + None, + description="Map list of key and value of custom parameters. Note these must start with customKey and customValue if they are non-standard", + ) + author: str | None = Field("author", description="The author of the document") + creation_date: str | None = Field( + "2023/10/01 12:00:00", + description="The creation date of the document (format: yyyy/MM/dd HH:mm:ss)", + pattern="yyyy/MM/dd HH:mm:ss", + ) + creator: str | None = Field("creator", description="The creator of the document") + delete_all: bool | None = Field(False, description="Delete all metadata if set to true") + keywords: str | None = Field("keywords", description="The keywords for the document") + modification_date: str | None = Field( + "2023/10/01 12:00:00", + description="The modification date of the document (format: yyyy/MM/dd HH:mm:ss)", + pattern="yyyy/MM/dd HH:mm:ss", + ) + producer: str | None = Field("producer", description="The producer of the document") + subject: str | None = Field("subject", description="The subject of the document") + title: str | None = Field("title", description="The title of the document") + trapped: Trapped | None = Field(Trapped.false, description="The trapped status of the document") + + +class UrlToPdfParams(ApiModel): + url_input: str | None = Field(None, description="The input URL to be converted to a PDF file") + + +class OutputFormat6(StrEnum): + eps = "eps" + ps = "ps" + pcl = "pcl" + xps = "xps" + + +class VectorToPdfParams(ApiModel): + output_format: OutputFormat6 | None = Field(OutputFormat6.eps, description="Target vector format extension") + prepress: Prepress | None = Field(Prepress.boolean_false, description="Apply Ghostscript prepress settings") + + +class RedactionArea(RootModel[Any]): + root: Any + + +class RedactParams(ApiModel): + convert_pdf_to_image: bool | None = Field(False, description="Convert the redacted PDF to an image") + page_numbers: str | None = Field( + "all", + description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", + ) + page_redaction_color: str | None = Field("#000000", description="The color used to fully redact certain pages") + redactions: list[RedactionArea] | None = Field(None, description="A list of areas that should be redacted") + + +class Model( + RootModel[ + CbrToPdfParams + | CbzToPdfParams + | EbookToPdfParams + | EmlToPdfParams + | HtmlToPdfParams + | ImgToPdfParams + | PdfToCbrParams + | PdfToCbzParams + | PdfToCsvParams + | PdfToEpubParams + | PdfToImgParams + | PdfToPdfaParams + | PdfToPresentationParams + | PdfToTextParams + | PdfToVectorParams + | PdfToWordParams + | PdfToXlsxParams + | SvgToPdfParams + | UrlToPdfParams + | VectorToPdfParams + | BookletImpositionParams + | CropParams + | EditTableOfContentsParams + | MergePdfsParams + | MultiPageLayoutParams + | OverlayPdfsParams + | RearrangePagesParams + | RemovePagesParams + | RotatePdfParams + | ScalePagesParams + | SplitBySizeOrCountParams + | SplitForPosterPrintParams + | SplitPagesParams + | SplitPdfByChaptersParams + | SplitPdfBySectionsParams + | AddAttachmentsParams + | AddImageParams + | AddPageNumbersParams + | AddStampParams + | AutoRenameParams + | AutoSplitPdfParams + | CompressPdfParams + | DeleteAttachmentParams + | ExtractImageScansParams + | ExtractImagesParams + | FlattenParams + | OcrPdfParams + | RemoveBlanksParams + | RenameAttachmentParams + | ReplaceInvertPdfParams + | ScannerEffectParams + | UpdateMetadataParams + | AddPasswordParams + | AddWatermarkParams + | AutoRedactParams + | CertSignParams + | SessionsParams + | RedactParams + | RemovePasswordParams + | SanitizePdfParams + | TimestampPdfParams + ] +): + root: ( + CbrToPdfParams + | CbzToPdfParams + | EbookToPdfParams + | EmlToPdfParams + | HtmlToPdfParams + | ImgToPdfParams + | PdfToCbrParams + | PdfToCbzParams + | PdfToCsvParams + | PdfToEpubParams + | PdfToImgParams + | PdfToPdfaParams + | PdfToPresentationParams + | PdfToTextParams + | PdfToVectorParams + | PdfToWordParams + | PdfToXlsxParams + | SvgToPdfParams + | UrlToPdfParams + | VectorToPdfParams + | BookletImpositionParams + | CropParams + | EditTableOfContentsParams + | MergePdfsParams + | MultiPageLayoutParams + | OverlayPdfsParams + | RearrangePagesParams + | RemovePagesParams + | RotatePdfParams + | ScalePagesParams + | SplitBySizeOrCountParams + | SplitForPosterPrintParams + | SplitPagesParams + | SplitPdfByChaptersParams + | SplitPdfBySectionsParams + | AddAttachmentsParams + | AddImageParams + | AddPageNumbersParams + | AddStampParams + | AutoRenameParams + | AutoSplitPdfParams + | CompressPdfParams + | DeleteAttachmentParams + | ExtractImageScansParams + | ExtractImagesParams + | FlattenParams + | OcrPdfParams + | RemoveBlanksParams + | RenameAttachmentParams + | ReplaceInvertPdfParams + | ScannerEffectParams + | UpdateMetadataParams + | AddPasswordParams + | AddWatermarkParams + | AutoRedactParams + | CertSignParams + | SessionsParams + | RedactParams + | RemovePasswordParams + | SanitizePdfParams + | TimestampPdfParams + ) type ParamToolModel = ( - AddAttachmentsParams - | AddPasswordParams - | AdjustContrastParams - | AutoRenameParams - | AutomateParams + CbrToPdfParams + | CbzToPdfParams + | EbookToPdfParams + | EmlToPdfParams + | HtmlToPdfParams + | ImgToPdfParams + | PdfToCbrParams + | PdfToCbzParams + | PdfToCsvParams + | PdfToEpubParams + | PdfToImgParams + | PdfToPdfaParams + | PdfToPresentationParams + | PdfToTextParams + | PdfToVectorParams + | PdfToWordParams + | PdfToXlsxParams + | SvgToPdfParams + | UrlToPdfParams + | VectorToPdfParams | BookletImpositionParams - | CertSignParams - | ChangeMetadataParams - | ChangePermissionsParams - | CompressParams - | ConvertParams | CropParams | EditTableOfContentsParams - | ExtractImagesParams - | ExtractPagesParams - | FlattenParams - | MergeParams - | OcrParams + | MergePdfsParams + | MultiPageLayoutParams | OverlayPdfsParams - | PageLayoutParams - | PdfToSinglePageParams - | RedactParams - | RemoveAnnotationsParams - | RemoveBlanksParams - | RemoveCertSignParams - | RemoveImageParams + | RearrangePagesParams | RemovePagesParams - | RemovePasswordParams - | ReorganizePagesParams - | RepairParams - | ReplaceColorParams - | RotateParams - | SanitizeParams + | RotatePdfParams | ScalePagesParams - | ScannerImageSplitParams - | SignParams - | SplitParams - | UnlockPdfformsParams - | WatermarkParams + | SplitBySizeOrCountParams + | SplitForPosterPrintParams + | SplitPagesParams + | SplitPdfByChaptersParams + | SplitPdfBySectionsParams + | AddAttachmentsParams + | AddImageParams + | AddPageNumbersParams + | AddStampParams + | AutoRenameParams + | AutoSplitPdfParams + | CompressPdfParams + | DeleteAttachmentParams + | ExtractImageScansParams + | ExtractImagesParams + | FlattenParams + | OcrPdfParams + | RemoveBlanksParams + | RenameAttachmentParams + | ReplaceInvertPdfParams + | ScannerEffectParams + | UpdateMetadataParams + | AddPasswordParams + | AddWatermarkParams + | AutoRedactParams + | CertSignParams + | SessionsParams + | RedactParams + | RemovePasswordParams + | SanitizePdfParams + | TimestampPdfParams ) type ParamToolModelType = type[ParamToolModel] -class OperationId(StrEnum): - ADD_ATTACHMENTS = "addAttachments" - ADD_PASSWORD = "addPassword" - ADJUST_CONTRAST = "adjustContrast" - AUTO_RENAME = "autoRename" - AUTOMATE = "automate" - BOOKLET_IMPOSITION = "bookletImposition" - CERT_SIGN = "certSign" - CHANGE_METADATA = "changeMetadata" - CHANGE_PERMISSIONS = "changePermissions" - COMPRESS = "compress" - CONVERT = "convert" - CROP = "crop" - EDIT_TABLE_OF_CONTENTS = "editTableOfContents" - EXTRACT_IMAGES = "extractImages" - EXTRACT_PAGES = "extractPages" - FLATTEN = "flatten" - MERGE = "merge" - OCR = "ocr" - OVERLAY_PDFS = "overlayPdfs" - PAGE_LAYOUT = "pageLayout" - PDF_TO_SINGLE_PAGE = "pdfToSinglePage" - REDACT = "redact" - REMOVE_ANNOTATIONS = "removeAnnotations" - REMOVE_BLANKS = "removeBlanks" - REMOVE_CERT_SIGN = "removeCertSign" - REMOVE_IMAGE = "removeImage" - REMOVE_PAGES = "removePages" - REMOVE_PASSWORD = "removePassword" - REORGANIZE_PAGES = "reorganizePages" - REPAIR = "repair" - REPLACE_COLOR = "replaceColor" - ROTATE = "rotate" - SANITIZE = "sanitize" - SCALE_PAGES = "scalePages" - SCANNER_IMAGE_SPLIT = "scannerImageSplit" - SIGN = "sign" - SPLIT = "split" - UNLOCK_PDFFORMS = "unlockPDFForms" - WATERMARK = "watermark" +class ToolEndpoint(StrEnum): + CBR_TO_PDF = "/api/v1/convert/cbr/pdf" + CBZ_TO_PDF = "/api/v1/convert/cbz/pdf" + EBOOK_TO_PDF = "/api/v1/convert/ebook/pdf" + EML_TO_PDF = "/api/v1/convert/eml/pdf" + HTML_TO_PDF = "/api/v1/convert/html/pdf" + IMG_TO_PDF = "/api/v1/convert/img/pdf" + PDF_TO_CBR = "/api/v1/convert/pdf/cbr" + PDF_TO_CBZ = "/api/v1/convert/pdf/cbz" + PDF_TO_CSV = "/api/v1/convert/pdf/csv" + PDF_TO_EPUB = "/api/v1/convert/pdf/epub" + PDF_TO_IMG = "/api/v1/convert/pdf/img" + 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_VECTOR = "/api/v1/convert/pdf/vector" + PDF_TO_WORD = "/api/v1/convert/pdf/word" + PDF_TO_XLSX = "/api/v1/convert/pdf/xlsx" + SVG_TO_PDF = "/api/v1/convert/svg/pdf" + URL_TO_PDF = "/api/v1/convert/url/pdf" + VECTOR_TO_PDF = "/api/v1/convert/vector/pdf" + BOOKLET_IMPOSITION = "/api/v1/general/booklet-imposition" + CROP = "/api/v1/general/crop" + EDIT_TABLE_OF_CONTENTS = "/api/v1/general/edit-table-of-contents" + MERGE_PDFS = "/api/v1/general/merge-pdfs" + MULTI_PAGE_LAYOUT = "/api/v1/general/multi-page-layout" + OVERLAY_PDFS = "/api/v1/general/overlay-pdfs" + REARRANGE_PAGES = "/api/v1/general/rearrange-pages" + REMOVE_PAGES = "/api/v1/general/remove-pages" + ROTATE_PDF = "/api/v1/general/rotate-pdf" + SCALE_PAGES = "/api/v1/general/scale-pages" + SPLIT_BY_SIZE_OR_COUNT = "/api/v1/general/split-by-size-or-count" + SPLIT_FOR_POSTER_PRINT = "/api/v1/general/split-for-poster-print" + SPLIT_PAGES = "/api/v1/general/split-pages" + SPLIT_PDF_BY_CHAPTERS = "/api/v1/general/split-pdf-by-chapters" + SPLIT_PDF_BY_SECTIONS = "/api/v1/general/split-pdf-by-sections" + ADD_ATTACHMENTS = "/api/v1/misc/add-attachments" + ADD_IMAGE = "/api/v1/misc/add-image" + ADD_PAGE_NUMBERS = "/api/v1/misc/add-page-numbers" + ADD_STAMP = "/api/v1/misc/add-stamp" + AUTO_RENAME = "/api/v1/misc/auto-rename" + AUTO_SPLIT_PDF = "/api/v1/misc/auto-split-pdf" + COMPRESS_PDF = "/api/v1/misc/compress-pdf" + DELETE_ATTACHMENT = "/api/v1/misc/delete-attachment" + EXTRACT_IMAGE_SCANS = "/api/v1/misc/extract-image-scans" + EXTRACT_IMAGES = "/api/v1/misc/extract-images" + FLATTEN = "/api/v1/misc/flatten" + OCR_PDF = "/api/v1/misc/ocr-pdf" + REMOVE_BLANKS = "/api/v1/misc/remove-blanks" + RENAME_ATTACHMENT = "/api/v1/misc/rename-attachment" + REPLACE_INVERT_PDF = "/api/v1/misc/replace-invert-pdf" + SCANNER_EFFECT = "/api/v1/misc/scanner-effect" + UPDATE_METADATA = "/api/v1/misc/update-metadata" + ADD_PASSWORD = "/api/v1/security/add-password" + ADD_WATERMARK = "/api/v1/security/add-watermark" + AUTO_REDACT = "/api/v1/security/auto-redact" + CERT_SIGN = "/api/v1/security/cert-sign" + SESSIONS = "/api/v1/security/cert-sign/sessions" + REDACT = "/api/v1/security/redact" + REMOVE_PASSWORD = "/api/v1/security/remove-password" + SANITIZE_PDF = "/api/v1/security/sanitize-pdf" + TIMESTAMP_PDF = "/api/v1/security/timestamp-pdf" -OPERATIONS: dict[OperationId, ParamToolModelType] = { - OperationId.ADD_ATTACHMENTS: AddAttachmentsParams, - OperationId.ADD_PASSWORD: AddPasswordParams, - OperationId.ADJUST_CONTRAST: AdjustContrastParams, - OperationId.AUTO_RENAME: AutoRenameParams, - OperationId.AUTOMATE: AutomateParams, - OperationId.BOOKLET_IMPOSITION: BookletImpositionParams, - OperationId.CERT_SIGN: CertSignParams, - OperationId.CHANGE_METADATA: ChangeMetadataParams, - OperationId.CHANGE_PERMISSIONS: ChangePermissionsParams, - OperationId.COMPRESS: CompressParams, - OperationId.CONVERT: ConvertParams, - OperationId.CROP: CropParams, - OperationId.EDIT_TABLE_OF_CONTENTS: EditTableOfContentsParams, - OperationId.EXTRACT_IMAGES: ExtractImagesParams, - OperationId.EXTRACT_PAGES: ExtractPagesParams, - OperationId.FLATTEN: FlattenParams, - OperationId.MERGE: MergeParams, - OperationId.OCR: OcrParams, - OperationId.OVERLAY_PDFS: OverlayPdfsParams, - OperationId.PAGE_LAYOUT: PageLayoutParams, - OperationId.PDF_TO_SINGLE_PAGE: PdfToSinglePageParams, - OperationId.REDACT: RedactParams, - OperationId.REMOVE_ANNOTATIONS: RemoveAnnotationsParams, - OperationId.REMOVE_BLANKS: RemoveBlanksParams, - OperationId.REMOVE_CERT_SIGN: RemoveCertSignParams, - OperationId.REMOVE_IMAGE: RemoveImageParams, - OperationId.REMOVE_PAGES: RemovePagesParams, - OperationId.REMOVE_PASSWORD: RemovePasswordParams, - OperationId.REORGANIZE_PAGES: ReorganizePagesParams, - OperationId.REPAIR: RepairParams, - OperationId.REPLACE_COLOR: ReplaceColorParams, - OperationId.ROTATE: RotateParams, - OperationId.SANITIZE: SanitizeParams, - OperationId.SCALE_PAGES: ScalePagesParams, - OperationId.SCANNER_IMAGE_SPLIT: ScannerImageSplitParams, - OperationId.SIGN: SignParams, - OperationId.SPLIT: SplitParams, - OperationId.UNLOCK_PDFFORMS: UnlockPdfformsParams, - OperationId.WATERMARK: WatermarkParams, +OPERATIONS: dict[ToolEndpoint, ParamToolModelType] = { + ToolEndpoint.CBR_TO_PDF: CbrToPdfParams, + ToolEndpoint.CBZ_TO_PDF: CbzToPdfParams, + ToolEndpoint.EBOOK_TO_PDF: EbookToPdfParams, + ToolEndpoint.EML_TO_PDF: EmlToPdfParams, + ToolEndpoint.HTML_TO_PDF: HtmlToPdfParams, + ToolEndpoint.IMG_TO_PDF: ImgToPdfParams, + ToolEndpoint.PDF_TO_CBR: PdfToCbrParams, + ToolEndpoint.PDF_TO_CBZ: PdfToCbzParams, + ToolEndpoint.PDF_TO_CSV: PdfToCsvParams, + ToolEndpoint.PDF_TO_EPUB: PdfToEpubParams, + ToolEndpoint.PDF_TO_IMG: PdfToImgParams, + ToolEndpoint.PDF_TO_PDFA: PdfToPdfaParams, + ToolEndpoint.PDF_TO_PRESENTATION: PdfToPresentationParams, + ToolEndpoint.PDF_TO_TEXT: PdfToTextParams, + ToolEndpoint.PDF_TO_VECTOR: PdfToVectorParams, + ToolEndpoint.PDF_TO_WORD: PdfToWordParams, + ToolEndpoint.PDF_TO_XLSX: PdfToXlsxParams, + ToolEndpoint.SVG_TO_PDF: SvgToPdfParams, + ToolEndpoint.URL_TO_PDF: UrlToPdfParams, + ToolEndpoint.VECTOR_TO_PDF: VectorToPdfParams, + ToolEndpoint.BOOKLET_IMPOSITION: BookletImpositionParams, + ToolEndpoint.CROP: CropParams, + ToolEndpoint.EDIT_TABLE_OF_CONTENTS: EditTableOfContentsParams, + ToolEndpoint.MERGE_PDFS: MergePdfsParams, + ToolEndpoint.MULTI_PAGE_LAYOUT: MultiPageLayoutParams, + ToolEndpoint.OVERLAY_PDFS: OverlayPdfsParams, + ToolEndpoint.REARRANGE_PAGES: RearrangePagesParams, + ToolEndpoint.REMOVE_PAGES: RemovePagesParams, + ToolEndpoint.ROTATE_PDF: RotatePdfParams, + ToolEndpoint.SCALE_PAGES: ScalePagesParams, + ToolEndpoint.SPLIT_BY_SIZE_OR_COUNT: SplitBySizeOrCountParams, + ToolEndpoint.SPLIT_FOR_POSTER_PRINT: SplitForPosterPrintParams, + ToolEndpoint.SPLIT_PAGES: SplitPagesParams, + ToolEndpoint.SPLIT_PDF_BY_CHAPTERS: SplitPdfByChaptersParams, + ToolEndpoint.SPLIT_PDF_BY_SECTIONS: SplitPdfBySectionsParams, + ToolEndpoint.ADD_ATTACHMENTS: AddAttachmentsParams, + ToolEndpoint.ADD_IMAGE: AddImageParams, + ToolEndpoint.ADD_PAGE_NUMBERS: AddPageNumbersParams, + ToolEndpoint.ADD_STAMP: AddStampParams, + ToolEndpoint.AUTO_RENAME: AutoRenameParams, + ToolEndpoint.AUTO_SPLIT_PDF: AutoSplitPdfParams, + ToolEndpoint.COMPRESS_PDF: CompressPdfParams, + ToolEndpoint.DELETE_ATTACHMENT: DeleteAttachmentParams, + ToolEndpoint.EXTRACT_IMAGE_SCANS: ExtractImageScansParams, + ToolEndpoint.EXTRACT_IMAGES: ExtractImagesParams, + ToolEndpoint.FLATTEN: FlattenParams, + ToolEndpoint.OCR_PDF: OcrPdfParams, + ToolEndpoint.REMOVE_BLANKS: RemoveBlanksParams, + ToolEndpoint.RENAME_ATTACHMENT: RenameAttachmentParams, + ToolEndpoint.REPLACE_INVERT_PDF: ReplaceInvertPdfParams, + ToolEndpoint.SCANNER_EFFECT: ScannerEffectParams, + ToolEndpoint.UPDATE_METADATA: UpdateMetadataParams, + ToolEndpoint.ADD_PASSWORD: AddPasswordParams, + ToolEndpoint.ADD_WATERMARK: AddWatermarkParams, + ToolEndpoint.AUTO_REDACT: AutoRedactParams, + ToolEndpoint.CERT_SIGN: CertSignParams, + ToolEndpoint.SESSIONS: SessionsParams, + ToolEndpoint.REDACT: RedactParams, + ToolEndpoint.REMOVE_PASSWORD: RemovePasswordParams, + ToolEndpoint.SANITIZE_PDF: SanitizePdfParams, + ToolEndpoint.TIMESTAMP_PDF: TimestampPdfParams, } diff --git a/engine/src/stirling/rag/README.md b/engine/src/stirling/rag/README.md new file mode 100644 index 0000000000..309a3a7c9b --- /dev/null +++ b/engine/src/stirling/rag/README.md @@ -0,0 +1,88 @@ +# RAG Integration Guide + +## Adding RAG to an Agent + +```python +from pydantic_ai import Agent + +from stirling.services import AppRuntime + +class MyAgent: + def __init__(self, runtime: AppRuntime) -> None: + rag = runtime.rag_capability + self.agent = Agent( + model=runtime.smart_model, + system_prompt="Your prompt here...", + instructions=rag.instructions, + toolsets=[rag.toolset], + ) +``` + +That's it. The agent gets a `search_knowledge` tool it can call autonomously. + +## Scoping to Specific Collections + +Collections are named buckets of indexed documents — think folders. By default an agent searches everything in the store. Pass `collections=` to restrict it to only the docs indexed under those names. + +```python +from stirling.rag import RagCapability + +# Only searches docs indexed under "company-docs" — ignores everything else +scoped = RagCapability(runtime.rag_service, collections=["company-docs"], top_k=3) + +# Searches multiple collections +multi = RagCapability(runtime.rag_service, collections=["company-docs", "product-specs"]) + +# No collections arg = searches all collections in the store +everything = RagCapability(runtime.rag_service) +``` + +## Config + +Non-secret defaults live in the committed `engine/.env`: + +``` +STIRLING_RAG_BACKEND=sqlite # or "pgvector" +STIRLING_RAG_EMBEDDING_MODEL=voyageai:voyage-4 +STIRLING_RAG_STORE_PATH=data/rag.db # used when backend=sqlite +STIRLING_RAG_PGVECTOR_DSN= # used when backend=pgvector +STIRLING_RAG_CHUNK_SIZE=512 +STIRLING_RAG_CHUNK_OVERLAP=64 +STIRLING_RAG_TOP_K=5 +``` + +Provider credentials (and any local overrides) go in the uncommitted `engine/.env.local`: + +``` +VOYAGE_API_KEY=your-key +``` + +## Backends + +**`sqlite`** — Embedded sqlite-vec. Single `.db` file, zero ops. Ideal for dev and self-hosted deployments. + +**`pgvector`** — External PostgreSQL with the `vector` extension. Point `STIRLING_RAG_PGVECTOR_DSN` at your Postgres instance. + +Both backends implement the same `VectorStore` interface, so agents and the RAG service work identically regardless of which you pick. + +For a self-hosted embedding server (e.g. Ollama, TEI, vLLM) set the model string accordingly and point at the server via its native env var: + +``` +# Ollama running on another machine +STIRLING_RAG_EMBEDDING_MODEL=ollama:nomic-embed-text +OLLAMA_HOST=http://192.168.1.50:11434 + +# Any OpenAI-compatible embedding server +STIRLING_RAG_EMBEDDING_MODEL=openai:my-model +OPENAI_BASE_URL=http://192.168.1.50:8080/v1 +``` + +## API Endpoints + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| GET | `/api/v1/rag/status` | Report embedding model and existing collections | +| POST | `/api/v1/rag/index` | Index text into a collection | +| POST | `/api/v1/rag/search` | Search a collection | +| GET | `/api/v1/rag/collections` | List collections | +| DELETE | `/api/v1/rag/collections/{name}` | Delete a collection | diff --git a/engine/src/stirling/rag/__init__.py b/engine/src/stirling/rag/__init__.py new file mode 100644 index 0000000000..3abc1f8180 --- /dev/null +++ b/engine/src/stirling/rag/__init__.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from stirling.rag.capability import RagCapability +from stirling.rag.embedder import EmbeddingService +from stirling.rag.pgvector_store import PgVectorStore +from stirling.rag.service import RagService +from stirling.rag.sqlite_vec_store import SqliteVecStore +from stirling.rag.store import Document, SearchResult, VectorStore + +__all__ = [ + "Document", + "EmbeddingService", + "PgVectorStore", + "RagCapability", + "RagService", + "SearchResult", + "SqliteVecStore", + "VectorStore", +] diff --git a/engine/src/stirling/rag/capability.py b/engine/src/stirling/rag/capability.py new file mode 100644 index 0000000000..9950cf30ac --- /dev/null +++ b/engine/src/stirling/rag/capability.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from collections.abc import Awaitable, Callable + +from pydantic_ai import FunctionToolset +from pydantic_ai.toolsets import AbstractToolset + +from stirling.rag.service import RagService + + +class RagCapability: + """Bundles RAG instructions and the ``search_knowledge`` toolset for agent injection. + + Agents consume this as:: + + rag = runtime.rag_capability + Agent( + ..., + instructions=rag.instructions, + toolsets=[rag.toolset], + ) + + When no collections are pinned, the instructions are generated dynamically at + run time so the agent sees the current list of collections in the store. + """ + + def __init__( + self, + rag_service: RagService, + collections: list[str] | None = None, + top_k: int = 5, + ) -> None: + self._rag_service = rag_service + self._collections = collections + self._top_k = top_k + toolset: FunctionToolset[None] = FunctionToolset() + toolset.add_function(self._search_knowledge, name="search_knowledge") + self._toolset = toolset + + @property + def instructions(self) -> str | Callable[[], Awaitable[str]]: + if self._collections: + return self._static_instructions_text(self._collections) + return self._dynamic_instructions + + @property + def toolset(self) -> AbstractToolset[None]: + return self._toolset + + @staticmethod + def _static_instructions_text(collections: list[str]) -> str: + collection_desc = f"collections: {', '.join(collections)}" + return ( + "You have access to a knowledge base search tool called 'search_knowledge'. " + f"It searches {collection_desc} for relevant information. " + "Use it when the provided context is insufficient to answer the user's question, " + "or when you think additional background information would improve your answer. " + "You do not have to use it if the answer is already clear from the provided text." + ) + + async def _dynamic_instructions(self) -> str: + collections = await self._rag_service.list_collections() + if collections: + names = ", ".join(collections) + collection_desc = f"the following knowledge base collections: {names}" + else: + collection_desc = "the knowledge base (currently empty — no collections indexed yet)" + return ( + "You have access to a knowledge base search tool called 'search_knowledge'. " + f"It searches {collection_desc} for relevant information. " + "Use it when the provided context is insufficient to answer the user's question, " + "or when you think additional background information would improve your answer. " + "You do not have to use it if the answer is already clear from the provided text." + ) + + async def _search_knowledge(self, query: str, max_results: int | None = None) -> str: + """Search the knowledge base for information relevant to the query. + + Args: + query: The search query describing what information you need. + max_results: Maximum number of results to return. + + Returns: + Formatted text with the most relevant knowledge base excerpts. + """ + k = max_results if max_results is not None else self._top_k + if self._collections: + all_results = [] + for col in self._collections: + col_results = await self._rag_service.search(query, collection=col, top_k=k) + all_results.extend(col_results) + all_results.sort(key=lambda r: r.score, reverse=True) + results = all_results[:k] + else: + results = await self._rag_service.search(query, top_k=k) + + if not results: + return "No relevant results found in the knowledge base." + + sections = [] + for i, result in enumerate(results, 1): + source = result.document.metadata.get("source", "unknown") + chunk_idx = result.document.metadata.get("chunk_index", "?") + score = f"{result.score:.3f}" + sections.append( + f"[Result {i} | source: {source}, chunk: {chunk_idx}, relevance: {score}]\n{result.document.text}" + ) + return "\n\n---\n\n".join(sections) diff --git a/engine/src/stirling/rag/chunker.py b/engine/src/stirling/rag/chunker.py new file mode 100644 index 0000000000..689365f6cb --- /dev/null +++ b/engine/src/stirling/rag/chunker.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +import re + +# TODO: replace with pydantic-ai's built-in chunking once +# https://github.com/pydantic/pydantic-ai/issues/3962 lands. + + +def chunk_text(text: str, chunk_size: int = 512, overlap: int = 64) -> list[str]: + """Split text into chunks of approximately chunk_size characters with overlap. + + Splits on paragraph then sentence boundaries to avoid cutting mid-thought. + Returns an empty list for empty/whitespace-only input. + """ + text = text.strip() + if not text: + return [] + + paragraphs = _split_paragraphs(text) + chunks: list[str] = [] + current: list[str] = [] + current_len = 0 + + for para in paragraphs: + para_len = len(para) + + if current_len + para_len <= chunk_size: + current.append(para) + current_len += para_len + continue + + # If the current buffer has content, flush it + if current: + chunks.append("\n\n".join(current)) + + # If this paragraph alone exceeds chunk_size, split it by sentences + if para_len > chunk_size: + sentence_chunks = _split_long_paragraph(para, chunk_size, overlap) + chunks.extend(sentence_chunks) + current = [] + current_len = 0 + else: + # Start new chunk with overlap from previous chunk + overlap_text = _get_overlap(chunks, overlap) if chunks else "" + if overlap_text: + current = [overlap_text, para] + current_len = len(overlap_text) + para_len + else: + current = [para] + current_len = para_len + + if current: + chunks.append("\n\n".join(current)) + + return [c.strip() for c in chunks if c.strip()] + + +def _split_paragraphs(text: str) -> list[str]: + """Split text into paragraphs on double newlines.""" + return [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()] + + +def _split_sentences(text: str) -> list[str]: + """Split text into sentences, keeping the delimiter attached.""" + parts = re.split(r"(?<=[.!?])\s+", text) + return [s.strip() for s in parts if s.strip()] + + +def _split_long_paragraph(paragraph: str, chunk_size: int, overlap: int) -> list[str]: + """Split a single long paragraph into sentence-boundary chunks.""" + sentences = _split_sentences(paragraph) + chunks: list[str] = [] + current: list[str] = [] + current_len = 0 + + for sentence in sentences: + sent_len = len(sentence) + + if current_len + sent_len <= chunk_size: + current.append(sentence) + current_len += sent_len + 1 # +1 for space + continue + + if current: + chunks.append(" ".join(current)) + + # If a single sentence exceeds chunk_size, force-split it + if sent_len > chunk_size: + for i in range(0, sent_len, chunk_size - overlap): + chunks.append(sentence[i : i + chunk_size]) + current = [] + current_len = 0 + else: + overlap_text = _get_overlap(chunks, overlap) if chunks else "" + if overlap_text: + current = [overlap_text, sentence] + current_len = len(overlap_text) + sent_len + 1 + else: + current = [sentence] + current_len = sent_len + + if current: + chunks.append(" ".join(current)) + + return chunks + + +def _get_overlap(chunks: list[str], overlap: int) -> str: + """Extract the last ~`overlap` characters from the most recent chunk, snapped to a word boundary.""" + if not chunks or overlap <= 0: + return "" + last = chunks[-1] + tail = last[-overlap:] if len(last) > overlap else last + # Snap to the nearest word boundary to avoid starting mid-word + space_idx = tail.find(" ") + if space_idx > 0: + tail = tail[space_idx + 1 :] + return tail diff --git a/engine/src/stirling/rag/embedder.py b/engine/src/stirling/rag/embedder.py new file mode 100644 index 0000000000..5df71bc808 --- /dev/null +++ b/engine/src/stirling/rag/embedder.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from pydantic_ai import Embedder + +from stirling.rag.chunker import chunk_text +from stirling.rag.store import Document + + +class EmbeddingService: + """Wraps Pydantic AI's Embedder to provide document chunking and embedding.""" + + def __init__(self, model_name: str, chunk_size: int = 512, chunk_overlap: int = 64) -> None: + self._embedder = Embedder(model_name) + self._chunk_size = chunk_size + self._chunk_overlap = chunk_overlap + + async def embed_query(self, text: str) -> list[float]: + """Embed a search query, optimised for retrieval.""" + result = await self._embedder.embed_query(text) + return list(result.embeddings[0]) + + async def embed_documents(self, texts: list[str]) -> list[list[float]]: + """Embed multiple document texts for indexing.""" + if not texts: + return [] + result = await self._embedder.embed_documents(texts) + return [list(emb) for emb in result.embeddings] + + def chunk_and_prepare( + self, + text: str, + source: str = "", + base_metadata: dict[str, str] | None = None, + ) -> list[Document]: + """Chunk text and return Document objects ready for embedding. + + Each chunk gets a unique ID based on source and chunk index. + """ + chunks = chunk_text(text, self._chunk_size, self._chunk_overlap) + documents: list[Document] = [] + for i, chunk in enumerate(chunks): + meta = dict(base_metadata) if base_metadata else {} + meta["source"] = source + meta["chunk_index"] = str(i) + doc_id = f"{source}:chunk:{i}" if source else f"chunk:{i}" + documents.append(Document(id=doc_id, text=chunk, metadata=meta)) + return documents diff --git a/engine/src/stirling/rag/pgvector_store.py b/engine/src/stirling/rag/pgvector_store.py new file mode 100644 index 0000000000..9eedffb47d --- /dev/null +++ b/engine/src/stirling/rag/pgvector_store.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import json + +import psycopg +from pgvector.psycopg import register_vector_async + +from stirling.rag.store import Document, SearchResult, VectorStore + + +class PgVectorStore(VectorStore): + """PostgreSQL + pgvector backed store. + + Connects to an external Postgres instance (DSN provided via config) and uses the + `vector` extension for similarity search. The schema is created on first use. + """ + + def __init__(self, dsn: str) -> None: + if not dsn: + raise ValueError("pgvector backend requires a non-empty DSN (STIRLING_RAG_PGVECTOR_DSN)") + self._dsn = dsn + self._initialized = False + + async def _connect(self) -> psycopg.AsyncConnection: + conn = await psycopg.AsyncConnection.connect(self._dsn) + await register_vector_async(conn) + return conn + + async def _ensure_schema(self) -> None: + if self._initialized: + return + async with await self._connect() as conn: + async with conn.cursor() as cur: + await cur.execute("CREATE EXTENSION IF NOT EXISTS vector") + await cur.execute( + """ + CREATE TABLE IF NOT EXISTS rag_documents ( + id TEXT NOT NULL, + collection TEXT NOT NULL, + text TEXT NOT NULL, + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + embedding vector NOT NULL, + PRIMARY KEY (id, collection) + ) + """ + ) + await cur.execute("CREATE INDEX IF NOT EXISTS idx_rag_collection ON rag_documents(collection)") + await conn.commit() + self._initialized = True + + async def add_documents( + self, + collection: str, + documents: list[Document], + embeddings: list[list[float]], + ) -> None: + if len(documents) != len(embeddings): + raise ValueError(f"Got {len(documents)} documents but {len(embeddings)} embeddings") + if not documents: + return + + await self._ensure_schema() + async with await self._connect() as conn: + async with conn.cursor() as cur: + for doc, emb in zip(documents, embeddings): + await cur.execute( + """ + INSERT INTO rag_documents (id, collection, text, metadata, embedding) + VALUES (%s, %s, %s, %s::jsonb, %s) + ON CONFLICT (id, collection) + DO UPDATE SET + text = EXCLUDED.text, + metadata = EXCLUDED.metadata, + embedding = EXCLUDED.embedding + """, + (doc.id, collection, doc.text, json.dumps(doc.metadata), emb), + ) + await conn.commit() + + async def search( + self, + collection: str, + query_embedding: list[float], + top_k: int = 5, + ) -> list[SearchResult]: + await self._ensure_schema() + async with await self._connect() as conn: + async with conn.cursor() as cur: + await cur.execute( + """ + SELECT id, text, metadata, 1 - (embedding <=> %s) AS score + FROM rag_documents + WHERE collection = %s + ORDER BY embedding <=> %s + LIMIT %s + """, + (query_embedding, collection, query_embedding, top_k), + ) + rows = await cur.fetchall() + + return [ + SearchResult( + document=Document(id=r[0], text=r[1], metadata=r[2] or {}), + score=float(r[3]), + ) + for r in rows + ] + + async def delete_collection(self, collection: str) -> None: + await self._ensure_schema() + async with await self._connect() as conn: + async with conn.cursor() as cur: + await cur.execute("DELETE FROM rag_documents WHERE collection = %s", (collection,)) + await conn.commit() + + async def list_collections(self) -> list[str]: + await self._ensure_schema() + async with await self._connect() as conn: + async with conn.cursor() as cur: + await cur.execute("SELECT DISTINCT collection FROM rag_documents ORDER BY collection") + rows = await cur.fetchall() + return [r[0] for r in rows] + + async def has_collection(self, collection: str) -> bool: + await self._ensure_schema() + async with await self._connect() as conn: + async with conn.cursor() as cur: + await cur.execute( + "SELECT 1 FROM rag_documents WHERE collection = %s LIMIT 1", + (collection,), + ) + row = await cur.fetchone() + return row is not None diff --git a/engine/src/stirling/rag/service.py b/engine/src/stirling/rag/service.py new file mode 100644 index 0000000000..b8c9c4ab19 --- /dev/null +++ b/engine/src/stirling/rag/service.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import logging + +from stirling.rag.embedder import EmbeddingService +from stirling.rag.store import Document, SearchResult, VectorStore + +logger = logging.getLogger(__name__) + + +class RagService: + """Orchestrates embedding and vector storage for RAG workflows.""" + + def __init__(self, embedder: EmbeddingService, store: VectorStore, default_top_k: int = 5) -> None: + self._embedder = embedder + self._store = store + self._default_top_k = default_top_k + + async def index_text( + self, + collection: str, + text: str, + source: str = "", + metadata: dict[str, str] | None = None, + ) -> int: + """Chunk, embed, and store text. Returns the number of chunks indexed.""" + documents = self._embedder.chunk_and_prepare(text, source=source, base_metadata=metadata) + if not documents: + return 0 + embeddings = await self._embedder.embed_documents([doc.text for doc in documents]) + await self._store.add_documents(collection, documents, embeddings) + return len(documents) + + async def index_documents(self, collection: str, documents: list[Document]) -> int: + """Embed and store pre-chunked documents. Returns the number stored.""" + if not documents: + return 0 + embeddings = await self._embedder.embed_documents([doc.text for doc in documents]) + await self._store.add_documents(collection, documents, embeddings) + return len(documents) + + async def search( + self, + query: str, + collection: str | None = None, + top_k: int | None = None, + ) -> list[SearchResult]: + """Embed query and search across one or all collections. + + If collection is None, searches all available collections and merges results. + """ + k = top_k if top_k is not None else self._default_top_k + query_embedding = await self._embedder.embed_query(query) + + if collection is not None: + if not await self._store.has_collection(collection): + return [] + return await self._store.search(collection, query_embedding, k) + + # Search all collections, skipping any that error (e.g. dimension mismatch) + collections = await self._store.list_collections() + all_results: list[SearchResult] = [] + for col_name in collections: + try: + results = await self._store.search(col_name, query_embedding, k) + all_results.extend(results) + except Exception: # noqa: BLE001 — any backend error on one collection should not stop the others + logger.warning("Skipping collection %s during cross-collection search", col_name, exc_info=True) + + # Sort by score descending, return top_k across all collections + all_results.sort(key=lambda r: r.score, reverse=True) + return all_results[:k] + + async def delete_collection(self, collection: str) -> None: + """Remove a collection and all its documents.""" + await self._store.delete_collection(collection) + + async def list_collections(self) -> list[str]: + """List all available collections.""" + return await self._store.list_collections() diff --git a/engine/src/stirling/rag/sqlite_vec_store.py b/engine/src/stirling/rag/sqlite_vec_store.py new file mode 100644 index 0000000000..b3008dcf22 --- /dev/null +++ b/engine/src/stirling/rag/sqlite_vec_store.py @@ -0,0 +1,227 @@ +from __future__ import annotations + +import asyncio +import json +import math +import re +import sqlite3 +from pathlib import Path + +import sqlite_vec + +from stirling.rag.store import Document, SearchResult, VectorStore + + +class SqliteVecStore(VectorStore): + """sqlite-vec backed vector store. Single-file SQLite database, embedded, no server. + + Each collection gets its own `vec0` virtual table with a fixed embedding dimension + (detected on first insert). Document metadata lives in a regular table joined by rowid. + """ + + def __init__(self, db_path: str | Path) -> None: + is_memory = str(db_path) == ":memory:" + self._db_path: Path | None = None if is_memory else Path(db_path) + + if self._db_path is not None: + self._db_path.parent.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(str(self._db_path), check_same_thread=False) + else: + conn = sqlite3.connect(":memory:", check_same_thread=False) + + conn.enable_load_extension(True) + sqlite_vec.load(conn) + conn.enable_load_extension(False) + if self._db_path is not None: + conn.execute("PRAGMA journal_mode=WAL") + + self._conn = conn + self._lock = asyncio.Lock() + self._init_schema() + + @classmethod + def ephemeral(cls) -> SqliteVecStore: + """In-memory store for testing.""" + return cls(":memory:") + + def _init_schema(self) -> None: + self._conn.execute( + """ + CREATE TABLE IF NOT EXISTS collections ( + name TEXT PRIMARY KEY, + dim INTEGER NOT NULL, + table_name TEXT NOT NULL + ) + """ + ) + self._conn.execute( + """ + CREATE TABLE IF NOT EXISTS documents ( + id TEXT NOT NULL, + collection TEXT NOT NULL, + text TEXT NOT NULL, + metadata TEXT NOT NULL DEFAULT '{}', + vec_rowid INTEGER NOT NULL, + PRIMARY KEY (id, collection) + ) + """ + ) + self._conn.execute("CREATE INDEX IF NOT EXISTS idx_doc_collection ON documents(collection)") + self._conn.commit() + + @staticmethod + def _sanitize_table_name(collection: str) -> str: + safe = re.sub(r"[^a-zA-Z0-9_]", "_", collection) + return f"vec_{safe}" + + @staticmethod + def _normalize(vector: list[float]) -> list[float]: + norm = math.sqrt(sum(x * x for x in vector)) + if norm == 0: + return list(vector) + return [x / norm for x in vector] + + async def add_documents( + self, + collection: str, + documents: list[Document], + embeddings: list[list[float]], + ) -> None: + if len(documents) != len(embeddings): + raise ValueError(f"Got {len(documents)} documents but {len(embeddings)} embeddings") + if not documents: + return + + async with self._lock: + await asyncio.to_thread(self._sync_add, collection, documents, embeddings) + + def _sync_add( + self, + collection: str, + documents: list[Document], + embeddings: list[list[float]], + ) -> None: + dim = len(embeddings[0]) + row = self._conn.execute("SELECT dim, table_name FROM collections WHERE name = ?", (collection,)).fetchone() + if row is None: + table_name = self._sanitize_table_name(collection) + self._conn.execute(f"CREATE VIRTUAL TABLE IF NOT EXISTS {table_name} USING vec0(embedding float[{dim}])") + self._conn.execute( + "INSERT INTO collections(name, dim, table_name) VALUES (?, ?, ?)", + (collection, dim, table_name), + ) + else: + existing_dim, table_name = row + if existing_dim != dim: + raise ValueError(f"Collection {collection} has dim {existing_dim}, got embedding of dim {dim}") + + # Upsert: delete existing docs with matching IDs first + ids = [doc.id for doc in documents] + placeholders = ",".join("?" * len(ids)) + existing = self._conn.execute( + f"SELECT vec_rowid FROM documents WHERE collection = ? AND id IN ({placeholders})", + (collection, *ids), + ).fetchall() + if existing: + vec_rowids = [r[0] for r in existing] + row_placeholders = ",".join("?" * len(vec_rowids)) + self._conn.execute( + f"DELETE FROM {table_name} WHERE rowid IN ({row_placeholders})", + vec_rowids, + ) + self._conn.execute( + f"DELETE FROM documents WHERE collection = ? AND id IN ({placeholders})", + (collection, *ids), + ) + + for doc, emb in zip(documents, embeddings): + normalized = self._normalize(list(emb)) + cursor = self._conn.execute( + f"INSERT INTO {table_name}(embedding) VALUES (?)", + (sqlite_vec.serialize_float32(normalized),), + ) + vec_rowid = cursor.lastrowid + self._conn.execute( + "INSERT INTO documents(id, collection, text, metadata, vec_rowid) VALUES (?, ?, ?, ?, ?)", + (doc.id, collection, doc.text, json.dumps(doc.metadata), vec_rowid), + ) + self._conn.commit() + + async def search( + self, + collection: str, + query_embedding: list[float], + top_k: int = 5, + ) -> list[SearchResult]: + async with self._lock: + return await asyncio.to_thread(self._sync_search, collection, query_embedding, top_k) + + def _sync_search( + self, + collection: str, + query_embedding: list[float], + top_k: int, + ) -> list[SearchResult]: + row = self._conn.execute("SELECT table_name, dim FROM collections WHERE name = ?", (collection,)).fetchone() + if row is None: + return [] + table_name, dim = row + if len(query_embedding) != dim: + raise ValueError(f"Query embedding dim {len(query_embedding)} does not match collection dim {dim}") + + normalized = self._normalize(list(query_embedding)) + query_blob = sqlite_vec.serialize_float32(normalized) + + results = self._conn.execute( + f""" + SELECT d.id, d.text, d.metadata, v.distance + FROM {table_name} v + JOIN documents d ON d.vec_rowid = v.rowid AND d.collection = ? + WHERE v.embedding MATCH ? AND k = ? + ORDER BY v.distance + """, + (collection, query_blob, top_k), + ).fetchall() + + return [ + SearchResult( + document=Document( + id=r[0], + text=r[1], + metadata=json.loads(r[2]) if r[2] else {}, + ), + # For normalized vectors: cosine_sim = 1 - (L2^2 / 2) + score=max(0.0, 1.0 - (r[3] ** 2) / 2.0), + ) + for r in results + ] + + async def delete_collection(self, collection: str) -> None: + async with self._lock: + await asyncio.to_thread(self._sync_delete_collection, collection) + + def _sync_delete_collection(self, collection: str) -> None: + row = self._conn.execute("SELECT table_name FROM collections WHERE name = ?", (collection,)).fetchone() + if row is None: + return + table_name = row[0] + self._conn.execute(f"DROP TABLE IF EXISTS {table_name}") + self._conn.execute("DELETE FROM documents WHERE collection = ?", (collection,)) + self._conn.execute("DELETE FROM collections WHERE name = ?", (collection,)) + self._conn.commit() + + async def list_collections(self) -> list[str]: + async with self._lock: + return await asyncio.to_thread(self._sync_list_collections) + + def _sync_list_collections(self) -> list[str]: + rows = self._conn.execute("SELECT name FROM collections ORDER BY name").fetchall() + return [r[0] for r in rows] + + async def has_collection(self, collection: str) -> bool: + async with self._lock: + return await asyncio.to_thread(self._sync_has_collection, collection) + + def _sync_has_collection(self, collection: str) -> bool: + row = self._conn.execute("SELECT 1 FROM collections WHERE name = ?", (collection,)).fetchone() + return row is not None diff --git a/engine/src/stirling/rag/store.py b/engine/src/stirling/rag/store.py new file mode 100644 index 0000000000..1ad0dffbf5 --- /dev/null +++ b/engine/src/stirling/rag/store.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field + + +@dataclass +class Document: + """A chunk of text with metadata, ready for embedding and storage.""" + + id: str + text: str + metadata: dict[str, str] = field(default_factory=dict) + + +@dataclass +class SearchResult: + """A document returned from a vector search with its relevance score.""" + + document: Document + score: float + + +class VectorStore(ABC): + """Abstract interface for vector storage backends. + + Implementations must handle persistence, collection management, + and nearest-neighbor search over pre-computed embeddings. + """ + + @abstractmethod + async def add_documents( + self, + collection: str, + documents: list[Document], + embeddings: list[list[float]], + ) -> None: + """Store documents with their embeddings in the named collection.""" + + @abstractmethod + async def search( + self, + collection: str, + query_embedding: list[float], + top_k: int = 5, + ) -> list[SearchResult]: + """Return the top_k most similar documents from the collection.""" + + @abstractmethod + async def delete_collection(self, collection: str) -> None: + """Remove a collection and all its documents.""" + + @abstractmethod + async def list_collections(self) -> list[str]: + """Return names of all existing collections.""" + + @abstractmethod + async def has_collection(self, collection: str) -> bool: + """Check whether a collection exists.""" diff --git a/engine/src/stirling/services/__init__.py b/engine/src/stirling/services/__init__.py index d4c79f4910..8894f41149 100644 --- a/engine/src/stirling/services/__init__.py +++ b/engine/src/stirling/services/__init__.py @@ -1,9 +1,11 @@ """Shared services used by the Stirling AI runtime.""" from .runtime import AppRuntime, build_model_settings, build_runtime +from .tracking import setup_posthog_tracking __all__ = [ "AppRuntime", "build_model_settings", "build_runtime", + "setup_posthog_tracking", ] diff --git a/engine/src/stirling/services/runtime.py b/engine/src/stirling/services/runtime.py index 656e7d9512..3b91abbd79 100644 --- a/engine/src/stirling/services/runtime.py +++ b/engine/src/stirling/services/runtime.py @@ -1,11 +1,23 @@ from __future__ import annotations +import logging from dataclasses import dataclass +from typing import assert_never from pydantic_ai.models import Model, infer_model from pydantic_ai.settings import ModelSettings -from stirling.config import AppSettings +from stirling.config import ENGINE_ROOT, AppSettings, RagBackend +from stirling.rag import ( + EmbeddingService, + PgVectorStore, + RagCapability, + RagService, + SqliteVecStore, + VectorStore, +) + +logger = logging.getLogger(__name__) @dataclass(frozen=True) @@ -13,6 +25,8 @@ class AppRuntime: settings: AppSettings fast_model: Model smart_model: Model + rag_service: RagService + rag_capability: RagCapability @property def fast_model_settings(self) -> ModelSettings: @@ -39,13 +53,47 @@ def validate_structured_output_support(model: Model, model_name: str) -> None: raise ValueError(f"Unsupported model {model_name}. This model does not support structured outputs.") +def _build_vector_store(settings: AppSettings) -> VectorStore: + """Build the configured vector store backend.""" + if settings.rag_backend == RagBackend.SQLITE: + store_path = settings.rag_store_path + # Treat ":memory:" as a special in-process token; otherwise resolve against the engine root. + if str(store_path) != ":memory:" and not store_path.is_absolute(): + store_path = ENGINE_ROOT / store_path + logger.info("RAG backend=sqlite, db_path=%s", store_path) + return SqliteVecStore(db_path=store_path) + if settings.rag_backend == RagBackend.PGVECTOR: + logger.info("RAG backend=pgvector, dsn=") + return PgVectorStore(dsn=settings.rag_pgvector_dsn) + assert_never(settings.rag_backend) + + +def _build_rag(settings: AppSettings) -> tuple[RagService, RagCapability]: + """Build the RAG service and capability.""" + logger.info("RAG: embedding_model=%s", settings.rag_embedding_model) + embedder = EmbeddingService( + model_name=settings.rag_embedding_model, + chunk_size=settings.rag_chunk_size, + chunk_overlap=settings.rag_chunk_overlap, + ) + store = _build_vector_store(settings) + service = RagService(embedder=embedder, store=store, default_top_k=settings.rag_default_top_k) + capability = RagCapability(rag_service=service, top_k=settings.rag_default_top_k) + return service, capability + + def build_runtime(settings: AppSettings) -> AppRuntime: fast_model = infer_model(settings.fast_model_name) smart_model = infer_model(settings.smart_model_name) validate_structured_output_support(fast_model, settings.fast_model_name) validate_structured_output_support(smart_model, settings.smart_model_name) + + rag_service, rag_capability = _build_rag(settings) + return AppRuntime( settings=settings, fast_model=fast_model, smart_model=smart_model, + rag_service=rag_service, + rag_capability=rag_capability, ) diff --git a/engine/src/stirling/services/tracking.py b/engine/src/stirling/services/tracking.py new file mode 100644 index 0000000000..f8c93524a1 --- /dev/null +++ b/engine/src/stirling/services/tracking.py @@ -0,0 +1,233 @@ +from __future__ import annotations + +import json +from collections import OrderedDict +from collections.abc import Mapping +from contextvars import ContextVar +from typing import Any + +from opentelemetry.context import Context +from opentelemetry.sdk.trace import ReadableSpan, SpanProcessor, TracerProvider +from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import ( # No public import for these constants yet + GEN_AI_INPUT_MESSAGES, + GEN_AI_OPERATION_NAME, + GEN_AI_OUTPUT_MESSAGES, + GEN_AI_REQUEST_MAX_TOKENS, + GEN_AI_REQUEST_MODEL, + GEN_AI_REQUEST_TEMPERATURE, + GEN_AI_RESPONSE_MODEL, + GEN_AI_SYSTEM, + GEN_AI_TOOL_DEFINITIONS, + GEN_AI_USAGE_INPUT_TOKENS, + GEN_AI_USAGE_OUTPUT_TOKENS, + GenAiOperationNameValues, +) +from opentelemetry.semconv.attributes.server_attributes import SERVER_ADDRESS, SERVER_PORT +from opentelemetry.trace import Span +from posthog.client import Client as PostHogClient + +from stirling.config import AppSettings + +# Per-request user ID, set by middleware from the X-User-Id header. +# When not set, PostHog generates a random ID and marks the event as personless. +current_user_id: ContextVar[str | None] = ContextVar("current_user_id", default=None) + + +class LRUSet: + """Least Recently Used Set: a set with a maximum size that evicts the oldest entries first.""" + + def __init__(self, max_size: int) -> None: + self._max_size = max_size + self._data: OrderedDict[str, None] = OrderedDict() + + def __contains__(self, key: str) -> bool: + return key in self._data + + def add(self, key: str) -> None: + self._data[key] = None + if len(self._data) > self._max_size: + self._data.popitem(last=False) + + +def _parse_json_attr(attrs: Mapping[str, Any], key: str) -> Any | None: + """Parse a JSON string span attribute, returning None on failure.""" + raw = attrs.get(key) + if raw is None: + return None + try: + return json.loads(str(raw)) + except (json.JSONDecodeError, TypeError): + return None + + +def _transform_output_choices(choices: list[Any]) -> list[Any]: + """Transform Pydantic AI's parts-based output format to PostHog-compatible format. + + Pydantic AI emits: ``[{"role": "assistant", "parts": [{"type": "tool_call", "name": "..."}]}]`` + PostHog expects: ``[{"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "..."}}]}]`` + """ + for choice in choices: + if not isinstance(choice, dict) or "parts" not in choice: + continue + tool_calls = [] + for part in choice.get("parts", []): + if isinstance(part, dict) and part.get("type") == "tool_call": + tool_calls.append( + { + "type": "function", + "id": part.get("id", ""), + "function": {"name": part.get("name", "")}, + } + ) + if tool_calls: + choice["tool_calls"] = tool_calls + choice["content"] = choice.pop("parts") + return choices + + +def _extract_user_message(attrs: Mapping[str, Any]) -> str: + """Extract the last user message text from the input messages span attribute.""" + messages = _parse_json_attr(attrs, GEN_AI_INPUT_MESSAGES) + if not isinstance(messages, list): + return "" + for msg in reversed(messages): + if not isinstance(msg, dict): + continue + if msg.get("role") == "user": + for part in msg.get("parts", []): + if isinstance(part, dict) and part.get("type") == "text": + return str(part.get("content", "")) + return "" + + +# TODO: Replace with an official PostHog integration if one ever exists +class PostHogSpanProcessor(SpanProcessor): + """Translates Pydantic AI OpenTelemetry spans into PostHog $ai_generation events.""" + + def __init__(self, client: PostHogClient) -> None: + self._client = client + self._seen_traces = LRUSet(max_size=10_000) + + def on_start(self, span: Span, parent_context: Context | None = None) -> None: + pass + + def on_end(self, span: ReadableSpan) -> None: + attrs = dict(span.attributes or {}) + if attrs.get(GEN_AI_OPERATION_NAME) != GenAiOperationNameValues.CHAT.value: + return + + properties = self._build_generation_properties(span, attrs) + self._maybe_emit_trace_event(span, attrs, properties) + self._client.capture( + distinct_id=current_user_id.get(), + event="$ai_generation", + properties=properties, + ) + + def _build_generation_properties(self, span: ReadableSpan, attrs: Mapping[str, Any]) -> dict[str, object]: + """Build the $ai_generation event properties from span data.""" + properties: dict[str, object] = { + "$ai_provider": attrs.get(GEN_AI_SYSTEM, ""), + "$ai_model": attrs.get(GEN_AI_RESPONSE_MODEL) or attrs.get(GEN_AI_REQUEST_MODEL, ""), + "$ai_input_tokens": attrs.get(GEN_AI_USAGE_INPUT_TOKENS, 0), + "$ai_output_tokens": attrs.get(GEN_AI_USAGE_OUTPUT_TOKENS, 0), + } + + if span.context: + properties["$ai_trace_id"] = format(span.context.trace_id, "032x") + properties["$ai_span_id"] = format(span.context.span_id, "016x") + if span.parent and span.parent.span_id: + properties["$ai_parent_id"] = format(span.parent.span_id, "016x") + if span.start_time and span.end_time: + properties["$ai_latency"] = (span.end_time - span.start_time) / 1e9 + + self._add_message_properties(properties, attrs) + self._add_model_parameters(properties, attrs) + self._add_tool_definitions(properties, attrs) + self._add_base_url(properties, attrs) + + return properties + + def _maybe_emit_trace_event( + self, span: ReadableSpan, attrs: Mapping[str, Any], properties: dict[str, object] + ) -> None: + """Emit an $ai_trace event for the first span seen per trace ID.""" + trace_id = str(properties.get("$ai_trace_id", "")) + if not trace_id or trace_id in self._seen_traces: + return + + self._seen_traces.add(trace_id) + trace_properties: dict[str, object] = { + "$ai_trace_id": trace_id, + "$ai_trace_name": _extract_user_message(attrs), + "$ai_provider": attrs.get(GEN_AI_SYSTEM, ""), + } + if span.start_time and span.end_time: + trace_properties["$ai_latency"] = (span.end_time - span.start_time) / 1e9 + self._client.capture( + distinct_id=current_user_id.get(), + event="$ai_trace", + properties=trace_properties, + ) + + @staticmethod + def _add_message_properties(properties: dict[str, object], attrs: Mapping[str, Any]) -> None: + input_messages = _parse_json_attr(attrs, GEN_AI_INPUT_MESSAGES) + if input_messages is not None: + properties["$ai_input"] = input_messages + + output_messages = _parse_json_attr(attrs, GEN_AI_OUTPUT_MESSAGES) + if isinstance(output_messages, list): + properties["$ai_output_choices"] = _transform_output_choices(output_messages) + elif output_messages is not None: + properties["$ai_output_choices"] = output_messages + + @staticmethod + def _add_model_parameters(properties: dict[str, object], attrs: Mapping[str, Any]) -> None: + model_parameters: dict[str, object] = {} + if GEN_AI_REQUEST_TEMPERATURE in attrs: + model_parameters["temperature"] = attrs[GEN_AI_REQUEST_TEMPERATURE] + if GEN_AI_REQUEST_MAX_TOKENS in attrs: + model_parameters["max_tokens"] = attrs[GEN_AI_REQUEST_MAX_TOKENS] + if model_parameters: + properties["$ai_model_parameters"] = model_parameters + + @staticmethod + def _add_tool_definitions(properties: dict[str, object], attrs: Mapping[str, Any]) -> None: + tools = _parse_json_attr(attrs, GEN_AI_TOOL_DEFINITIONS) + if tools is not None: + properties["$ai_tools"] = tools + + @staticmethod + def _add_base_url(properties: dict[str, object], attrs: Mapping[str, Any]) -> None: + parts: list[str] = [] + if host := attrs.get(SERVER_ADDRESS): + parts.append(str(host)) + if port := attrs.get(SERVER_PORT): + parts.append(str(port)) + if parts: + properties["$ai_base_url"] = ":".join(parts) + + def shutdown(self) -> None: + self._client.shutdown() + + def force_flush(self, timeout_millis: int = 30000) -> bool: + self._client.flush() + return True + + +def setup_posthog_tracking(settings: AppSettings) -> TracerProvider | None: + """Configure OpenTelemetry with a PostHog span processor for LLM analytics. + + Returns the TracerProvider so it can be shut down on app exit, + or None when tracking is disabled. + """ + if not settings.posthog_enabled or not settings.posthog_api_key: + return None + + client = PostHogClient(project_api_key=settings.posthog_api_key, host=settings.posthog_host) + processor = PostHogSpanProcessor(client) + + provider = TracerProvider() + provider.add_span_processor(processor) + return provider diff --git a/engine/tests/conftest.py b/engine/tests/conftest.py new file mode 100644 index 0000000000..3e1809f769 --- /dev/null +++ b/engine/tests/conftest.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from collections.abc import Iterator +from pathlib import Path + +import pytest + +from stirling.config import AppSettings, RagBackend, load_settings +from stirling.services import build_runtime +from stirling.services.runtime import AppRuntime + + +@pytest.fixture(autouse=True) +def clear_settings_cache() -> Iterator[None]: + load_settings.cache_clear() + yield + load_settings.cache_clear() + + +def build_app_settings() -> AppSettings: + return AppSettings( + smart_model_name="test", + fast_model_name="test", + smart_model_max_tokens=8192, + fast_model_max_tokens=2048, + rag_backend=RagBackend.SQLITE, + rag_embedding_model="voyageai:voyage-4", + rag_store_path=Path(":memory:"), + rag_pgvector_dsn="", + rag_chunk_size=512, + rag_chunk_overlap=64, + rag_default_top_k=5, + posthog_enabled=False, + posthog_api_key="", + posthog_host="https://eu.i.posthog.com", + ) + + +@pytest.fixture +def app_settings() -> AppSettings: + return build_app_settings() + + +@pytest.fixture +def runtime(app_settings: AppSettings) -> AppRuntime: + return build_runtime(app_settings) diff --git a/engine/tests/ledger/__init__.py b/engine/tests/ledger/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/engine/tests/ledger/test_arithmetic_scanner.py b/engine/tests/ledger/test_arithmetic_scanner.py new file mode 100644 index 0000000000..00fe4c68ff --- /dev/null +++ b/engine/tests/ledger/test_arithmetic_scanner.py @@ -0,0 +1,133 @@ +""" +ArithmeticScanner — unit tests. + +Tests cover the two inline arithmetic patterns the scanner targets: + 1. Equals expressions: A + B = C + 2. Total-then-addends: Total: C (A + B) +""" + +from decimal import Decimal + +import pytest + +from stirling.agents.ledger.validators.arithmetic import ArithmeticScanner + + +@pytest.fixture +def scanner() -> ArithmeticScanner: + return ArithmeticScanner(tolerance=Decimal("0.01")) + + +# --------------------------------------------------------------------------- +# Equals expressions: A + B + C = D +# --------------------------------------------------------------------------- + + +def test_correct_equals_expression(scanner: ArithmeticScanner) -> None: + """A correct sum should produce no findings.""" + text = "The total cost is 100 + 200 + 150 = 450." + assert scanner.scan(page=0, text=text) == [] + + +def test_wrong_equals_expression(scanner: ArithmeticScanner) -> None: + """An incorrect sum should produce one error discrepancy.""" + text = "Revenue: 500 + 300 = 900" # should be 800 + discrepancies = scanner.scan(page=3, text=text) + assert len(discrepancies) == 1 + d = discrepancies[0] + assert d.page == 3 + assert d.kind == "arithmetic" + assert d.severity == "error" + assert d.stated == "900" + assert d.expected == "800" + + +def test_subtraction_expression(scanner: ArithmeticScanner) -> None: + """Subtraction in expressions should be evaluated correctly.""" + text = "Net: 1000 - 250 = 750" + assert scanner.scan(page=0, text=text) == [] + + +def test_wrong_subtraction(scanner: ArithmeticScanner) -> None: + text = "Net: 1000 - 250 = 800" # should be 750 + discrepancies = scanner.scan(page=0, text=text) + assert len(discrepancies) == 1 + assert discrepancies[0].expected == "750" + + +def test_currency_symbols_stripped(scanner: ArithmeticScanner) -> None: + """Currency symbols and thousand separators must not break parsing.""" + text = "Total: £1,000 + £500 = £1,500" + assert scanner.scan(page=0, text=text) == [] + + +def test_multiple_expressions_in_text(scanner: ArithmeticScanner) -> None: + """Multiple expressions in the same text should each be evaluated.""" + text = ( + "Q1 revenue: 100 + 200 = 300. " + "Q2 revenue: 150 + 100 = 350. " # wrong: should be 250 + ) + discrepancies = scanner.scan(page=0, text=text) + assert len(discrepancies) == 1 + assert discrepancies[0].expected == "250" + + +# --------------------------------------------------------------------------- +# Total-then-addends: "Total: X (A + B + C)" +# --------------------------------------------------------------------------- + + +def test_correct_total_then_addends(scanner: ArithmeticScanner) -> None: + text = "Grand Total: 750 (300 + 250 + 200)" + assert scanner.scan(page=0, text=text) == [] + + +def test_wrong_total_then_addends(scanner: ArithmeticScanner) -> None: + text = "Grand Total: 900 (300 + 250 + 200)" # addends sum to 750 + discrepancies = scanner.scan(page=0, text=text) + assert len(discrepancies) == 1 + d = discrepancies[0] + assert d.stated == "900" + assert d.expected == "750" + + +def test_total_keyword_variations(scanner: ArithmeticScanner) -> None: + """The pattern must work for 'Sum', 'Subtotal', 'Grand Total' etc.""" + cases = [ + ("Sum: 600 (200 + 200 + 200)", True), + ("Subtotal: 600 (200 + 200 + 200)", True), + ("Total: 999 (200 + 200 + 200)", False), # wrong + ] + for text, should_be_clean in cases: + result = scanner.scan(page=0, text=text) + if should_be_clean: + assert result == [], f"Expected clean for: {text!r}" + else: + assert len(result) == 1, f"Expected error for: {text!r}" + + +# --------------------------------------------------------------------------- +# Edge cases +# --------------------------------------------------------------------------- + + +def test_no_expressions_in_text(scanner: ArithmeticScanner) -> None: + text = "This paragraph discusses revenue trends but contains no arithmetic." + assert scanner.scan(page=0, text=text) == [] + + +def test_empty_text(scanner: ArithmeticScanner) -> None: + assert scanner.scan(page=0, text="") == [] + + +def test_leading_negative_expression(scanner: ArithmeticScanner) -> None: + """Expressions starting with a negative number should evaluate correctly.""" + text = "Adjustment: -100 + 250 = 150" + assert scanner.scan(page=0, text=text) == [] + + +def test_leading_negative_wrong(scanner: ArithmeticScanner) -> None: + text = "Adjustment: -100 + 250 = 200" # should be 150 + discrepancies = scanner.scan(page=0, text=text) + assert len(discrepancies) == 1 + assert discrepancies[0].expected == "150" diff --git a/engine/tests/ledger/test_figure_tracker.py b/engine/tests/ledger/test_figure_tracker.py new file mode 100644 index 0000000000..fec3050628 --- /dev/null +++ b/engine/tests/ledger/test_figure_tracker.py @@ -0,0 +1,100 @@ +""" +FigureTracker — unit tests. + +Tests that named figures are correctly accumulated and that conflicting +sightings (same label, different value) are surfaced as consistency warnings. +""" + +from decimal import Decimal + +import pytest + +from stirling.agents.ledger.validators.figures import FigureTracker + + +@pytest.fixture +def tracker() -> FigureTracker: + return FigureTracker(tolerance=Decimal("0.01")) + + +# --------------------------------------------------------------------------- +# No conflicts +# --------------------------------------------------------------------------- + + +def test_no_conflicts_single_figure(tracker: FigureTracker) -> None: + tracker.record("Net Profit", Decimal("1200.00"), page=3, raw="£1,200.00") + assert tracker.conflicts() == [] + + +def test_no_conflicts_consistent_figure(tracker: FigureTracker) -> None: + """The same figure cited identically on two pages must not raise a conflict.""" + tracker.record("Total Revenue", Decimal("5000.00"), page=1, raw="£5,000") + tracker.record("Total Revenue", Decimal("5000.00"), page=8, raw="£5,000") + assert tracker.conflicts() == [] + + +def test_no_conflicts_within_tolerance(tracker: FigureTracker) -> None: + """A difference within tolerance must not be flagged.""" + tracker.record("VAT", Decimal("100.00"), page=2, raw="£100.00") + tracker.record("VAT", Decimal("100.005"), page=5, raw="£100.005") + assert tracker.conflicts() == [] + + +# --------------------------------------------------------------------------- +# Conflicts +# --------------------------------------------------------------------------- + + +def test_conflict_different_values(tracker: FigureTracker) -> None: + """Same label, different value on two pages → one consistency warning.""" + tracker.record("Net Profit", Decimal("1200.00"), page=3, raw="£1,200") + tracker.record("Net Profit", Decimal("1250.00"), page=7, raw="£1,250") + conflicts = tracker.conflicts() + assert len(conflicts) == 1 + d = conflicts[0] + assert d.kind == "consistency" + assert d.severity == "warning" + assert d.page == 7 # later occurrence is flagged + + +def test_conflict_three_sightings_two_values(tracker: FigureTracker) -> None: + """Three sightings where one differs from canonical → 1 conflict.""" + tracker.record("Revenue", Decimal("1000"), page=1, raw="£1,000") + tracker.record("Revenue", Decimal("1000"), page=3, raw="£1,000") + tracker.record("Revenue", Decimal("999"), page=5, raw="£999") + conflicts = tracker.conflicts() + # Canonical=p1 (1000). p3 matches, p5 differs → 1 conflict + assert len(conflicts) == 1 + assert conflicts[0].page == 5 + + +# --------------------------------------------------------------------------- +# Label normalisation +# --------------------------------------------------------------------------- + + +def test_label_normalisation_case_insensitive(tracker: FigureTracker) -> None: + """Labels must be compared case-insensitively.""" + tracker.record("Net Profit", Decimal("1200"), page=2, raw="1200") + tracker.record("net profit", Decimal("1100"), page=4, raw="1100") + assert len(tracker.conflicts()) == 1 + + +def test_label_normalisation_punctuation(tracker: FigureTracker) -> None: + """Colons and dashes in labels must be normalised before comparison.""" + tracker.record("Total Revenue:", Decimal("5000"), page=1, raw="5000") + tracker.record("Total Revenue —", Decimal("4000"), page=9, raw="4000") + assert len(tracker.conflicts()) == 1 + + +# --------------------------------------------------------------------------- +# Entry count +# --------------------------------------------------------------------------- + + +def test_entry_count(tracker: FigureTracker) -> None: + tracker.record("A", Decimal("1"), page=0, raw="1") + tracker.record("A", Decimal("1"), page=1, raw="1") + tracker.record("B", Decimal("2"), page=2, raw="2") + assert tracker.entry_count == 3 diff --git a/engine/tests/ledger/test_formula_evaluator.py b/engine/tests/ledger/test_formula_evaluator.py new file mode 100644 index 0000000000..1cf5a254d2 --- /dev/null +++ b/engine/tests/ledger/test_formula_evaluator.py @@ -0,0 +1,205 @@ +""" +FormulaEvaluator — unit tests. + +Tests cover: + - Operator precedence (* / before + -) + - Column reference replacement (colN with word boundaries) + - Negative number handling + - each_row, column_total, and single_cell scopes +""" + +from decimal import Decimal + +import pytest + +from stirling.agents.ledger.validators.formula import FormulaEvaluator + + +@pytest.fixture +def evaluator() -> FormulaEvaluator: + return FormulaEvaluator(tolerance=Decimal("0.01")) + + +# --------------------------------------------------------------------------- +# _safe_eval — operator precedence +# --------------------------------------------------------------------------- + + +def test_safe_eval_addition(evaluator: FormulaEvaluator) -> None: + assert evaluator._safe_eval("2 + 3") == Decimal("5") + + +def test_safe_eval_multiplication_before_addition(evaluator: FormulaEvaluator) -> None: + """2 + 3 * 4 should be 14, not 20.""" + assert evaluator._safe_eval("2 + 3 * 4") == Decimal("14") + + +def test_safe_eval_division_before_subtraction(evaluator: FormulaEvaluator) -> None: + """10 - 6 / 2 should be 7, not 2.""" + assert evaluator._safe_eval("10 - 6 / 2") == Decimal("7") + + +def test_safe_eval_mixed_precedence(evaluator: FormulaEvaluator) -> None: + """1 + 2 * 3 - 4 / 2 should be 1 + 6 - 2 = 5.""" + assert evaluator._safe_eval("1 + 2 * 3 - 4 / 2") == Decimal("5") + + +def test_safe_eval_all_multiplication(evaluator: FormulaEvaluator) -> None: + assert evaluator._safe_eval("2 * 3 * 4") == Decimal("24") + + +def test_safe_eval_division_by_zero(evaluator: FormulaEvaluator) -> None: + assert evaluator._safe_eval("10 / 0") is None + + +def test_safe_eval_negative_result(evaluator: FormulaEvaluator) -> None: + assert evaluator._safe_eval("3 - 5") == Decimal("-2") + + +def test_safe_eval_leading_negative(evaluator: FormulaEvaluator) -> None: + """Expressions starting with a negative number should work.""" + result = evaluator._safe_eval("-100 + 200") + assert result == Decimal("100") + + +def test_safe_eval_empty(evaluator: FormulaEvaluator) -> None: + assert evaluator._safe_eval("") is None + + +def test_safe_eval_single_number(evaluator: FormulaEvaluator) -> None: + assert evaluator._safe_eval("42") == Decimal("42") + + +def test_safe_eval_decimal_numbers(evaluator: FormulaEvaluator) -> None: + result = evaluator._safe_eval("1.5 * 2 + 0.5") + assert result == Decimal("3.5") + + +# --------------------------------------------------------------------------- +# colN replacement — word boundary safety +# --------------------------------------------------------------------------- + + +def test_col1_does_not_corrupt_col12(evaluator: FormulaEvaluator) -> None: + """col1 replacement must not alter col12.""" + csv = "a,b,c,d,e,f,g,h,i,j,k,l,m\n0,10,0,0,0,0,0,0,0,0,0,0,120\n" + # col1=10, col12=120 → col12 - col1 should be 110 + result = evaluator.evaluate( + page=0, + table_csv=csv, + formula="col0 = col12 - col1", + scope="each_row", + description="test", + ) + # row 1: col0=0, expected=120-10=110 → discrepancy + assert len(result) == 1 + assert result[0].expected == "110" + + +def test_col_replacement_adjacent_columns(evaluator: FormulaEvaluator) -> None: + """col1 and col10 should both be replaced correctly.""" + csv = "a,b,c,d,e,f,g,h,i,j,k\n55,5,0,0,0,0,0,0,0,0,50\n" + # col0=55, col1=5, col10=50 → col1 + col10 = 55 + result = evaluator.evaluate( + page=0, + table_csv=csv, + formula="col0 = col1 + col10", + scope="each_row", + description="test", + ) + assert result == [] # 5 + 50 = 55, matches col0 + + +# --------------------------------------------------------------------------- +# each_row scope +# --------------------------------------------------------------------------- + + +def test_each_row_correct(evaluator: FormulaEvaluator) -> None: + csv = "Item,Qty,Price,Total\nWidget,10,5,50\nGadget,3,20,60\n" + result = evaluator.evaluate( + page=0, + table_csv=csv, + formula="col3 = col1 * col2", + scope="each_row", + description="unit price check", + ) + assert result == [] + + +def test_each_row_error(evaluator: FormulaEvaluator) -> None: + csv = "Item,Qty,Price,Total\nWidget,10,5,50\nGadget,3,20,99\n" + result = evaluator.evaluate( + page=0, + table_csv=csv, + formula="col3 = col1 * col2", + scope="each_row", + description="unit price check", + ) + assert len(result) == 1 + assert result[0].expected == "60" + assert result[0].stated == "99" + + +# --------------------------------------------------------------------------- +# column_total scope +# --------------------------------------------------------------------------- + + +def test_column_total_correct(evaluator: FormulaEvaluator) -> None: + csv = "Name,Amount\nA,100\nB,200\nTotal,300\n" + result = evaluator.evaluate( + page=0, + table_csv=csv, + formula="sum", + scope="column_total", + description="total check", + target_row=3, + target_col=1, + ) + assert result == [] + + +def test_column_total_error(evaluator: FormulaEvaluator) -> None: + csv = "Name,Amount\nA,100\nB,200\nTotal,400\n" + result = evaluator.evaluate( + page=0, + table_csv=csv, + formula="sum", + scope="column_total", + description="total check", + target_row=3, + target_col=1, + ) + assert len(result) == 1 + assert result[0].expected == "300" + + +# --------------------------------------------------------------------------- +# single_cell scope +# --------------------------------------------------------------------------- + + +def test_single_cell_correct(evaluator: FormulaEvaluator) -> None: + csv = "A,B,C\n10,20,30\n5,15,20\n15,35,50\n" + result = evaluator.evaluate( + page=0, + table_csv=csv, + formula="cell(3,2) = cell(1,2) + cell(2,2)", + scope="single_cell", + description="grand total", + ) + assert result == [] + + +def test_single_cell_error(evaluator: FormulaEvaluator) -> None: + csv = "A,B,C\n10,20,30\n5,15,20\n15,35,99\n" + result = evaluator.evaluate( + page=0, + table_csv=csv, + formula="cell(3,2) = cell(1,2) + cell(2,2)", + scope="single_cell", + description="grand total", + ) + assert len(result) == 1 + assert result[0].expected == "50" diff --git a/engine/tests/ledger/test_models.py b/engine/tests/ledger/test_models.py new file mode 100644 index 0000000000..98b0ef9c85 --- /dev/null +++ b/engine/tests/ledger/test_models.py @@ -0,0 +1,144 @@ +""" +Ledger models — unit tests for serialisation and business logic. + +These tests confirm the wire contract: models round-trip through JSON +correctly and their helper properties behave as documented. +""" + +import pytest +from pydantic import ValidationError + +from stirling.contracts.ledger import ( + Discrepancy, + DiscrepancyKind, + Evidence, + Folio, + FolioManifest, + FolioType, + Requisition, + Severity, + Verdict, +) + +# --------------------------------------------------------------------------- +# FolioManifest +# --------------------------------------------------------------------------- + + +def test_folio_manifest_round_trip() -> None: + manifest = FolioManifest( + session_id="abc-123", + page_count=3, + folio_types=[FolioType.TEXT, FolioType.IMAGE, FolioType.MIXED], + ) + reloaded = FolioManifest.model_validate_json(manifest.model_dump_json()) + assert reloaded == manifest + + +def test_folio_manifest_round_bounds() -> None: + with pytest.raises(ValidationError): + FolioManifest(session_id="x", page_count=1, folio_types=[FolioType.TEXT], round=0) + with pytest.raises(ValidationError): + FolioManifest(session_id="x", page_count=1, folio_types=[FolioType.TEXT], round=4) + + +# --------------------------------------------------------------------------- +# Requisition +# --------------------------------------------------------------------------- + + +def test_requisition_empty() -> None: + req = Requisition(rationale="nothing needed") + assert req.need_text == [] + assert req.need_tables == [] + assert req.need_ocr == [] + + +def test_requisition_type_discriminator() -> None: + req = Requisition(need_text=[0, 1], rationale="needs text") + assert req.type == "requisition" + + +# --------------------------------------------------------------------------- +# Folio.readable_text +# --------------------------------------------------------------------------- + + +def test_folio_readable_text_prefers_ocr() -> None: + folio = Folio(page=0, text="digital text", ocr_text="ocr text") + assert folio.readable_text == "ocr text" + + +def test_folio_readable_text_falls_back_to_text() -> None: + folio = Folio(page=0, text="digital text") + assert folio.readable_text == "digital text" + + +def test_folio_readable_text_empty_when_none() -> None: + folio = Folio(page=0) + assert folio.readable_text == "" + + +# --------------------------------------------------------------------------- +# Verdict +# --------------------------------------------------------------------------- + + +def test_verdict_clean_flag() -> None: + verdict = Verdict( + session_id="s1", + discrepancies=[], + pages_examined=[0, 1], + rounds_taken=2, + summary="All figures balance.", + clean=True, + ) + assert verdict.error_count == 0 + assert verdict.warning_count == 0 + assert verdict.clean is True + + +def test_verdict_error_and_warning_counts() -> None: + discrepancies = [ + Discrepancy( + page=0, + kind=DiscrepancyKind.TALLY, + severity=Severity.ERROR, + description="bad sum", + stated="100", + expected="110", + ), + Discrepancy( + page=1, + kind=DiscrepancyKind.CONSISTENCY, + severity=Severity.WARNING, + description="mismatched figure", + stated="500", + expected="550", + ), + ] + verdict = Verdict( + session_id="s1", + discrepancies=discrepancies, + pages_examined=[0, 1], + rounds_taken=1, + summary="Issues found.", + clean=False, + ) + assert verdict.error_count == 1 + assert verdict.warning_count == 1 + + +# --------------------------------------------------------------------------- +# Evidence.final_round +# --------------------------------------------------------------------------- + + +def test_evidence_final_round() -> None: + evidence = Evidence( + session_id="s", + folios=[Folio(page=0, text="hello")], + round=3, + final_round=True, + ) + assert evidence.final_round is True diff --git a/engine/tests/ledger/test_routes.py b/engine/tests/ledger/test_routes.py new file mode 100644 index 0000000000..ca322a6364 --- /dev/null +++ b/engine/tests/ledger/test_routes.py @@ -0,0 +1,243 @@ +""" +Ledger Auditor — FastAPI route tests. + +Uses FastAPI's TestClient with dependency overrides. All LLM calls are +mocked out; these tests exercise HTTP parsing, serialisation, and response +enveloping only — not the agent's reasoning. +""" + +from __future__ import annotations + +from collections.abc import Iterator +from decimal import Decimal + +import pytest +from fastapi.testclient import TestClient + +from stirling.api import app +from stirling.api.dependencies import get_math_auditor_agent +from stirling.config import AppSettings, load_settings +from stirling.contracts.ledger import ( + Discrepancy, + DiscrepancyKind, + Evidence, + FolioManifest, + Requisition, + Severity, + Verdict, +) + +# --------------------------------------------------------------------------- +# Stubs +# --------------------------------------------------------------------------- + + +class StubSettingsProvider: + def __call__(self) -> AppSettings: + from conftest import build_app_settings + + return build_app_settings() + + +class StubLedgerAgent: + """Stub that returns canned responses without touching any model.""" + + def __init__( + self, + requisition: Requisition | None = None, + verdict: Verdict | None = None, + ) -> None: + self._requisition = requisition or _stub_requisition() + self._verdict = verdict or _stub_verdict() + self.examine_calls: list[FolioManifest] = [] + self.audit_calls: list[tuple[Evidence, Decimal]] = [] + + async def examine(self, manifest: FolioManifest) -> Requisition: + self.examine_calls.append(manifest) + return self._requisition + + async def audit(self, evidence: Evidence, tolerance: Decimal = Decimal("0.01")) -> Verdict: + self.audit_calls.append((evidence, tolerance)) + return self._verdict + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _stub_requisition() -> Requisition: + return Requisition( + need_text=[0, 2], + need_tables=[0], + need_ocr=[1], + rationale="Page 1 is image-only; pages 0 and 2 have financial text.", + ) + + +def _stub_verdict( + clean: bool = True, + discrepancies: list[Discrepancy] | None = None, +) -> Verdict: + return Verdict( + session_id="test-session", + discrepancies=discrepancies or [], + pages_examined=[0, 2], + rounds_taken=2, + summary="No errors found." if clean else "1 tally error found.", + clean=clean, + ) + + +def _manifest_body(**overrides: object) -> dict[str, object]: + base: dict[str, object] = { + "sessionId": "test-session", + "pageCount": 3, + "folioTypes": ["text", "image", "mixed"], + "round": 1, + } + return {**base, **overrides} + + +def _evidence_body(**overrides: object) -> dict[str, object]: + base: dict[str, object] = { + "sessionId": "test-session", + "folios": [ + {"page": 0, "text": "Fee: £100\nTax: £20\nTotal: £120"}, + {"page": 2, "text": "Summary: all tallies correct"}, + ], + "round": 2, + "finalRound": False, + } + return {**base, **overrides} + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture +def stub_agent() -> StubLedgerAgent: + return StubLedgerAgent() + + +@pytest.fixture +def client(stub_agent: StubLedgerAgent) -> Iterator[TestClient]: + app.dependency_overrides[load_settings] = StubSettingsProvider() + app.dependency_overrides[get_math_auditor_agent] = lambda: stub_agent + yield TestClient(app, raise_server_exceptions=False) + app.dependency_overrides.pop(load_settings, None) + app.dependency_overrides.pop(get_math_auditor_agent, None) + + +# --------------------------------------------------------------------------- +# POST /api/v1/ai/math-auditor-agent/examine +# --------------------------------------------------------------------------- + + +class TestExamineEndpoint: + """Tests for POST /api/v1/ai/math-auditor-agent/examine.""" + + def test_returns_200(self, client: TestClient) -> None: + resp = client.post("/api/v1/ai/math-auditor-agent/examine", json=_manifest_body()) + assert resp.status_code == 200 + + def test_response_is_requisition(self, client: TestClient) -> None: + resp = client.post("/api/v1/ai/math-auditor-agent/examine", json=_manifest_body()) + body = resp.json() + assert body["type"] == "requisition" + assert body["needText"] == [0, 2] + assert body["needTables"] == [0] + assert body["needOcr"] == [1] + assert "rationale" in body + + def test_examine_called_with_parsed_manifest( + self, + client: TestClient, + stub_agent: StubLedgerAgent, + ) -> None: + client.post("/api/v1/ai/math-auditor-agent/examine", json=_manifest_body(sessionId="my-session", pageCount=3)) + assert len(stub_agent.examine_calls) == 1 + manifest = stub_agent.examine_calls[0] + assert manifest.session_id == "my-session" + assert manifest.page_count == 3 + + def test_content_type_is_json(self, client: TestClient) -> None: + resp = client.post("/api/v1/ai/math-auditor-agent/examine", json=_manifest_body()) + assert "application/json" in resp.headers["content-type"] + + +# --------------------------------------------------------------------------- +# POST /api/v1/ai/math-auditor-agent/deliberate +# --------------------------------------------------------------------------- + + +class TestDeliberateEndpoint: + """Tests for POST /api/v1/ai/math-auditor-agent/deliberate.""" + + def test_returns_200_clean(self, client: TestClient) -> None: + resp = client.post("/api/v1/ai/math-auditor-agent/deliberate", json=_evidence_body()) + assert resp.status_code == 200 + + def test_response_is_verdict(self, client: TestClient) -> None: + resp = client.post("/api/v1/ai/math-auditor-agent/deliberate", json=_evidence_body()) + body = resp.json() + assert body["type"] == "verdict" + assert body["clean"] is True + + def test_discrepancies_serialised(self, client: TestClient) -> None: + d = Discrepancy( + page=0, + kind=DiscrepancyKind.TALLY, + severity=Severity.ERROR, + description="Column total wrong", + stated="250", + expected="300", + ) + stub = StubLedgerAgent(verdict=_stub_verdict(clean=False, discrepancies=[d])) + app.dependency_overrides[get_math_auditor_agent] = lambda: stub + resp = client.post("/api/v1/ai/math-auditor-agent/deliberate", json=_evidence_body()) + body = resp.json() + discrepancies = body["discrepancies"] + assert len(discrepancies) == 1 + assert discrepancies[0]["kind"] == "tally" + assert discrepancies[0]["severity"] == "error" + assert discrepancies[0]["stated"] == "250" + assert discrepancies[0]["expected"] == "300" + + def test_tolerance_query_param_forwarded( + self, + client: TestClient, + stub_agent: StubLedgerAgent, + ) -> None: + client.post("/api/v1/ai/math-auditor-agent/deliberate?tolerance=0.05", json=_evidence_body()) + assert len(stub_agent.audit_calls) == 1 + _, tolerance = stub_agent.audit_calls[0] + assert tolerance == Decimal("0.05") + + def test_default_tolerance_when_omitted( + self, + client: TestClient, + stub_agent: StubLedgerAgent, + ) -> None: + client.post("/api/v1/ai/math-auditor-agent/deliberate", json=_evidence_body()) + _, tolerance = stub_agent.audit_calls[0] + assert tolerance == Decimal("0.01") + + def test_invalid_tolerance_returns_400( + self, + client: TestClient, + stub_agent: StubLedgerAgent, + ) -> None: + resp = client.post("/api/v1/ai/math-auditor-agent/deliberate?tolerance=notanumber", json=_evidence_body()) + assert resp.status_code == 400 + + def test_final_round_flag_parsed( + self, + client: TestClient, + stub_agent: StubLedgerAgent, + ) -> None: + client.post("/api/v1/ai/math-auditor-agent/deliberate", json=_evidence_body(finalRound=True)) + evidence, _ = stub_agent.audit_calls[0] + assert evidence.final_round is True diff --git a/engine/tests/test_pdf_edit_agent.py b/engine/tests/test_pdf_edit_agent.py index e78b1b9255..fdbeb722ee 100644 --- a/engine/tests/test_pdf_edit_agent.py +++ b/engine/tests/test_pdf_edit_agent.py @@ -5,7 +5,6 @@ from dataclasses import dataclass import pytest from stirling.agents import PdfEditAgent, PdfEditParameterSelector, PdfEditPlanSelection -from stirling.config import AppSettings from stirling.contracts import ( EditCannotDoResponse, EditClarificationRequest, @@ -13,23 +12,14 @@ from stirling.contracts import ( PdfEditRequest, ToolOperationStep, ) -from stirling.models.tool_models import CompressParams, OperationId, RotateParams -from stirling.services import build_runtime - - -def build_test_settings() -> AppSettings: - return AppSettings( - smart_model_name="test", - fast_model_name="test", - smart_model_max_tokens=8192, - fast_model_max_tokens=2048, - ) +from stirling.models.tool_models import Angle, FlattenParams, RotatePdfParams, ToolEndpoint +from stirling.services.runtime import AppRuntime @dataclass(frozen=True) class ParameterSelectorCall: request: PdfEditRequest - operation_plan: list[OperationId] + operation_plan: list[ToolEndpoint] operation_index: int generated_steps: list[ToolOperationStep] @@ -41,10 +31,10 @@ class RecordingParameterSelector: async def select( self, request: PdfEditRequest, - operation_plan: list[OperationId], + operation_plan: list[ToolEndpoint], operation_index: int, generated_steps: list[ToolOperationStep], - ) -> RotateParams | CompressParams: + ) -> RotatePdfParams | FlattenParams: self.calls.append( ParameterSelectorCall( request=request, @@ -54,17 +44,18 @@ class RecordingParameterSelector: ) ) if operation_index == 0: - return RotateParams(angle=90) - return CompressParams(compression_level=5) + return RotatePdfParams(angle=Angle(90)) + return FlattenParams(flatten_only_forms=False, render_dpi=None) class StubPdfEditAgent(PdfEditAgent): def __init__( self, + runtime: AppRuntime, selection: PdfEditPlanSelection | EditClarificationRequest | EditCannotDoResponse, parameter_selector: RecordingParameterSelector | PdfEditParameterSelector | None = None, ) -> None: - super().__init__(build_runtime(build_test_settings())) + super().__init__(runtime) self.selection = selection if parameter_selector is not None: self.parameter_selector = parameter_selector @@ -77,11 +68,12 @@ class StubPdfEditAgent(PdfEditAgent): @pytest.mark.anyio -async def test_pdf_edit_agent_builds_multi_step_plan() -> None: +async def test_pdf_edit_agent_builds_multi_step_plan(runtime: AppRuntime) -> None: parameter_selector = RecordingParameterSelector() agent = StubPdfEditAgent( + runtime, PdfEditPlanSelection( - operations=[OperationId.ROTATE, OperationId.COMPRESS], + operations=[ToolEndpoint.ROTATE_PDF, ToolEndpoint.FLATTEN], summary="Rotate the PDF, then compress it.", rationale="The pages need reorientation before reducing file size.", ), @@ -98,17 +90,18 @@ async def test_pdf_edit_agent_builds_multi_step_plan() -> None: assert isinstance(response, EditPlanResponse) assert response.summary == "Rotate the PDF, then compress it." assert response.rationale == "The pages need reorientation before reducing file size." - assert [step.tool for step in response.steps] == [OperationId.ROTATE, OperationId.COMPRESS] - assert isinstance(response.steps[0].parameters, RotateParams) - assert isinstance(response.steps[1].parameters, CompressParams) + assert [step.tool for step in response.steps] == [ToolEndpoint.ROTATE_PDF, ToolEndpoint.FLATTEN] + assert isinstance(response.steps[0].parameters, RotatePdfParams) + assert isinstance(response.steps[1].parameters, FlattenParams) @pytest.mark.anyio -async def test_pdf_edit_agent_passes_previous_steps_to_parameter_selector() -> None: +async def test_pdf_edit_agent_passes_previous_steps_to_parameter_selector(runtime: AppRuntime) -> None: parameter_selector = RecordingParameterSelector() agent = StubPdfEditAgent( + runtime, PdfEditPlanSelection( - operations=[OperationId.ROTATE, OperationId.COMPRESS], + operations=[ToolEndpoint.ROTATE_PDF, ToolEndpoint.FLATTEN], summary="Rotate the PDF, then compress it.", ), parameter_selector=parameter_selector, @@ -127,19 +120,20 @@ async def test_pdf_edit_agent_passes_previous_steps_to_parameter_selector() -> N assert parameter_selector.calls[1].operation_index == 1 assert parameter_selector.calls[1].generated_steps == [ ToolOperationStep( - tool=OperationId.ROTATE, - parameters=RotateParams(angle=90), + tool=ToolEndpoint.ROTATE_PDF, + parameters=RotatePdfParams(angle=Angle(90)), ) ] @pytest.mark.anyio -async def test_pdf_edit_agent_returns_clarification_without_partial_plan() -> None: +async def test_pdf_edit_agent_returns_clarification_without_partial_plan(runtime: AppRuntime) -> None: agent = StubPdfEditAgent( + runtime, EditClarificationRequest( question="Which pages should be rotated?", reason="The request does not say which pages to change.", - ) + ), ) response = await agent.handle(PdfEditRequest(user_message="Rotate some pages.")) @@ -148,11 +142,12 @@ async def test_pdf_edit_agent_returns_clarification_without_partial_plan() -> No @pytest.mark.anyio -async def test_pdf_edit_agent_returns_cannot_do_without_partial_plan() -> None: +async def test_pdf_edit_agent_returns_cannot_do_without_partial_plan(runtime: AppRuntime) -> None: agent = StubPdfEditAgent( + runtime, EditCannotDoResponse( reason="This request requires OCR, which is not part of PDF edit planning.", - ) + ), ) response = await agent.handle(PdfEditRequest(user_message="Read this scan and summarize it.")) diff --git a/engine/tests/test_pdf_question_agent.py b/engine/tests/test_pdf_question_agent.py index 52df9474ce..b284870b76 100644 --- a/engine/tests/test_pdf_question_agent.py +++ b/engine/tests/test_pdf_question_agent.py @@ -3,19 +3,20 @@ from __future__ import annotations import pytest from stirling.agents import PdfQuestionAgent -from stirling.config import AppSettings from stirling.contracts import ( + ExtractedFileText, PdfQuestionAnswerResponse, - PdfQuestionNeedTextResponse, + PdfQuestionNeedContentResponse, PdfQuestionNotFoundResponse, PdfQuestionRequest, + PdfTextSelection, ) -from stirling.services import build_runtime +from stirling.services.runtime import AppRuntime class StubPdfQuestionAgent(PdfQuestionAgent): - def __init__(self, response: PdfQuestionAnswerResponse | PdfQuestionNotFoundResponse) -> None: - super().__init__(build_runtime(build_test_settings())) + def __init__(self, runtime: AppRuntime, response: PdfQuestionAnswerResponse | PdfQuestionNotFoundResponse) -> None: + super().__init__(runtime) self.response = response async def _run_answer_agent( @@ -25,38 +26,39 @@ class StubPdfQuestionAgent(PdfQuestionAgent): return self.response -def build_test_settings() -> AppSettings: - return AppSettings( - smart_model_name="test", - fast_model_name="test", - smart_model_max_tokens=8192, - fast_model_max_tokens=2048, +def invoice_page() -> ExtractedFileText: + return ExtractedFileText( + file_name="invoice.pdf", + pages=[PdfTextSelection(page_number=1, text="Invoice total: 120.00")], ) @pytest.mark.anyio -async def test_pdf_question_agent_requires_extracted_text() -> None: - agent = PdfQuestionAgent(build_runtime(build_test_settings())) +async def test_pdf_question_agent_requires_extracted_text(runtime: AppRuntime) -> None: + agent = PdfQuestionAgent(runtime) - response = await agent.handle(PdfQuestionRequest(question="What is the total?", extracted_text="")) + response = await agent.handle( + PdfQuestionRequest(question="What is the total?", page_text=[], file_names=["test.pdf"]) + ) - assert isinstance(response, PdfQuestionNeedTextResponse) + assert isinstance(response, PdfQuestionNeedContentResponse) @pytest.mark.anyio -async def test_pdf_question_agent_returns_grounded_answer() -> None: +async def test_pdf_question_agent_returns_grounded_answer(runtime: AppRuntime) -> None: agent = StubPdfQuestionAgent( + runtime, PdfQuestionAnswerResponse( answer="The invoice total is 120.00.", - evidence=["Invoice total: 120.00"], - ) + evidence=[invoice_page()], + ), ) response = await agent.handle( PdfQuestionRequest( question="What is the total?", - extracted_text="Invoice total: 120.00", - file_name="invoice.pdf", + page_text=[invoice_page()], + file_names=["invoice.pdf"], ) ) @@ -65,14 +67,19 @@ async def test_pdf_question_agent_returns_grounded_answer() -> None: @pytest.mark.anyio -async def test_pdf_question_agent_returns_not_found_when_text_is_insufficient() -> None: - agent = StubPdfQuestionAgent(PdfQuestionNotFoundResponse(reason="The answer is not present in the text.")) +async def test_pdf_question_agent_returns_not_found_when_text_is_insufficient(runtime: AppRuntime) -> None: + agent = StubPdfQuestionAgent(runtime, PdfQuestionNotFoundResponse(reason="The answer is not present in the text.")) response = await agent.handle( PdfQuestionRequest( question="What is the total?", - extracted_text="This page contains only a shipping address.", - file_name="invoice.pdf", + page_text=[ + ExtractedFileText( + file_name="invoice.pdf", + pages=[PdfTextSelection(page_number=1, text="This page contains only a shipping address.")], + ) + ], + file_names=["invoice.pdf"], ) ) diff --git a/engine/tests/test_rag.py b/engine/tests/test_rag.py new file mode 100644 index 0000000000..da1d524653 --- /dev/null +++ b/engine/tests/test_rag.py @@ -0,0 +1,283 @@ +from __future__ import annotations + +import pytest + +from stirling.rag.capability import RagCapability +from stirling.rag.chunker import chunk_text +from stirling.rag.service import RagService +from stirling.rag.sqlite_vec_store import SqliteVecStore +from stirling.rag.store import Document, SearchResult + +# ── chunk_text ────────────────────────────────────────────────────────── + + +class TestChunkText: + def test_empty_input_returns_empty(self) -> None: + assert chunk_text("") == [] + assert chunk_text(" ") == [] + + def test_short_text_returns_single_chunk(self) -> None: + text = "Hello world." + chunks = chunk_text(text, chunk_size=100) + assert len(chunks) == 1 + assert chunks[0] == "Hello world." + + def test_splits_on_paragraph_boundaries(self) -> None: + text = "First paragraph.\n\nSecond paragraph.\n\nThird paragraph." + chunks = chunk_text(text, chunk_size=30, overlap=0) + # Each paragraph fits in 30 chars, so they should be split + assert len(chunks) >= 2 + assert "First paragraph." in chunks[0] + + def test_long_text_produces_multiple_chunks(self) -> None: + text = " ".join(["word"] * 200) + chunks = chunk_text(text, chunk_size=100, overlap=10) + assert len(chunks) > 1 + for chunk in chunks: + # Chunks may slightly exceed due to sentence boundary snapping + assert len(chunk) <= 200 # generous upper bound + + def test_overlap_produces_shared_content(self) -> None: + sentences = [f"Sentence number {i}." for i in range(20)] + text = " ".join(sentences) + chunks = chunk_text(text, chunk_size=100, overlap=30) + if len(chunks) >= 2: + # After word-boundary snapping, the second chunk should share + # some content with the tail of the first chunk + words_in_first_tail = chunks[0].split()[-3:] # last 3 words + overlap_text = " ".join(words_in_first_tail) + assert overlap_text in chunks[1], f"Expected overlap '{overlap_text}' in chunk[1]: '{chunks[1][:80]}...'" + + +# ── SqliteVecStore ────────────────────────────────────────────────────── + + +class TestSqliteVecStore: + """Each test gets its own ephemeral store to avoid cross-test dimension conflicts.""" + + @pytest.mark.anyio + async def test_add_and_search(self) -> None: + store = SqliteVecStore.ephemeral() + docs = [ + Document(id="1", text="Python is a programming language", metadata={"source": "test"}), + Document(id="2", text="Java is another programming language", metadata={"source": "test"}), + Document(id="3", text="The weather today is sunny", metadata={"source": "test"}), + ] + # Simple 3-dimensional embeddings for testing + embeddings = [ + [1.0, 0.0, 0.0], + [0.9, 0.1, 0.0], + [0.0, 0.0, 1.0], + ] + await store.add_documents("test-col", docs, embeddings) + + # Search with a query close to the programming-related docs + results = await store.search("test-col", [1.0, 0.05, 0.0], top_k=2) + assert len(results) == 2 + assert isinstance(results[0], SearchResult) + # The closest should be doc "1" (exact match on first dimension) + assert results[0].document.id == "1" + assert results[0].score > 0.5 + + @pytest.mark.anyio + async def test_list_and_has_collection(self) -> None: + store = SqliteVecStore.ephemeral() + docs = [Document(id="1", text="test", metadata={})] + await store.add_documents("my-collection", docs, [[1.0, 0.0]]) + + collections = await store.list_collections() + assert "my-collection" in collections + assert await store.has_collection("my-collection") is True + assert await store.has_collection("nonexistent") is False + + @pytest.mark.anyio + async def test_delete_collection(self) -> None: + store = SqliteVecStore.ephemeral() + docs = [Document(id="1", text="test", metadata={})] + await store.add_documents("to-delete", docs, [[1.0]]) + + assert await store.has_collection("to-delete") is True + await store.delete_collection("to-delete") + assert await store.has_collection("to-delete") is False + + @pytest.mark.anyio + async def test_search_empty_collection(self) -> None: + store = SqliteVecStore.ephemeral() + docs = [Document(id="1", text="test", metadata={})] + await store.add_documents("empty-test", docs, [[1.0, 0.0]]) + results = await store.search("empty-test", [1.0, 0.0], top_k=5) + assert len(results) == 1 + + @pytest.mark.anyio + async def test_mismatched_docs_embeddings_raises(self) -> None: + store = SqliteVecStore.ephemeral() + docs = [Document(id="1", text="test", metadata={})] + with pytest.raises(ValueError, match="documents.*embeddings"): + await store.add_documents("bad", docs, [[1.0], [2.0]]) + + +# ── RagService (with stub embedder) ──────────────────────────────────── + + +class StubEmbeddingService: + """A minimal stub that returns fixed-dimension embeddings for testing.""" + + def __init__(self, dim: int = 8) -> None: + self._dim = dim + + async def embed_query(self, text: str) -> list[float]: + # Deterministic embedding based on hash of text + h = hash(text) % 1000 + return [(h + i) / 1000.0 for i in range(self._dim)] + + async def embed_documents(self, texts: list[str]) -> list[list[float]]: + return [await self.embed_query(t) for t in texts] + + def chunk_and_prepare( + self, + text: str, + source: str = "", + base_metadata: dict[str, str] | None = None, + ) -> list[Document]: + from stirling.rag.chunker import chunk_text + + chunks = chunk_text(text, 100, 10) + docs = [] + for i, chunk in enumerate(chunks): + meta = dict(base_metadata) if base_metadata else {} + meta["source"] = source + meta["chunk_index"] = str(i) + doc_id = f"{source}:chunk:{i}" if source else f"chunk:{i}" + docs.append(Document(id=doc_id, text=chunk, metadata=meta)) + return docs + + +@pytest.fixture +def rag_service() -> RagService: + """Each RagService test gets its own fresh ephemeral store to avoid dimension conflicts.""" + store = SqliteVecStore.ephemeral() + return RagService(embedder=StubEmbeddingService(), store=store, default_top_k=3) # type: ignore[arg-type] + + +class TestRagService: + @pytest.mark.anyio + async def test_index_and_search(self, rag_service: RagService) -> None: + text = "Python is great for data science. It has many libraries like pandas and numpy." + count = await rag_service.index_text("docs", text, source="guide.pdf") + assert count > 0 + + results = await rag_service.search("Python libraries", collection="docs") + assert len(results) > 0 + assert results[0].document.text # non-empty text + + @pytest.mark.anyio + async def test_index_empty_text_returns_zero(self, rag_service: RagService) -> None: + count = await rag_service.index_text("docs", "", source="empty.pdf") + assert count == 0 + + @pytest.mark.anyio + async def test_search_nonexistent_collection_returns_empty(self, rag_service: RagService) -> None: + results = await rag_service.search("anything", collection="nonexistent") + assert results == [] + + @pytest.mark.anyio + async def test_search_all_collections(self, rag_service: RagService) -> None: + await rag_service.index_text("col-a", "Machine learning overview.", source="ml.pdf") + await rag_service.index_text("col-b", "Deep learning with neural networks.", source="dl.pdf") + + results = await rag_service.search("neural networks") + assert len(results) > 0 + + @pytest.mark.anyio + async def test_delete_collection(self, rag_service: RagService) -> None: + await rag_service.index_text("temp", "Temporary data.", source="tmp.pdf") + collections = await rag_service.list_collections() + assert "temp" in collections + + await rag_service.delete_collection("temp") + collections = await rag_service.list_collections() + assert "temp" not in collections + + +# ── RagCapability ────────────────────────────────────────────────────── + + +async def _invoke_search_knowledge(capability: RagCapability, query: str, max_results: int = 5) -> str: + """Extract and call the search_knowledge tool function from a RagCapability's toolset.""" + from pydantic_ai import FunctionToolset + + toolset = capability.toolset + assert isinstance(toolset, FunctionToolset) + tool = toolset.tools["search_knowledge"] + return await tool.function(query=query, max_results=max_results) # type: ignore[call-arg] — pyright can't infer the generic tool function's kwargs + + +class TestRagCapability: + def test_instructions_static_when_collections_pinned(self, rag_service: RagService) -> None: + cap = RagCapability(rag_service, collections=["docs", "manuals"]) + instructions = cap.instructions + assert isinstance(instructions, str) + assert "docs, manuals" in instructions + assert "search_knowledge" in instructions + + def test_instructions_dynamic_when_no_collections(self, rag_service: RagService) -> None: + cap = RagCapability(rag_service) + instructions = cap.instructions + assert callable(instructions) + + @pytest.mark.anyio + async def test_dynamic_instructions_list_available_collections(self, rag_service: RagService) -> None: + await rag_service.index_text("col-a", "Alpha content.", source="a.pdf") + await rag_service.index_text("col-b", "Beta content.", source="b.pdf") + cap = RagCapability(rag_service) + instructions_fn = cap.instructions + assert callable(instructions_fn) + text = await instructions_fn() + assert "col-a" in text + assert "col-b" in text + + @pytest.mark.anyio + async def test_dynamic_instructions_when_store_empty(self, rag_service: RagService) -> None: + cap = RagCapability(rag_service) + instructions_fn = cap.instructions + assert callable(instructions_fn) + text = await instructions_fn() + assert "empty" in text.lower() + + @pytest.mark.anyio + async def test_search_knowledge_returns_no_results_message_when_empty(self, rag_service: RagService) -> None: + cap = RagCapability(rag_service) + output = await _invoke_search_knowledge(cap, "anything") + assert output == "No relevant results found in the knowledge base." + + @pytest.mark.anyio + async def test_search_knowledge_formats_results_with_source_and_score(self, rag_service: RagService) -> None: + await rag_service.index_text("docs", "Python is a programming language.", source="guide.pdf") + cap = RagCapability(rag_service) + output = await _invoke_search_knowledge(cap, "Python") + assert "[Result 1" in output + assert "source: guide.pdf" in output + assert "chunk:" in output + assert "relevance:" in output + + @pytest.mark.anyio + async def test_search_knowledge_restricts_to_pinned_collections(self, rag_service: RagService) -> None: + await rag_service.index_text("pinned", "Pinned collection content.", source="pinned.pdf") + await rag_service.index_text("other", "Content in another collection.", source="other.pdf") + + cap = RagCapability(rag_service, collections=["pinned"]) + output = await _invoke_search_knowledge(cap, "content") + assert "pinned.pdf" in output + assert "other.pdf" not in output + + @pytest.mark.anyio + async def test_search_knowledge_respects_max_results(self, rag_service: RagService) -> None: + paragraphs = "\n\n".join(f"Paragraph {i} about topic." for i in range(10)) + await rag_service.index_text("bulk", paragraphs, source="bulk.pdf") + + cap = RagCapability(rag_service) + output = await _invoke_search_knowledge(cap, "topic", max_results=2) + # Only two results requested, so only Result 1 and Result 2 should appear + assert "[Result 1" in output + assert "[Result 2" in output + assert "[Result 3" not in output diff --git a/engine/tests/test_rag_routes.py b/engine/tests/test_rag_routes.py new file mode 100644 index 0000000000..6227159279 --- /dev/null +++ b/engine/tests/test_rag_routes.py @@ -0,0 +1,205 @@ +from __future__ import annotations + +from collections.abc import Iterator + +import pytest +from fastapi.testclient import TestClient + +from stirling.api import app +from stirling.api.dependencies import get_rag_embedding_model, get_rag_service +from stirling.rag import Document, RagService, SqliteVecStore + +TEST_EMBEDDING_MODEL = "test-embedder" + + +class StubEmbedder: + """Deterministic embeddings for route tests — no network, no provider needed.""" + + def __init__(self, dim: int = 8) -> None: + self._dim = dim + + async def embed_query(self, text: str) -> list[float]: + h = hash(text) % 1000 + return [(h + i) / 1000.0 for i in range(self._dim)] + + async def embed_documents(self, texts: list[str]) -> list[list[float]]: + return [await self.embed_query(t) for t in texts] + + def chunk_and_prepare( + self, + text: str, + source: str = "", + base_metadata: dict[str, str] | None = None, + ) -> list[Document]: + from stirling.rag.chunker import chunk_text + + chunks = chunk_text(text, 100, 10) + docs = [] + for i, chunk in enumerate(chunks): + meta = dict(base_metadata) if base_metadata else {} + meta["source"] = source + meta["chunk_index"] = str(i) + doc_id = f"{source}:chunk:{i}" if source else f"chunk:{i}" + docs.append(Document(id=doc_id, text=chunk, metadata=meta)) + return docs + + +def _build_service() -> RagService: + return RagService( + embedder=StubEmbedder(), # type: ignore[arg-type] + store=SqliteVecStore.ephemeral(), + default_top_k=3, + ) + + +@pytest.fixture +def client() -> Iterator[TestClient]: + service = _build_service() + app.dependency_overrides[get_rag_service] = lambda: service + app.dependency_overrides[get_rag_embedding_model] = lambda: TEST_EMBEDDING_MODEL + try: + yield TestClient(app) + finally: + app.dependency_overrides.pop(get_rag_service, None) + app.dependency_overrides.pop(get_rag_embedding_model, None) + + +# ── /status ───────────────────────────────────────────────────────────── + + +def test_status_reports_embedding_model_and_collections(client: TestClient) -> None: + client.post( + "/api/v1/rag/index", + json={"collection": "my-docs", "text": "Hello world.", "source": "a.pdf"}, + ) + response = client.get("/api/v1/rag/status") + assert response.status_code == 200 + body = response.json() + assert body["embeddingModel"] == TEST_EMBEDDING_MODEL + assert "my-docs" in body["collections"] + + +def test_status_when_empty(client: TestClient) -> None: + response = client.get("/api/v1/rag/status") + assert response.status_code == 200 + body = response.json() + assert body == {"embeddingModel": TEST_EMBEDDING_MODEL, "collections": []} + + +# ── /index ────────────────────────────────────────────────────────────── + + +def test_index_returns_chunk_count(client: TestClient) -> None: + response = client.post( + "/api/v1/rag/index", + json={"collection": "indexed", "text": "Short text.", "source": "doc.pdf"}, + ) + assert response.status_code == 200 + body = response.json() + assert body["collection"] == "indexed" + assert body["chunksIndexed"] >= 1 + + +def test_index_rejects_empty_collection_name(client: TestClient) -> None: + response = client.post( + "/api/v1/rag/index", + json={"collection": "", "text": "Text.", "source": "x.pdf"}, + ) + assert response.status_code == 422 + + +def test_index_rejects_oversized_text(client: TestClient) -> None: + huge = "x" * 1_000_001 # Just over the 1MB cap + response = client.post( + "/api/v1/rag/index", + json={"collection": "toobig", "text": huge}, + ) + assert response.status_code == 422 + + +# ── /search ───────────────────────────────────────────────────────────── + + +def test_search_returns_results(client: TestClient) -> None: + client.post( + "/api/v1/rag/index", + json={"collection": "search-test", "text": "Python is fun.", "source": "guide.pdf"}, + ) + response = client.post( + "/api/v1/rag/search", + json={"query": "Python", "collection": "search-test", "topK": 3}, + ) + assert response.status_code == 200 + body = response.json() + assert body["query"] == "Python" + assert len(body["results"]) >= 1 + first = body["results"][0] + assert first["source"] == "guide.pdf" + assert "score" in first + + +def test_search_rejects_empty_collection_name(client: TestClient) -> None: + response = client.post( + "/api/v1/rag/search", + json={"query": "anything", "collection": ""}, + ) + assert response.status_code == 422 + + +def test_search_without_collection_searches_all(client: TestClient) -> None: + client.post( + "/api/v1/rag/index", + json={"collection": "col-one", "text": "Alpha content.", "source": "one.pdf"}, + ) + client.post( + "/api/v1/rag/index", + json={"collection": "col-two", "text": "Beta content.", "source": "two.pdf"}, + ) + response = client.post( + "/api/v1/rag/search", + json={"query": "content"}, + ) + assert response.status_code == 200 + body = response.json() + assert len(body["results"]) >= 1 + + +# ── /collections ──────────────────────────────────────────────────────── + + +def test_collections_empty_when_no_data(client: TestClient) -> None: + response = client.get("/api/v1/rag/collections") + assert response.status_code == 200 + assert response.json() == {"collections": []} + + +def test_collections_lists_indexed(client: TestClient) -> None: + client.post( + "/api/v1/rag/index", + json={"collection": "list-me", "text": "Text.", "source": "x.pdf"}, + ) + response = client.get("/api/v1/rag/collections") + assert response.status_code == 200 + assert "list-me" in response.json()["collections"] + + +# ── DELETE /collections/{name} ────────────────────────────────────────── + + +def test_delete_collection_removes_it(client: TestClient) -> None: + client.post( + "/api/v1/rag/index", + json={"collection": "to-delete", "text": "Text.", "source": "x.pdf"}, + ) + response = client.delete("/api/v1/rag/collections/to-delete") + assert response.status_code == 200 + assert response.json() == {"status": "deleted", "collection": "to-delete"} + + listing = client.get("/api/v1/rag/collections").json() + assert "to-delete" not in listing["collections"] + + +def test_delete_nonexistent_collection_is_idempotent(client: TestClient) -> None: + response = client.delete("/api/v1/rag/collections/never-existed") + assert response.status_code == 200 + assert response.json() == {"status": "deleted", "collection": "never-existed"} diff --git a/engine/tests/test_stirling_api.py b/engine/tests/test_stirling_api.py index ad07396325..98fa1f966f 100644 --- a/engine/tests/test_stirling_api.py +++ b/engine/tests/test_stirling_api.py @@ -1,3 +1,4 @@ +from conftest import build_app_settings from fastapi.testclient import TestClient from stirling.api import app @@ -11,7 +12,7 @@ from stirling.api.dependencies import ( get_pdf_question_agent, get_user_spec_agent, ) -from stirling.config import AppSettings, load_settings +from stirling.config import load_settings from stirling.contracts import ( AgentDraft, AgentDraftRequest, @@ -19,42 +20,30 @@ from stirling.contracts import ( AgentExecutionRequest, AgentRevisionRequest, AgentRevisionResponse, - CannotContinueExecutionAction, - EditCannotDoResponse, - OrchestratorRequest, - PdfEditRequest, - PdfQuestionNotFoundResponse, - PdfQuestionRequest, - UnsupportedCapabilityResponse, -) -from stirling.contracts.form_fill import ( AnalysedFileResult, + CannotContinueExecutionAction, CrossFileRole, DetectedRole, DocumentExtractionRequest, + EditCannotDoResponse, FileFillResult, FormAnalysisRequest, FormAnalysisResponse, FormFillBatchRequest, FormFillBatchResponse, KnowledgeUpdateResponse, + OrchestratorRequest, + PdfEditRequest, + PdfQuestionNeedContentResponse, + PdfQuestionNotFoundResponse, + PdfQuestionRequest, ) -from stirling.models.tool_models import RotateParams - - -class StubSettingsProvider: - def __call__(self) -> AppSettings: - return AppSettings( - smart_model_name="test", - fast_model_name="test", - smart_model_max_tokens=8192, - fast_model_max_tokens=2048, - ) +from stirling.models.tool_models import Angle, RotatePdfParams class StubOrchestratorAgent: - async def handle(self, request: OrchestratorRequest) -> UnsupportedCapabilityResponse: - return UnsupportedCapabilityResponse(capability="pdf_edit", message=request.user_message) + async def handle(self, request: OrchestratorRequest) -> PdfQuestionNeedContentResponse: + return PdfQuestionNeedContentResponse(reason=request.user_message, files=[], max_pages=1, max_characters=1000) class StubPdfEditAgent: @@ -127,14 +116,7 @@ class StubExecutionPlanningAgent: return CannotContinueExecutionAction(reason=str(request.current_step_index)) -client: TestClient = TestClient(app) - - -def override_settings() -> AppSettings: - return StubSettingsProvider()() - - -app.dependency_overrides[load_settings] = override_settings +app.dependency_overrides[load_settings] = build_app_settings app.dependency_overrides[get_orchestrator_agent] = lambda: StubOrchestratorAgent() app.dependency_overrides[get_pdf_edit_agent] = lambda: StubPdfEditAgent() app.dependency_overrides[get_pdf_question_agent] = lambda: StubPdfQuestionAgent() @@ -144,6 +126,8 @@ app.dependency_overrides[get_form_analyser_agent] = lambda: StubFormAnalyserAgen app.dependency_overrides[get_form_filler_agent] = lambda: StubFormFillerAgent() app.dependency_overrides[get_document_extractor_agent] = lambda: StubDocumentExtractorAgent() +client: TestClient = TestClient(app) + def test_health_route() -> None: response = client.get("/health") @@ -153,10 +137,10 @@ def test_health_route() -> None: def test_orchestrator_route() -> None: - response = client.post("/api/v1/orchestrator", json={"userMessage": "route this"}) + response = client.post("/api/v1/orchestrator", json={"userMessage": "route this", "fileNames": ["test.pdf"]}) assert response.status_code == 200 - assert response.json()["outcome"] == "unsupported_capability" + assert response.json()["outcome"] == "need_content" def test_pdf_edit_route() -> None: @@ -167,7 +151,14 @@ def test_pdf_edit_route() -> None: def test_pdf_questions_route() -> None: - response = client.post("/api/v1/pdf/questions", json={"question": "what is this?"}) + response = client.post( + "/api/v1/pdf/questions", + json={ + "question": "what is this?", + "fileNames": ["test.pdf"], + "pageText": [{"fileName": "test.pdf", "pages": [{"pageNumber": 1, "text": "Example"}]}], + }, + ) assert response.status_code == 200 assert response.json()["outcome"] == "not_found" @@ -192,8 +183,8 @@ def test_agent_revise_route() -> None: "steps": [ { "kind": "tool", - "tool": "rotate", - "parameters": RotateParams(angle=90).model_dump(by_alias=True), + "tool": "/api/v1/general/rotate-pdf", + "parameters": RotatePdfParams(angle=Angle(90)).model_dump(by_alias=True), } ], }, @@ -252,8 +243,8 @@ def test_next_action_route() -> None: "steps": [ { "kind": "tool", - "tool": "rotate", - "parameters": RotateParams(angle=90).model_dump(by_alias=True), + "tool": "/api/v1/general/rotate-pdf", + "parameters": RotatePdfParams(angle=Angle(90)).model_dump(by_alias=True), } ], }, diff --git a/engine/tests/test_stirling_contracts.py b/engine/tests/test_stirling_contracts.py index 526abb4be2..df183e8c82 100644 --- a/engine/tests/test_stirling_contracts.py +++ b/engine/tests/test_stirling_contracts.py @@ -1,36 +1,47 @@ -from collections.abc import Iterator - -import pytest - -from stirling.config import AppSettings, load_settings +from stirling.config import AppSettings from stirling.contracts import ( AgentExecutionRequest, AgentSpec, AgentSpecStep, EditPlanResponse, ExecutionContext, - OrchestratorRequest, - PdfQuestionAnswerResponse, - ToolOperationStep, -) -from stirling.contracts.form_fill import ( + ExtractedFileText, + ExtractedTextArtifact, KnowledgeEntry, KnowledgeUpdateResponse, + OrchestratorRequest, + PdfQuestionAnswerResponse, + PdfTextSelection, + ToolOperationStep, ) -from stirling.models.tool_models import OperationId, RotateParams +from stirling.models.tool_models import Angle, RotatePdfParams, ToolEndpoint def test_orchestrator_request_accepts_user_message() -> None: - request = OrchestratorRequest(user_message="Rotate the PDF") + request = OrchestratorRequest( + user_message="Rotate the PDF", + file_names=["test.pdf"], + artifacts=[ + ExtractedTextArtifact( + files=[ + ExtractedFileText( + file_name="test.pdf", + pages=[PdfTextSelection(page_number=1, text="Hello")], + ) + ] + ) + ], + ) assert request.user_message == "Rotate the PDF" + assert len(request.artifacts) == 1 def test_agent_execution_request_uses_typed_agent_spec() -> None: steps: list[AgentSpecStep] = [ ToolOperationStep( - tool=OperationId.ROTATE, - parameters=RotateParams(angle=90), + tool=ToolEndpoint.ROTATE_PDF, + parameters=RotatePdfParams(angle=Angle(90)), ) ] request = AgentExecutionRequest( @@ -48,13 +59,13 @@ def test_agent_execution_request_uses_typed_agent_spec() -> None: def test_edit_plan_response_has_typed_steps() -> None: - steps = [ToolOperationStep(tool=OperationId.ROTATE, parameters=RotateParams(angle=90))] + steps = [ToolOperationStep(tool=ToolEndpoint.ROTATE_PDF, parameters=RotatePdfParams(angle=Angle(90)))] response = EditPlanResponse( summary="Rotate the input PDF by 90 degrees.", steps=steps, ) - assert response.steps[0].tool == OperationId.ROTATE + assert response.steps[0].tool == ToolEndpoint.ROTATE_PDF def test_pdf_question_answer_defaults_evidence_list() -> None: @@ -63,13 +74,6 @@ def test_pdf_question_answer_defaults_evidence_list() -> None: assert response.evidence == [] -@pytest.fixture(autouse=True) -def clear_settings_cache() -> Iterator[None]: - load_settings.cache_clear() - yield - load_settings.cache_clear() - - def test_knowledge_update_response_discriminator() -> None: update = KnowledgeUpdateResponse( proposed_entries=[KnowledgeEntry(key="name", value="John", source="CV")], @@ -79,11 +83,25 @@ def test_knowledge_update_response_discriminator() -> None: def test_app_settings_accepts_model_configuration() -> None: + from pathlib import Path + + from stirling.config import RagBackend + settings = AppSettings( smart_model_name="claude-sonnet-4-5-20250929", fast_model_name="claude-haiku-4-5-20251001", smart_model_max_tokens=8192, fast_model_max_tokens=2048, + rag_backend=RagBackend.SQLITE, + rag_embedding_model="voyageai:voyage-4", + rag_store_path=Path(":memory:"), + rag_pgvector_dsn="", + rag_chunk_size=512, + rag_chunk_overlap=64, + rag_default_top_k=5, + posthog_enabled=False, + posthog_api_key="", + posthog_host="https://eu.i.posthog.com", ) assert settings.smart_model_name diff --git a/engine/tests/test_user_spec_agent.py b/engine/tests/test_user_spec_agent.py index fee8595b6f..3a91fcf1fb 100644 --- a/engine/tests/test_user_spec_agent.py +++ b/engine/tests/test_user_spec_agent.py @@ -4,7 +4,6 @@ import pytest from pydantic import ValidationError from stirling.agents import UserSpecAgent -from stirling.config import AppSettings from stirling.contracts import ( AgentDraft, AgentDraftRequest, @@ -14,35 +13,28 @@ from stirling.contracts import ( EditPlanResponse, ToolOperationStep, ) -from stirling.models.tool_models import CompressParams, OperationId, RotateParams -from stirling.services import build_runtime - - -def build_test_settings() -> AppSettings: - return AppSettings( - smart_model_name="test", - fast_model_name="test", - smart_model_max_tokens=8192, - fast_model_max_tokens=2048, - ) +from stirling.models.tool_models import Angle, FlattenParams, RotatePdfParams, ToolEndpoint +from stirling.services.runtime import AppRuntime class StubUserSpecAgent(UserSpecAgent): - def __init__(self, draft_result: AgentDraft, revision_result: AgentDraft) -> None: - super().__init__(build_runtime(build_test_settings())) + def __init__(self, runtime: AppRuntime, draft_result: AgentDraft, revision_result: AgentDraft) -> None: + super().__init__(runtime) self.draft_result = draft_result self.revision_result = revision_result self.edit_plan = EditPlanResponse( summary="Rotate the document.", steps=[ ToolOperationStep( - tool=OperationId.ROTATE, - parameters=RotateParams(angle=90), + tool=ToolEndpoint.ROTATE_PDF, + parameters=RotatePdfParams(angle=Angle(90)), ) ], ) - async def _build_edit_plan(self, user_message: str) -> EditPlanResponse: + async def _build_edit_plan( + self, user_message: str, conversation_history: list[ConversationMessage] + ) -> EditPlanResponse: return self.edit_plan async def _run_draft_agent(self, request: AgentDraftRequest, edit_plan: EditPlanResponse) -> AgentDraft: @@ -53,10 +45,12 @@ class StubUserSpecAgent(UserSpecAgent): class ClarifyingUserSpecAgent(UserSpecAgent): - def __init__(self) -> None: - super().__init__(build_runtime(build_test_settings())) + def __init__(self, runtime: AppRuntime) -> None: + super().__init__(runtime) - async def _build_edit_plan(self, user_message: str) -> EditClarificationRequest: + async def _build_edit_plan( + self, user_message: str, conversation_history: list[ConversationMessage] + ) -> EditClarificationRequest: return EditClarificationRequest( question="Which pages should be changed?", reason="The request does not specify the target pages.", @@ -64,16 +58,17 @@ class ClarifyingUserSpecAgent(UserSpecAgent): @pytest.mark.anyio -async def test_user_spec_agent_drafts_agent_spec() -> None: +async def test_user_spec_agent_drafts_agent_spec(runtime: AppRuntime) -> None: agent = StubUserSpecAgent( + runtime, AgentDraft( name="Invoice Cleanup", description="Prepare invoices for review.", objective="Normalize invoices before accounting review.", steps=[ ToolOperationStep( - tool=OperationId.ROTATE, - parameters=RotateParams(angle=90), + tool=ToolEndpoint.ROTATE_PDF, + parameters=RotatePdfParams(angle=Angle(90)), ) ], ), @@ -100,19 +95,20 @@ async def test_user_spec_agent_drafts_agent_spec() -> None: @pytest.mark.anyio -async def test_user_spec_agent_revises_existing_draft() -> None: +async def test_user_spec_agent_revises_existing_draft(runtime: AppRuntime) -> None: current_draft = AgentDraft( name="Invoice Cleanup", description="Prepare invoices for review.", objective="Normalize invoices before accounting review.", steps=[ ToolOperationStep( - tool=OperationId.ROTATE, - parameters=RotateParams(angle=90), + tool=ToolEndpoint.ROTATE_PDF, + parameters=RotatePdfParams(angle=Angle(90)), ) ], ) agent = StubUserSpecAgent( + runtime, draft_result=current_draft, revision_result=AgentDraft( name="Invoice Cleanup", @@ -120,12 +116,12 @@ async def test_user_spec_agent_revises_existing_draft() -> None: objective="Normalize invoices before accounting review.", steps=[ ToolOperationStep( - tool=OperationId.ROTATE, - parameters=RotateParams(angle=90), + tool=ToolEndpoint.ROTATE_PDF, + parameters=RotatePdfParams(angle=Angle(90)), ), ToolOperationStep( - tool=OperationId.COMPRESS, - parameters=CompressParams(compression_level=5), + tool=ToolEndpoint.FLATTEN, + parameters=FlattenParams(flatten_only_forms=False, render_dpi=None), ), ], ), @@ -146,14 +142,14 @@ async def test_user_spec_agent_revises_existing_draft() -> None: def test_tool_operation_step_rejects_mismatched_parameters() -> None: with pytest.raises(ValidationError): ToolOperationStep( - tool=OperationId.ROTATE, - parameters=CompressParams(compression_level=5), + tool=ToolEndpoint.ROTATE_PDF, + parameters=FlattenParams(flatten_only_forms=False, render_dpi=None), ) @pytest.mark.anyio -async def test_user_spec_agent_propagates_edit_clarification() -> None: - agent = ClarifyingUserSpecAgent() +async def test_user_spec_agent_propagates_edit_clarification(runtime: AppRuntime) -> None: + agent = ClarifyingUserSpecAgent(runtime) response = await agent.draft(AgentDraftRequest(user_message="Build an agent to rotate some pages.")) diff --git a/engine/uv.lock b/engine/uv.lock index 9292fd2393..c8f79e6372 100644 --- a/engine/uv.lock +++ b/engine/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.13" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.14'", +] [[package]] name = "ag-ui-protocol" @@ -37,7 +41,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.3" +version = "3.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -48,59 +52,68 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, - { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, - { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, - { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, - { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, - { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, - { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, - { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, - { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, - { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, - { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, - { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, - { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, - { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, - { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, - { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, - { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, - { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, - { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, - { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, - { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, - { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, - { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, - { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, - { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +] + +[[package]] +name = "aiolimiter" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/23/b52debf471f7a1e42e362d959a3982bdcb4fe13a5d46e63d28868807a79c/aiolimiter-1.2.1.tar.gz", hash = "sha256:e02a37ea1a855d9e832252a105420ad4d15011505512a1a1d814647451b5cca9", size = 7185, upload-time = "2024-12-08T15:31:51.496Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/ba/df6e8e1045aebc4778d19b8a3a9bc1808adb1619ba94ca354d9ba17d86c3/aiolimiter-1.2.1-py3-none-any.whl", hash = "sha256:d3f249e9059a20badcb56b61601a83556133655c11d1eb3dd3e04ff069e5f3c7", size = 6711, upload-time = "2024-12-08T15:31:49.874Z" }, ] [[package]] @@ -135,7 +148,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.86.0" +version = "0.93.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -147,9 +160,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/7a/8b390dc47945d3169875d342847431e5f7d5fa716b2e37494d57cfc1db10/anthropic-0.86.0.tar.gz", hash = "sha256:60023a7e879aa4fbb1fed99d487fe407b2ebf6569603e5047cfe304cebdaa0e5", size = 583820, upload-time = "2026-03-18T18:43:08.017Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/70/2429d6f7c2516db99fb342c3ad89575ab3e0cd31d3d2f6cba5fdf5e9c65b/anthropic-0.93.0.tar.gz", hash = "sha256:fea8376f7d5cdf99d5e8e85a48fe7a7bd8ab307cdfee4b1e8283a18b1c0ce1b5", size = 654155, upload-time = "2026-04-09T18:13:53.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/5f/67db29c6e5d16c8c9c4652d3efb934d89cb750cad201539141781d8eae14/anthropic-0.86.0-py3-none-any.whl", hash = "sha256:9d2bbd339446acce98858c5627d33056efe01f70435b22b63546fe7edae0cd57", size = 469400, upload-time = "2026-03-18T18:43:06.526Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7b/5b2c11902707c49c7a99418eb027ed3eb63876193fee5c80b5c878e3a673/anthropic-0.93.0-py3-none-any.whl", hash = "sha256:2c20b2ce6d305564c66a6cbaedddee8efdd3b9753098bf314093fcf4c662d04c", size = 627482, upload-time = "2026-04-09T18:13:51.606Z" }, ] [[package]] @@ -194,6 +207,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, ] +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + [[package]] name = "beartype" version = "0.22.9" @@ -203,6 +225,33 @@ 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 = "black" +version = "26.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { 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" } +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" }, +] + [[package]] name = "boto3" version = "1.42.74" @@ -410,55 +459,55 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.5" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, ] [[package]] @@ -476,6 +525,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/2261922126b2e50c601fe22d7ff5194e0a4d50e654836260c0665e24d862/cyclopts-4.10.1-py3-none-any.whl", hash = "sha256:35f37257139380a386d9fe4475e1e7c87ca7795765ef4f31abba579fcfcb6ecd", size = 204331, upload-time = "2026-03-23T14:43:02.625Z" }, ] +[[package]] +name = "datamodel-code-generator" +version = "0.56.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "black" }, + { name = "genson" }, + { name = "inflect" }, + { name = "isort" }, + { 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" } +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" }, +] + +[package.optional-dependencies] +ruff = [ + { name = "ruff" }, +] + [[package]] name = "distro" version = "1.9.0" @@ -531,34 +604,52 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "fastapi" }, + { name = "opentelemetry-sdk" }, + { name = "pgvector" }, + { name = "posthog" }, + { name = "psycopg", extra = ["binary"] }, { name = "pydantic" }, { name = "pydantic-ai" }, + { name = "pydantic-ai-slim", extra = ["voyageai"] }, { name = "pydantic-settings" }, { name = "python-dotenv" }, + { name = "sqlite-vec" }, { name = "uvicorn" }, ] [package.dev-dependencies] dev = [ + { name = "anyio" }, + { name = "datamodel-code-generator", extra = ["ruff"] }, { name = "pyright" }, { name = "pytest" }, + { name = "referencing" }, { name = "ruff" }, ] [package.metadata] requires-dist = [ { 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"], specifier = ">=3.2" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pydantic-ai", specifier = ">=1.67.0" }, + { name = "pydantic-ai-slim", extras = ["voyageai"], specifier = ">=1.67.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" }, ] @@ -636,7 +727,7 @@ wheels = [ [[package]] name = "fastmcp" -version = "3.1.1" +version = "3.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, @@ -661,9 +752,21 @@ dependencies = [ { name = "watchfiles" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/83/c95d3bf717698a693eccb43e137a32939d2549876e884e246028bff6ecce/fastmcp-3.1.1.tar.gz", hash = "sha256:db184b5391a31199323766a3abf3a8bfbb8010479f77eca84c0e554f18655c48", size = 17347644, upload-time = "2026-03-14T19:12:20.235Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/42/7eed0a38e3b7a386805fecacf8a5a9353a2b3040395ef9e30e585d8549ac/fastmcp-3.2.3.tar.gz", hash = "sha256:4f02ae8b00227285a0cf6544dea1db29b022c8cdd8d3dfdec7118540210ae60a", size = 26328743, upload-time = "2026-04-09T22:05:03.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/ea/570122de7e24f72138d006f799768e14cc1ccf7fcb22b7750b2bd276c711/fastmcp-3.1.1-py3-none-any.whl", hash = "sha256:8132ba069d89f14566b3266919d6d72e2ec23dd45d8944622dca407e9beda7eb", size = 633754, upload-time = "2026-03-14T19:12:22.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/84b6dcba793178a44b9d99b4def6cd62f870dcfc5bb7b9153ac390135812/fastmcp-3.2.3-py3-none-any.whl", hash = "sha256:cc50af6eed1f62ed8b6ebf4987286d8d1d006f08d5bec739d5c7fb76160e0911", size = 707260, upload-time = "2026-04-09T22:05:01.225Z" }, +] + +[[package]] +name = "ffmpeg-python" +version = "0.2.0" +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" } +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" }, ] [[package]] @@ -757,6 +860,15 @@ 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" }, +] + [[package]] name = "genai-prices" version = "0.0.56" @@ -770,6 +882,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/f6/8ef7e4c286deb2709d11ca96a5237caae3ef4876ab3c48095856cfd2df30/genai_prices-0.0.56-py3-none-any.whl", hash = "sha256:dbe86be8f3f556bed1b72209ed36851fec8b01793b3b220f42921a4e7da945f6", size = 68966, upload-time = "2026-03-20T20:33:02.555Z" }, ] +[[package]] +name = "genson" +version = "1.3.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" } +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" }, +] + [[package]] name = "google-auth" version = "2.49.1" @@ -997,6 +1118,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] +[[package]] +name = "inflect" +version = "7.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, + { name = "typeguard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/c6/943357d44a21fd995723d07ccaddd78023eace03c1846049a2645d4324a3/inflect-7.5.0.tar.gz", hash = "sha256:faf19801c3742ed5a05a8ce388e0d8fe1a07f8d095c82201eb904f5d27ad571f", size = 73751, upload-time = "2024-12-28T17:11:18.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/eb/427ed2b20a38a4ee29f24dbe4ae2dafab198674fe9a85e3d6adf9e5f5f41/inflect-7.5.0-py3-none-any.whl", hash = "sha256:2aea70e5e70c35d8350b8097396ec155ffd68def678c7ff97f51aa69c1d92344", size = 35197, upload-time = "2024-12-28T17:11:15.931Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -1006,6 +1140,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "isort" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, +] + [[package]] name = "jaraco-classes" version = "3.4.0" @@ -1048,6 +1191,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "jiter" version = "0.13.0" @@ -1108,6 +1263,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer", marker = "python_full_version < '3.14'" }, +] +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 = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + [[package]] name = "jsonpath-python" version = "1.1.5" @@ -1117,6 +1284,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/50/1a313fb700526b134c71eb8a225d8b83be0385dbb0204337b4379c698cef/jsonpath_python-1.1.5-py3-none-any.whl", hash = "sha256:a60315404d70a65e76c9a782c84e50600480221d94a58af47b7b4d437351cb4b", size = 14090, upload-time = "2026-03-17T06:16:39.152Z" }, ] +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, +] + [[package]] name = "jsonref" version = "1.1.0" @@ -1184,6 +1360,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, ] +[[package]] +name = "langchain-core" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch", 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'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/fe/20190232d9b513242899dbb0c2bb77e31b4d61e343743adbe90ebc2603d2/langchain_core-1.3.0.tar.gz", hash = "sha256:14a39f528bf459aa3aa40d0a7f7f1bae7520d435ef991ae14a4ceb74d8c49046", size = 860755, upload-time = "2026-04-17T14:51:38.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/e2/dbfa347aa072a6dc4cd38d6f9ebfc730b4c14c258c47f480f4c5c546f177/langchain_core-1.3.0-py3-none-any.whl", hash = "sha256:baf16ee028475df177b9ab8869a751c79406d64a6f12125b93802991b566cced", size = 515140, upload-time = "2026-04-17T14:51:36.274Z" }, +] + +[[package]] +name = "langchain-text-splitters" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core", marker = "python_full_version < '3.14'" }, +] +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 = [ + { url = "https://files.pythonhosted.org/packages/d3/26/1ef06f56198d631296d646a6223de35bcc6cf9795ceb2442816bc963b84c/langchain_text_splitters-1.1.2-py3-none-any.whl", hash = "sha256:a2de0d799ff31886429fd6e2e0032df275b60ec817c19059a7b46181cc1c2f10", size = 35903, upload-time = "2026-04-16T14:20:38.243Z" }, +] + +[[package]] +name = "langsmith" +version = "0.7.32" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { 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 = "uuid-utils", marker = "python_full_version < '3.14'" }, + { name = "xxhash", marker = "python_full_version < '3.14'" }, + { name = "zstandard", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/b4/a0b4a501bee6b8a741ce29f8c48155b132118483cddc6f9247735ddb38fa/langsmith-0.7.32.tar.gz", hash = "sha256:b59b8e106d0e4c4842e158229296086e2aa7c561e3f602acda73d3ad0062e915", size = 1184518, upload-time = "2026-04-15T23:42:41.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/bc/148f98ac7dad73ac5e1b1c985290079cfeeb9ba13d760a24f25002beb2c9/langsmith-0.7.32-py3-none-any.whl", hash = "sha256:e1fde928990c4c52f47dc5132708cec674355d9101723d564183e965f383bf5f", size = 378272, upload-time = "2026-04-15T23:42:39.905Z" }, +] + [[package]] name = "logfire" version = "4.30.0" @@ -1228,6 +1455,58 @@ 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" }, ] +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { 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.26.0" @@ -1371,6 +1650,15 @@ wheels = [ { 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" }, ] +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "nexus-rpc" version = "1.2.0" @@ -1392,6 +1680,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "numpy" +version = "2.4.4" +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" } +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" }, +] + [[package]] name = "openai" version = "2.29.0" @@ -1545,6 +1883,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199", size = 8947, upload-time = "2025-12-11T13:36:37.151Z" }, ] +[[package]] +name = "orjson" +version = "3.11.8" +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" } +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" }, +] + [[package]] name = "packaging" version = "25.0" @@ -1563,6 +1939,85 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, ] +[[package]] +name = "pathspec" +version = "1.0.4" +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" } +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" }, +] + +[[package]] +name = "pgvector" +version = "0.4.2" +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" } +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" }, +] + +[[package]] +name = "pillow" +version = "12.2.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" } +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" }, +] + [[package]] name = "platformdirs" version = "4.9.4" @@ -1581,6 +2036,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "posthog" +version = "7.9.12" +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" } +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" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -1677,6 +2149,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] +[[package]] +name = "psycopg" +version = "3.3.3" +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" } +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" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.3" +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" }, +] + [[package]] name = "py-key-value-aio" version = "0.4.4" @@ -1843,6 +2361,9 @@ vertexai = [ { name = "google-auth" }, { name = "requests" }, ] +voyageai = [ + { name = "voyageai", marker = "python_full_version < '3.14'" }, +] xai = [ { name = "xai-sdk" }, ] @@ -2037,6 +2558,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { 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" @@ -2195,6 +2740,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests", marker = "python_full_version < '3.14'" }, +] +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 = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + [[package]] name = "rich" version = "14.3.3" @@ -2364,6 +2921,18 @@ 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 = "sqlite-vec" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/85/9fad0045d8e7c8df3e0fa5a56c630e8e15ad6e5ca2e6106fceb666aa6638/sqlite_vec-0.1.9-py3-none-macosx_10_6_x86_64.whl", hash = "sha256:1b62a7f0a060d9475575d4e599bbf94a13d85af896bc1ce86ee80d1b5b48e5fb", size = 131171, upload-time = "2026-03-31T08:02:31.717Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3d/3677e0cd2f92e5ebc43cd29fbf565b75582bff1ccfa0b8327c7508e1084f/sqlite_vec-0.1.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d52e30513bae4cc9778ddbf6145610434081be4c3afe57cd877893bad9f6b6c", size = 165434, upload-time = "2026-03-31T08:02:32.712Z" }, + { url = "https://files.pythonhosted.org/packages/00/d4/f2b936d3bdc38eadcbd2a87875815db36430fab0363182ba5d12cd8e0b51/sqlite_vec-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e921e592f24a5f9a18f590b6ddd530eb637e2d474e3b1972f9bbeb773aa3cb9", size = 160076, upload-time = "2026-03-31T08:02:33.796Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ad/6afd073b0f817b3e03f9e37ad626ae341805891f23c74b5292818f49ac63/sqlite_vec-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64.whl", hash = "sha256:1515727990b49e79bcaf75fdee2ffc7d461f8b66905013231251f1c8938e7786", size = 163388, upload-time = "2026-03-31T08:02:34.888Z" }, + { url = "https://files.pythonhosted.org/packages/42/89/81b2907cda14e566b9bf215e2ad82fc9b349edf07d2010756ffdb902f328/sqlite_vec-0.1.9-py3-none-win_amd64.whl", hash = "sha256:4a28dc12fa4b53d7b1dced22da2488fade444e96b5d16fd2d698cd670675cf32", size = 292804, upload-time = "2026-03-31T08:02:36.035Z" }, +] + [[package]] name = "sse-starlette" version = "3.3.3" @@ -2495,6 +3064,18 @@ 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" }, ] +[[package]] +name = "typeguard" +version = "4.5.1" +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" } +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" }, +] + [[package]] name = "typer" version = "0.24.1" @@ -2552,6 +3133,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tzdata" +version = "2026.1" +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" } +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" }, +] + [[package]] name = "uncalled-for" version = "0.2.0" @@ -2570,6 +3160,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] +[[package]] +name = "uuid-utils" +version = "0.14.1" +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" } +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" }, +] + [[package]] name = "uvicorn" version = "0.42.0" @@ -2583,6 +3195,27 @@ 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" }, ] +[[package]] +name = "voyageai" +version = "0.3.7" +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'" }, +] +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" } +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" }, +] + [[package]] name = "watchfiles" version = "1.1.1" @@ -2743,6 +3376,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/5d/62c394d46e56e43989b34977b490f84bd60ff715e8ea696880f63546b8be/xai_sdk-1.9.1-py3-none-any.whl", hash = "sha256:3f313f1238d847ec08401894c42bf91f034fdeae04fe279c974b0e2a9644573d", size = 247192, upload-time = "2026-03-19T22:57:45.447Z" }, ] +[[package]] +name = "xxhash" +version = "3.6.0" +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" } +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" }, +] + [[package]] name = "yarl" version = "1.23.0" @@ -2837,3 +3538,43 @@ sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50e 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" }, ] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { 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/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000000..3ad5fb2f0b --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,13 @@ +dist/ +# Tauri/Cargo build output (binary assets named *.js etc. confuse Prettier) +src-tauri/target/ +node_modules/ +public/vendor/ +public/pdfjs*/ +public/js/thirdParty/ +public/css/cookieconsent.css +src-tauri/target/ +*.min.* +*.md +*.wxs +src/output.css diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000000..58bc875631 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "endOfLine": "lf" +} diff --git a/frontend/README.md b/frontend/README.md index e3bd1887b0..a8802b24ef 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,145 +1,79 @@ # Frontend + +All frontend commands are run from the repository root using [Task](https://taskfile.dev/): + +- `task frontend:dev` — start Vite dev server (localhost:5173) +- `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:typecheck` — run TypeScript type checking +- `task frontend:check` — run typecheck + lint + test +- `task frontend:install` — install npm dependencies + +For desktop app development, see the [Tauri](#tauri) section below. + ## Environment Variables -The frontend requires environment variables to be set before running. `npm run dev` will create a `.env` file for you automatically on first run using the defaults from `config/.env.example` - for most development work this is all you need. +The frontend requires environment variables to be set before running. `task frontend:dev` will create a `.env` file for you automatically on first run using the defaults from `config/.env.example` - for most development work this is all you need. If you need to configure specific services (Google Drive, Supabase, Stripe, PostHog), edit your local `.env` file. The values in `config/.env.example` show what each variable does and provides sensible defaults where applicable. -For desktop (Tauri) development, `npm run tauri-dev` will additionally create a `.env.desktop` file from `config/.env.desktop.example`. +For desktop (Tauri) development, `task desktop:dev` will additionally create a `.env.desktop` file from `config/.env.desktop.example`. ## Docker Setup For Docker deployments and configuration, see the [Docker README](../docker/README.md). -## Available Scripts - -In the project directory, you can run: - -### `npm start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. - -The page will reload when you make changes.\ -You may also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `npm run build` - -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `npm run eject` - -**Note: this is a one-way operation. Once you `eject`, you can't go back!** - -If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. - -You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) - -### Analyzing the Bundle Size - -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) - -### Making a Progressive Web App - -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) - -### Advanced Configuration - -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) - -### Deployment - -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) - -### `npm run build` fails to minify - -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) - - ## Tauri -In order to run Tauri, you first have to build the Java backend for Tauri to use. -**macOS/Linux:** - -From the root of the repo, run: - -```bash -./gradlew clean build -./scripts/build-tauri-jlink.sh -``` - -**Windows** - -From the root of the repo, run: - -```batch -gradlew clean build -scripts\build-tauri-jlink.bat -``` - -### Testing the Bundled Runtime - -Before building the full Tauri app, you can test the bundled runtime: - -**macOS/Linux:** -```bash -./frontend/src-tauri/runtime/launch-stirling.sh -``` - -**Windows:** -```cmd -frontend\src-tauri\runtime\launch-stirling.bat -``` - -This will start Stirling-PDF using the bundled JRE, accessible at http://localhost:8080 +All desktop tasks are available via [Task](https://taskfile.dev). From the root of the repo: ### Dev -To run Tauri in development. Use the command in the `frontend` folder: ```bash -npm run tauri-dev +task desktop:dev ``` -This will run the gradle runboot command and the tauri dev command concurrently, starting the app once both are stable. - -> [!NOTE] -> -> Desktop builds require additional environment variables. See [Environment Variables](#environment-variables) -> above - `npm run tauri-dev` will set these up automatically from `config/.env.desktop.example` on first run. +This ensures the JLink runtime and backend JAR exist (skipping if already built), then starts Tauri in dev mode. ### Build -To build a deployment of the Tauri app. Use this command in the `frontend` folder: ```bash -npm run tauri-build +task desktop:build ``` -This will bundle the backend and frontend into one executable for each target. Targets can be set within the `tauri.conf.json` file. +This does a full clean rebuild of the backend JAR and JLink runtime, then builds the Tauri app for production. + +Platform-specific dev builds are also available: + +```bash +task desktop:build:dev # No bundling +task desktop:build:dev:mac # macOS .app bundle +task desktop:build:dev:windows # Windows NSIS installer +task desktop:build:dev:linux # Linux AppImage +``` + +### JLink Tasks + +You can also run JLink steps individually: + +```bash +task desktop:jlink # Build JAR + create JLink runtime +task desktop:jlink:jar # Build backend JAR only +task desktop:jlink:runtime # Create JLink custom JRE only +task desktop:jlink:clean # Remove JLink artifacts +``` + +### Clean + +```bash +task desktop:clean +``` + +Removes all desktop build artifacts including JLink runtime, bundled JARs, Cargo build, and dist/build directories. > [!NOTE] > > Desktop builds require additional environment variables. See [Environment Variables](#environment-variables) -> above - `npm run tauri-build` will set these up automatically from `config/.env.desktop.example` on first run. +> above - `task desktop:dev` will set these up automatically from `config/.env.desktop.example` on first run. diff --git a/frontend/config/.env.desktop.example b/frontend/config/.env.desktop.example index 2e58bebec8..a83666a4e1 100644 --- a/frontend/config/.env.desktop.example +++ b/frontend/config/.env.desktop.example @@ -10,4 +10,4 @@ VITE_SAAS_BACKEND_API_URL=https://api2.stirling.com # Dev only: set to true to mimic an expired access token (no valid JWT for API/auth checks). # Production builds ignore this. Restart tauri-dev after changing. -VITE_DEV_SIMULATE_EXPIRED_JWT=false +VITE_DEV_SIMULATE_EXPIRED_JWT= diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 6f0ed90cbb..a963558ec1 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -1,62 +1,55 @@ // @ts-check -import eslint from '@eslint/js'; -import globals from 'globals'; -import { defineConfig } from 'eslint/config'; -import tseslint from 'typescript-eslint'; +import eslint from "@eslint/js"; +import globals from "globals"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; -const srcGlobs = [ - 'src/**/*.{js,mjs,jsx,ts,tsx}', -]; -const nodeGlobs = [ - 'scripts/**/*.{js,ts,mjs}', - '*.config.{js,ts,mjs}', -]; +const srcGlobs = ["src/**/*.{js,mjs,jsx,ts,tsx}"]; +const nodeGlobs = ["scripts/**/*.{js,ts,mjs}", "*.config.{js,ts,mjs}"]; const baseRestrictedImportPatterns = [ - { regex: '^\\.', message: "Use @app/* imports instead of relative imports." }, - { regex: '^src/', message: "Use @app/* imports instead of absolute src/ imports." }, + { regex: "^\\.", message: "Use @app/* imports instead of relative imports." }, + { + regex: "^src/", + message: "Use @app/* imports instead of absolute src/ imports.", + }, ]; export default defineConfig( { // Everything that contains 3rd party code that we don't want to lint - ignores: [ - 'dist', - 'node_modules', - 'public', - 'src-tauri', - ], + ignores: ["dist", "node_modules", "public", "src-tauri"], }, eslint.configs.recommended, tseslint.configs.recommended, { rules: { - 'no-restricted-imports': [ - 'error', + "no-restricted-imports": [ + "error", { patterns: baseRestrictedImportPatterns, }, ], - '@typescript-eslint/no-empty-object-type': [ - 'error', + "@typescript-eslint/no-empty-object-type": [ + "error", { // Allow empty extending interfaces because there's no real reason not to, and it makes it obvious where to put extra attributes in the future - allowInterfaces: 'with-single-extends', + allowInterfaces: "with-single-extends", }, ], - '@typescript-eslint/no-explicit-any': 'off', // Temporarily disabled until codebase conformant - '@typescript-eslint/no-require-imports': 'off', // Temporarily disabled until codebase conformant - '@typescript-eslint/no-unused-vars': [ - 'error', + "@typescript-eslint/no-explicit-any": "off", // Temporarily disabled until codebase conformant + "@typescript-eslint/no-require-imports": "off", // Temporarily disabled until codebase conformant + "@typescript-eslint/no-unused-vars": [ + "error", { - 'args': 'all', // All function args must be used (or explicitly ignored) - 'argsIgnorePattern': '^_', // Allow unused variables beginning with an underscore - 'caughtErrors': 'all', // Caught errors must be used (or explicitly ignored) - 'caughtErrorsIgnorePattern': '^_', // Allow unused variables beginning with an underscore - 'destructuredArrayIgnorePattern': '^_', // Allow unused variables beginning with an underscore - 'varsIgnorePattern': '^_', // Allow unused variables beginning with an underscore - 'ignoreRestSiblings': true, // Allow unused variables when removing attributes from objects (otherwise this requires explicit renaming like `({ x: _x, ...y }) => y`, which is clunky) + args: "all", // All function args must be used (or explicitly ignored) + argsIgnorePattern: "^_", // Allow unused variables beginning with an underscore + caughtErrors: "all", // Caught errors must be used (or explicitly ignored) + caughtErrorsIgnorePattern: "^_", // Allow unused variables beginning with an underscore + destructuredArrayIgnorePattern: "^_", // Allow unused variables beginning with an underscore + varsIgnorePattern: "^_", // Allow unused variables beginning with an underscore + ignoreRestSiblings: true, // Allow unused variables when removing attributes from objects (otherwise this requires explicit renaming like `({ x: _x, ...y }) => y`, which is clunky) }, ], }, @@ -65,16 +58,17 @@ export default defineConfig( // Use the stub/shadow pattern instead: define a stub in src/core/ and override in src/desktop/. { files: srcGlobs, - ignores: ['src/desktop/**'], + ignores: ["src/desktop/**"], rules: { - 'no-restricted-imports': [ - 'error', + "no-restricted-imports": [ + "error", { patterns: [ ...baseRestrictedImportPatterns, { - regex: '^@tauri-apps/', - message: "Tauri APIs are desktop-only. Review frontend/DeveloperGuide.md for structure advice.", + regex: "^@tauri-apps/", + message: + "Tauri APIs are desktop-only. Review frontend/DeveloperGuide.md for structure advice.", }, ], }, @@ -83,7 +77,12 @@ export default defineConfig( }, // Folders that have been cleaned up and are now conformant - stricter rules enforced here { - files: ['src/saas/**/*.{js,mjs,jsx,ts,tsx}'], + files: [ + "src/desktop/**/*.{js,mjs,jsx,ts,tsx}", + "src/proprietary/**/*.{js,mjs,jsx,ts,tsx}", + "src/saas/**/*.{js,mjs,jsx,ts,tsx}", + "src/prototypes/**/*.{js,mjs,jsx,ts,tsx}", + ], languageOptions: { parserOptions: { project: true, @@ -91,8 +90,8 @@ export default defineConfig( }, }, rules: { - '@typescript-eslint/no-explicit-any': 'error', - '@typescript-eslint/no-unnecessary-type-assertion': 'error', + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-unnecessary-type-assertion": "error", }, }, // Config for browser scripts @@ -101,8 +100,8 @@ export default defineConfig( languageOptions: { globals: { ...globals.browser, - } - } + }, + }, }, // Config for node scripts { @@ -110,7 +109,7 @@ export default defineConfig( languageOptions: { globals: { ...globals.node, - } - } + }, + }, }, ); diff --git a/frontend/index.html b/frontend/index.html index c790b8d1ed..98b620b407 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,4 +1,4 @@ - + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f1ce75050c..0bb7efc56f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -59,7 +59,7 @@ "@tauri-apps/plugin-shell": "^2.3.5", "@userback/widget": "^0.3.12", "autoprefixer": "^10.4.21", - "axios": "^1.13.2", + "axios": "^1.15.0", "d3": "^7.9.0", "globals": "^17.1.0", "i18next": "^25.5.2", @@ -68,6 +68,7 @@ "license-report": "^6.8.0", "pdfjs-dist": "^5.4.149", "peerjs": "^1.5.5", + "pixelmatch": "^7.1.0", "posthog-js": "^1.268.0", "qrcode.react": "^4.2.0", "react": "^19.1.1", @@ -114,6 +115,7 @@ "postcss-cli": "^11.0.1", "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", + "prettier": "^3.8.1", "puppeteer": "^24.25.0", "tsx": "^4.21.0", "typescript": "^5.9.2", @@ -214,9 +216,9 @@ "license": "MIT" }, "node_modules/@atlaskit/pragmatic-drag-and-drop": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.7.7.tgz", - "integrity": "sha512-jX+68AoSTqO/fhCyJDTZ38Ey6/wyL2Iq+J/moanma0YyktpnoHxevjY1UNJHYp0NCburdQDZSL1ZFac1mO1osQ==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.7.9.tgz", + "integrity": "sha512-m/bcw5flyjfcF/rdX4JeomtIBrWuDNOwcQieiywHv7zkfIRmUC34Q9ZLeNGVoz73UiGsRqxysMuw4tC7lSJ89g==", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.0.0", @@ -295,9 +297,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -310,9 +312,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -374,9 +376,9 @@ } }, "node_modules/@cantoo/pdf-lib": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@cantoo/pdf-lib/-/pdf-lib-2.6.1.tgz", - "integrity": "sha512-Nr/N5kR0xEzibtXei25E8LX9ThYsAN+Wob9jGZ1MSkMzWfxSo1fQwHc/BumE11bMMKEzn7jG5nT+kGlzAaAb2Q==", + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@cantoo/pdf-lib/-/pdf-lib-2.6.5.tgz", + "integrity": "sha512-3eMHEaqKHt/G/q+6QjT06A3lz0S/a8x3+myiSN7FNeL3uWcedO0lpfs6TWofa4C03Z1wz3tWeHoa4CsI7DrTSA==", "license": "MIT", "dependencies": { "@pdf-lib/standard-fonts": "^1.0.0", @@ -484,9 +486,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.28.tgz", - "integrity": "sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", "dev": true, "funding": [ { @@ -498,7 +500,15 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0" + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } }, "node_modules/@csstools/css-tokenizer": { "version": "4.0.0", @@ -574,13 +584,13 @@ } }, "node_modules/@embedpdf/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-2.8.0.tgz", - "integrity": "sha512-ui0HR4fl7ndiGPw40kMBxXCO9gZHctV1u3Q+/XTd34ONYJ+Pa2LoWNVW2IuPDK7PgKzABPT2axntowlVLPP10g==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-2.9.1.tgz", + "integrity": "sha512-DlFV2o+tv9S+j4TeBVkRaIjjE9o3Tq3+hvJNoIOFtl87cR77UVQqEIRqOf61yk85Y+T2LfmnVPWjNcMuiKUh8w==", "license": "MIT", "dependencies": { - "@embedpdf/engines": "2.8.0", - "@embedpdf/models": "2.8.0" + "@embedpdf/engines": "2.9.1", + "@embedpdf/models": "2.9.1" }, "peerDependencies": { "preact": "^10.26.4", @@ -591,9 +601,9 @@ } }, "node_modules/@embedpdf/engines": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-2.8.0.tgz", - "integrity": "sha512-s749nppKxOcgvFraySKrwtiCt2VMXFe8TFuZUV5R7z8TtMagt6o5NOk6VsdvIpggUYxIsiKhLkFvAqvkNgcjng==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-2.9.1.tgz", + "integrity": "sha512-zyUdKgM2BZVzkqkbIMiTPAKIdH4M3bRV91P1nXUJUU94AmL9DrDrMkf2Nv6+S0bxGt19OpqDlclGSRZ8txhbGw==", "license": "MIT", "dependencies": { "@embedpdf/fonts-arabic": "1.0.0", @@ -603,8 +613,8 @@ "@embedpdf/fonts-latin": "1.0.0", "@embedpdf/fonts-sc": "1.0.0", "@embedpdf/fonts-tc": "1.0.0", - "@embedpdf/models": "2.8.0", - "@embedpdf/pdfium": "2.8.0" + "@embedpdf/models": "2.9.1", + "@embedpdf/pdfium": "2.9.1" }, "peerDependencies": { "preact": "^10.26.4", @@ -657,31 +667,31 @@ "license": "OFL-1.1" }, "node_modules/@embedpdf/models": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-2.8.0.tgz", - "integrity": "sha512-kk3Fm8exMmEX9Ce7VQePybmo04NQGdpsO3FsX1YOQqHpLVBk7tiTeOdetjBqI+YhQ2zWLa2naNKSOSGGzYLyxA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-2.9.1.tgz", + "integrity": "sha512-hUKj30D+a9dDOQlbqbrpjaECDPIcw/526Vo/s+eqJBY8zDNUkZ6meX+aVUrKg8+ApBm2dcEdVo3ff7KgNRhUGw==", "license": "MIT" }, "node_modules/@embedpdf/pdfium": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-2.8.0.tgz", - "integrity": "sha512-RlNLRNboF1Y6fNDy4sJ/a/FEYxATZyeM+n25r3KZJjG+RaM6bxBWXvWlFlGBU5Vx2eqQ5AzDAmIE9cn1agFmqA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-2.9.1.tgz", + "integrity": "sha512-GUu1rDF3XP8X7UpQNnOCvc/jAX/Tw0NoUpOF0aEksVZ6CoujBxtu84P3kWAxcJl6cicyIJq1GAp9szHBSYrTrQ==", "license": "MIT" }, "node_modules/@embedpdf/plugin-annotation": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-2.8.0.tgz", - "integrity": "sha512-h31dT0pvQjFSwsBLytL4BBLf3WDdz9kmAYNKR10filikge7MpgTzgVYFD0C6AOyy2qK1Y/vqworCyh+emVD5aA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-2.9.1.tgz", + "integrity": "sha512-aNtXjI3NUwz7kdmWsQIWzuS1QdZmuHXGCc+Kwl9u5O0PAgoj74OLsgoNEcFzz9m1rljyq3WPVnLczO6ByiifpQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0", - "@embedpdf/utils": "2.8.0" + "@embedpdf/models": "2.9.1", + "@embedpdf/utils": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-history": "2.8.0", - "@embedpdf/plugin-interaction-manager": "2.8.0", - "@embedpdf/plugin-selection": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-history": "2.9.1", + "@embedpdf/plugin-interaction-manager": "2.9.1", + "@embedpdf/plugin-selection": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -690,15 +700,15 @@ } }, "node_modules/@embedpdf/plugin-attachment": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-attachment/-/plugin-attachment-2.8.0.tgz", - "integrity": "sha512-g2jCwjhQsij9zz2JOxZJkIeLTAUxiBKsFh6K4hcsG45ougw/mI3WCw1f+bZlAPkZuOWPo2/nthsHb+wAlrcykQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-attachment/-/plugin-attachment-2.9.1.tgz", + "integrity": "sha512-7fPPHLWHZE9SXZRPnibo6o/AkuiknJQckdNgO16g9EdbPbO6IDNg/8e8pyjK4Knc+ckyyF4gnJ1lemWswSELUw==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -707,15 +717,15 @@ } }, "node_modules/@embedpdf/plugin-bookmark": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-bookmark/-/plugin-bookmark-2.8.0.tgz", - "integrity": "sha512-ab49e17amEshweobU2GbtDEuRDHj89vRYiahkRq9nU1ACI16JIyh4t4fE/m/ucL9YhMMXldjwdtTGHceV566Jg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-bookmark/-/plugin-bookmark-2.9.1.tgz", + "integrity": "sha512-5qgz4yBFEi6h1cogvn9q9CBObGdNLVfkX809kVNPHuRIYsghEieqYkdoh1BNwdP/bm9+D7a6pcXZrVvbXs5e5w==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -724,15 +734,15 @@ } }, "node_modules/@embedpdf/plugin-document-manager": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-document-manager/-/plugin-document-manager-2.8.0.tgz", - "integrity": "sha512-SS+IKJ2+rk4dHM3PvQQrvnfNdb5oOF1INR5r2w0MKo22C9WPD8Ncg1Jak/mCrGbSrJemW3pkIrKZhoREQtSrbA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-document-manager/-/plugin-document-manager-2.9.1.tgz", + "integrity": "sha512-TrqkZvPIQxcWikL9Fmm8/qAJvI+uG7ahPXuyGyyF0JxkrrHCkUMCL8SPJN/fntcPnLJRIi3/K/xR5Qnt/L0aAg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -741,15 +751,15 @@ } }, "node_modules/@embedpdf/plugin-export": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-2.8.0.tgz", - "integrity": "sha512-AvqfyhB58HLoZMKyXLLT+1ebE7wrMEnIAjr+OqEuaNnekxvb8atD8EBByzgtXLiNLStlxFn5g1CKyQThpw4Pcw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-2.9.1.tgz", + "integrity": "sha512-oYl8H0km1m75SYz17mVwxm9ljZvW0yBV8sTcyCa7YhnqE7yxtczu7jw3BrU3KW4Sua9mr9izhs3AixvWkTASQg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -758,15 +768,15 @@ } }, "node_modules/@embedpdf/plugin-history": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-2.8.0.tgz", - "integrity": "sha512-S6TO7DqMqVtBYsztgvPvq8BOJTMl8rWdGjVMuoxD93HZdSgogoculwCATrJGor/BC+X6Vmtaqg6NJWSdIAeBEQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-2.9.1.tgz", + "integrity": "sha512-3AcvSTT7fmqe1ve/FvR3lJ5q7t5JYmnnAg8LKc9ATsDjS9J5b0WE03Omz9a8/sL19iKq8xeR1+W28phgvlcKNw==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -775,15 +785,15 @@ } }, "node_modules/@embedpdf/plugin-interaction-manager": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-2.8.0.tgz", - "integrity": "sha512-xdRTAp1YiXWm+3WVqIN8dkRT3I/dHTumLJy5Kvt7lc1W2XM3M5bfCk5eTTcjY1DPv2buyt44i/4XNWoXAgBXDg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-2.9.1.tgz", + "integrity": "sha512-/wpdStr1NeyMCvAEMVSCPC0a3zaMd+TSK4u8INsIo3b1RoFfb9iTlBB+qW/aaxvZJ/C7MChQ7cLX6VSKXK/6JQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -792,17 +802,17 @@ } }, "node_modules/@embedpdf/plugin-pan": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-2.8.0.tgz", - "integrity": "sha512-EFqTEHk9E7AMRwguRO24jRl0J/5+pG07wlAm5U2rB0LDk30oAqVklFuDygaALdi8ZRBdkWOfD3Wl3gurQvfwAA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-2.9.1.tgz", + "integrity": "sha512-LH5fp/2xKWuEYb5cc5jNWdXkhgOL+8TEf4oLcgao6hK33aV1xry+HAg5ATy+OPpIJItCwjU/Lk7Mc7uibYcFlA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-interaction-manager": "2.8.0", - "@embedpdf/plugin-viewport": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-interaction-manager": "2.9.1", + "@embedpdf/plugin-viewport": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -811,15 +821,15 @@ } }, "node_modules/@embedpdf/plugin-print": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-print/-/plugin-print-2.8.0.tgz", - "integrity": "sha512-PUdw2/1GwbewYrxVgsb+3lkYyYYcLU5qpazrBZjjx0v+SEpb2SMQOOPNUILeJGKbYUlxSz3qv/loFwqzALkUQA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-print/-/plugin-print-2.9.1.tgz", + "integrity": "sha512-brTn0R8AVyAfpm0SYxz/8bg0e373n4KaeIgw5E51DSrczVr3zIgqYGSvXFbfbv57ZUp63Hb93sVorP1et2AeUw==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=18.0.0", "react-dom": ">=18.0.0", @@ -828,20 +838,20 @@ } }, "node_modules/@embedpdf/plugin-redaction": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-2.8.0.tgz", - "integrity": "sha512-pIDFNd9rX7cwrhY6rFCBa5MTnGdLZHX7magToqna/Ffs1KEr0CfNF8jIXG0/E6KB6SsppbHwC7AkwRVWz8NoHg==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-2.9.1.tgz", + "integrity": "sha512-2R81U4ex/JU4IAJ0+G3eIMOwpq80HnAIZ2sI5yMhkDbz+ZZE/sdAP4JKTOFIbhTkwk7DmC222yW34dQJoX4tRQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0", - "@embedpdf/utils": "2.8.0" + "@embedpdf/models": "2.9.1", + "@embedpdf/utils": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-annotation": "2.8.0", - "@embedpdf/plugin-history": "2.8.0", - "@embedpdf/plugin-interaction-manager": "2.8.0", - "@embedpdf/plugin-selection": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-annotation": "2.9.1", + "@embedpdf/plugin-history": "2.9.1", + "@embedpdf/plugin-interaction-manager": "2.9.1", + "@embedpdf/plugin-selection": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -850,15 +860,15 @@ } }, "node_modules/@embedpdf/plugin-render": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-2.8.0.tgz", - "integrity": "sha512-jVGSuyg366LmFzbDpqszLbu3G6VOfv1u46D1C0ph6pL3jisTlRswRosaXS4eVE/fAYTryBrI0olCpVYYct4bQw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-2.9.1.tgz", + "integrity": "sha512-mtfu6uDxlz3+j0xPXfKyvuu8iCFjapPkbnx8vGQ0z2PBNAMm+05hsNIzxJSGMP2VCFo09SOz2zCs7ch9J6NeNg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -867,15 +877,15 @@ } }, "node_modules/@embedpdf/plugin-rotate": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-2.8.0.tgz", - "integrity": "sha512-lWATWEwhkBW77dJaKXlSJbqEMLTSy1lOhn0kDAZ5eyPZRo+7UWe7LHuT5HWzzRlbY9rpvnyixCT6Zp9rKT+WnQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-2.9.1.tgz", + "integrity": "sha512-OgfMI2IsSPHKs4A0DGpmHpxuhoBcZjvw+tz+CXGBi6ILL6oS7z5wXhoMZfRgCgkvjII8RnmjBvmLeahoSaepAA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -884,16 +894,16 @@ } }, "node_modules/@embedpdf/plugin-scroll": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-2.8.0.tgz", - "integrity": "sha512-l6hFH6lsAI+07ZGuOwbC8qcRNdYzWSIfRszjt9UmhKZbvXLEp6YJeS/XOUC/37Kqi30tsmLDyZhMW1AooGAr1A==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-2.9.1.tgz", + "integrity": "sha512-+U3PSIUuNlIOTXzRhnPBP+Rx20sFOd3OPiowyI2EP/Kx/j5R/amgL/t2rjrpw9gjXEMEGsli9Fn4UqnVgMrPaQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-viewport": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-viewport": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -902,15 +912,15 @@ } }, "node_modules/@embedpdf/plugin-search": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-2.8.0.tgz", - "integrity": "sha512-HZ7munHdAF2pJ1cT02yc5I0Xcr4b4CQ8GkhD4qhTpZK0yga/tQiDY2Jjstygv7XiZwHIDKvqOuQsQmOXDsEemw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-2.9.1.tgz", + "integrity": "sha512-NefZgXPfj1MW8i5bYEfuphpXXAODutJTtwCHpLff1YwPw6liuBug9G1lDKSpNddvp+7aSkTcXV5LYNhcPxb1vg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -919,17 +929,17 @@ } }, "node_modules/@embedpdf/plugin-selection": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-2.8.0.tgz", - "integrity": "sha512-DcPyOp2WKoVYVpbZIP5t+JsEmCL9Y7bxe8PmiEBtlm1lustuUezdWyA2G4wsGCR4I/5uNlY85qBGCBhL195sSA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-2.9.1.tgz", + "integrity": "sha512-dVLjiLGnZDo0xO7lZulLGl3cJ/mO7BcA3PGO2uMdhqSWK4tAF/DrakvwXdD581VBwXD/C25EJhxiNa2L7mU4wg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0", - "@embedpdf/utils": "2.8.0" + "@embedpdf/models": "2.9.1", + "@embedpdf/utils": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-interaction-manager": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-interaction-manager": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -938,15 +948,15 @@ } }, "node_modules/@embedpdf/plugin-spread": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-2.8.0.tgz", - "integrity": "sha512-0Ld5HERaG8cKyWW7ktojG3FK6angkzYnBnw7Rnqf4cQuNdO7nKSTslyxkrLzF3vvmKrB//2ghm/pRVkgci4BOw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-2.9.1.tgz", + "integrity": "sha512-s9J4tvoucNac8pUAHVhv3PWDWMZJyK0ikaG78VdGwh4G2iMo5HW1LKDJkJ29l1TjAuxhIoKihpfQbLgUgi7JHA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -955,16 +965,16 @@ } }, "node_modules/@embedpdf/plugin-thumbnail": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-2.8.0.tgz", - "integrity": "sha512-Dq/Cqsn4GClRXNWerqlXNH8WDWK/TtHxwq6yr2kqgXPo1FyREto4I7GfdO1hygTu0Dl6HK2auEVHBVC8QT53fw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-2.9.1.tgz", + "integrity": "sha512-TAEUkVxvvB5kh2VVGf9RMuJFq++CQgWpKJ62ed2GhB9WvVKP5gBYj7G5ff90TLCrAV8iXZL9ENKcHgGKxrZrYw==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-render": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-render": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -973,18 +983,18 @@ } }, "node_modules/@embedpdf/plugin-tiling": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-2.8.0.tgz", - "integrity": "sha512-gPe5mG6hyyruki1eSuQYD2KDbo0Z0TxzSk8TcHIdEYYF6NlI2OghuO8Vczz5WLU8clX8xzn8c/c+tQMHEXfbZA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-2.9.1.tgz", + "integrity": "sha512-UQ/gr/Rdzj7sMsgvtuClw6Jq25fMr+OEdW4tQmg2bV/MqpKptpOEUZ5Aiay4M03X3cgoxhS8UvobRMnjlpYljA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-render": "2.8.0", - "@embedpdf/plugin-scroll": "2.8.0", - "@embedpdf/plugin-viewport": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-render": "2.9.1", + "@embedpdf/plugin-scroll": "2.9.1", + "@embedpdf/plugin-viewport": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -993,15 +1003,15 @@ } }, "node_modules/@embedpdf/plugin-viewport": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-2.8.0.tgz", - "integrity": "sha512-E16hc4yPA54XQGHp0Dy3OYyE8ilBaJE7LJirVmha4kMkP7XBu6xHNOJrXtq4GsZmdLQkf+x8ie2DDbWS+tcwnw==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-2.9.1.tgz", + "integrity": "sha512-bVhBuZHTppKV+OB5lBLqXQv+5oW1A7kAIc5UzsImBwl6NpwH+2PdVkelfrF37yEqnEF/mdxobriWSP0aOVl93w==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", + "@embedpdf/core": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -1010,17 +1020,17 @@ } }, "node_modules/@embedpdf/plugin-zoom": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-2.8.0.tgz", - "integrity": "sha512-HepSQ7NFYhMsQsgfnC/G3b3LOYtNCWkCME0C0jBLrEYsgqsWozf97jIgzfOAeJsugyjQmojpgLUNt3xE9CtG6Q==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-2.9.1.tgz", + "integrity": "sha512-FVOYj+AKTc2aCPrZHOrgbiYNTfBmiwVpFCedmqeGLYOIYTIeGzwJlyIHLU0PmIC/nuHcu1ZFwh0tH23fWAfVCQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "2.8.0" + "@embedpdf/models": "2.9.1" }, "peerDependencies": { - "@embedpdf/core": "2.8.0", - "@embedpdf/plugin-scroll": "2.8.0", - "@embedpdf/plugin-viewport": "2.8.0", + "@embedpdf/core": "2.9.1", + "@embedpdf/plugin-scroll": "2.9.1", + "@embedpdf/plugin-viewport": "2.9.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -1029,9 +1039,9 @@ } }, "node_modules/@embedpdf/utils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-2.8.0.tgz", - "integrity": "sha512-mt3DiQ8pnPk95q0zv7dXfN+y5fzJT2WtXyST8ziYEgmfhz0l2HT/MHCAwLzB3Whlnd2BdHThLfJyG1UD6pC84g==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-2.9.1.tgz", + "integrity": "sha512-IKe/k5DruzpuyGJBIoLEL9AKJ9rEBDLgi1eSCWQPyYkuLWTJg4rBzyGh57eS4S4K4dzQrlJ3Z9zGAW1mgbY2Jw==", "license": "MIT", "peerDependencies": { "preact": "^10.26.4", @@ -1188,9 +1198,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", "cpu": [ "ppc64" ], @@ -1205,9 +1215,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", "cpu": [ "arm" ], @@ -1222,9 +1232,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", "cpu": [ "arm64" ], @@ -1239,9 +1249,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", "cpu": [ "x64" ], @@ -1256,9 +1266,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", "cpu": [ "arm64" ], @@ -1273,9 +1283,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", "cpu": [ "x64" ], @@ -1290,9 +1300,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", "cpu": [ "arm64" ], @@ -1307,9 +1317,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", "cpu": [ "x64" ], @@ -1324,9 +1334,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", "cpu": [ "arm" ], @@ -1341,9 +1351,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", "cpu": [ "arm64" ], @@ -1358,9 +1368,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", "cpu": [ "ia32" ], @@ -1375,9 +1385,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", "cpu": [ "loong64" ], @@ -1392,9 +1402,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", "cpu": [ "mips64el" ], @@ -1409,9 +1419,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", "cpu": [ "ppc64" ], @@ -1426,9 +1436,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", "cpu": [ "riscv64" ], @@ -1443,9 +1453,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", "cpu": [ "s390x" ], @@ -1460,9 +1470,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", "cpu": [ "x64" ], @@ -1477,9 +1487,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", "cpu": [ "arm64" ], @@ -1494,9 +1504,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", "cpu": [ "x64" ], @@ -1511,9 +1521,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", "cpu": [ "arm64" ], @@ -1528,9 +1538,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", "cpu": [ "x64" ], @@ -1545,9 +1555,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", "cpu": [ "arm64" ], @@ -1562,9 +1572,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", "cpu": [ "x64" ], @@ -1579,9 +1589,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", "cpu": [ "arm64" ], @@ -1596,9 +1606,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", "cpu": [ "ia32" ], @@ -1613,9 +1623,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", "cpu": [ "x64" ], @@ -1659,37 +1669,37 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz", - "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.2", + "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", - "minimatch": "^10.2.1" + "minimatch": "^10.2.4" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", - "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.0" + "@eslint/core": "^1.1.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", - "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1721,9 +1731,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", - "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1731,13 +1741,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", - "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.0", + "@eslint/core": "^1.1.1", "levn": "^0.4.1" }, "engines": { @@ -1745,9 +1755,9 @@ } }, "node_modules/@exodus/bytes": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", - "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", "dev": true, "license": "MIT", "engines": { @@ -1763,32 +1773,32 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", - "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", - "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.4", - "@floating-ui/utils": "^0.2.10" + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/react": { - "version": "0.27.18", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.18.tgz", - "integrity": "sha512-xJWJxvmy3a05j643gQt+pRbht5XnTlGpsEsAPnMi5F5YTOEEJymA90uZKBD8OvIv5XvZ1qi4GcccSlqT3Bq44Q==", + "version": "0.27.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz", + "integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==", "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.1.7", - "@floating-ui/utils": "^0.2.10", + "@floating-ui/react-dom": "^2.1.8", + "@floating-ui/utils": "^0.2.11", "tabbable": "^6.0.0" }, "peerDependencies": { @@ -1797,12 +1807,12 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", - "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.5" + "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", @@ -1810,9 +1820,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, "node_modules/@humanfs/core": { @@ -1868,9 +1878,9 @@ } }, "node_modules/@iconify-json/material-symbols": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.58.tgz", - "integrity": "sha512-yPDXwGFNZ4Fq6O8NGbMGP7N4lVk8uX+oMwF3rIb6WRv6lID1W+pd9GN/KiM20rxZR36FjrG6TI5+x2LKLHdOnA==", + "version": "1.2.63", + "resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.63.tgz", + "integrity": "sha512-R4PS/l8K6j+dk2P2MoYFLJgfbZ4YDo6XjCOpX6b1tvX+BhiSpSOjc1b6cnb/mvWe+JWBKlt4pcvPNiAijFLPnA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1996,9 +2006,9 @@ "license": "MIT" }, "node_modules/@mantine/core": { - "version": "8.3.15", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.15.tgz", - "integrity": "sha512-wBn/GogB4x7a2Uj7Ztt3amRaApjED+9XqfE4wyCLh88R7KV55k9vnTdCx+irI/GLOOu9tXNUGm3a4t5sTajwkQ==", + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.18.tgz", + "integrity": "sha512-9tph1lTVogKPjTx02eUxDUOdXacPzK62UuSqb4TdGliI54/Xgxftq0Dfqu6XuhCxn9J5MDJaNiLDvL/1KRkYqA==", "license": "MIT", "dependencies": { "@floating-ui/react": "^0.27.16", @@ -2009,55 +2019,55 @@ "type-fest": "^4.41.0" }, "peerDependencies": { - "@mantine/hooks": "8.3.15", + "@mantine/hooks": "8.3.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/dates": { - "version": "8.3.15", - "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-8.3.15.tgz", - "integrity": "sha512-4WlGHCOAE4in88rQFNlPVl14e7WFWb+YBqxmx4rvAXLj9xLgUxYJO44fva1eIOwNPlTqwbx+GgsEr/HwlcmDMg==", + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-8.3.18.tgz", + "integrity": "sha512-FHx5teJOhupI0gO2o5evtVYQEdqOjayOkLRhEQfB5Nc5DvcysfPfmNILGkc1Nrp9ZQeQWKLT9qr+CkcCXwHOaw==", "license": "MIT", "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { - "@mantine/core": "8.3.15", - "@mantine/hooks": "8.3.15", + "@mantine/core": "8.3.18", + "@mantine/hooks": "8.3.18", "dayjs": ">=1.0.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/dropzone": { - "version": "8.3.15", - "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-8.3.15.tgz", - "integrity": "sha512-12bx1msHULi4D2/VV2PHTBBSshjax/ogLZEIAewX4tK0vRN3OKtA0qR+lqKhywUW4KYv4Z9Dr6O1LoGKHntrUA==", + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-8.3.18.tgz", + "integrity": "sha512-GaYUUl/382R7hl1g6heTCZ5a6T5x6qYPg0oID6ik/J0j7e5+XMZyTH5ITpaqpsBQ09GKKsF5y3iNehpSby8Kew==", "license": "MIT", "dependencies": { "react-dropzone": "15.0.0" }, "peerDependencies": { - "@mantine/core": "8.3.15", - "@mantine/hooks": "8.3.15", + "@mantine/core": "8.3.18", + "@mantine/hooks": "8.3.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/hooks": { - "version": "8.3.15", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.15.tgz", - "integrity": "sha512-AUSnpUlzttHzJht3CJ1YWi16iy6NWRwtyWO5RLGHHsmiW05DyG0qOPKF8+R5dLHuOCnl3XOu4roI2Y1ku9U04Q==", + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.18.tgz", + "integrity": "sha512-QoWr9+S8gg5050TQ06aTSxtlpGjYOpIllRbjYYXlRvZeTsUqiTbVfvQROLexu4rEaK+yy9Wwriwl9PMRgbLqPw==", "license": "MIT", "peerDependencies": { "react": "^18.x || ^19.x" } }, "node_modules/@maxim_mazurok/gapi.client.discovery-v1": { - "version": "0.4.20200806", - "resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.discovery-v1/-/gapi.client.discovery-v1-0.4.20200806.tgz", - "integrity": "sha512-Jeo/KZqK39DI6ExXHcJ4lqnn1O/wEqboQ6eQ8WnNpu5eJ7wUnX/C5KazOgs1aRhnIB/dVzDe8wm62nmtkMIoaw==", + "version": "0.5.20200806", + "resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.discovery-v1/-/gapi.client.discovery-v1-0.5.20200806.tgz", + "integrity": "sha512-oVq9hnnI5VhAtsx55iJbPz8NRfJtWFpI1kINKeuygzCvsx90b1GQDeN3MDUvhADXiQ7+Izs316cqBnJjoDxCow==", "dev": true, "license": "MIT", "dependencies": { @@ -2066,9 +2076,9 @@ } }, "node_modules/@maxim_mazurok/gapi.client.drive-v3": { - "version": "0.1.20260220", - "resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.drive-v3/-/gapi.client.drive-v3-0.1.20260220.tgz", - "integrity": "sha512-ySN46cAYsMw6IiZ7a3eKeUqyH++eL4sPIFlgwu33l0mJHLevK4Qd5VxJOgMS8nBp44xKssCCBLRuRq091rp1WA==", + "version": "0.2.20260311", + "resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.drive-v3/-/gapi.client.drive-v3-0.2.20260311.tgz", + "integrity": "sha512-2SVn8bIFZB9pq1JjqBNY/Agebv8gjHdr5k+ippSVsz07eWpl7d0Vxj4huX1O60ev0NDqrIx6a7w6MFFUIKlN6w==", "dev": true, "license": "MIT", "dependencies": { @@ -2086,9 +2096,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.8.tgz", - "integrity": "sha512-s9UHZo7QJVly7gNArEZkbbsimHqJZhElgBpXIJdehZ4OWXt+CCr0SBDgUCDJnQrqpd1dWK2dLq5rmO4mCBmI3w==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.9.tgz", + "integrity": "sha512-MOkOCTfbMJwLshlBCKJ59V2F/uaLYfmKnN76kksj6jlGUVdI25A9Hzs08m+zjBRdLv+sK7Rqdsefe8X7h/6PCw==", "license": "MIT", "funding": { "type": "opencollective", @@ -2096,9 +2106,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.8.tgz", - "integrity": "sha512-88sWg/UJc1X82OMO+ISR4E3P58I3BjFVg0qkmDu7OWlN8VijneZD3ylFA+ImxuPjMHW3SHosfSJYy1fztoz0fw==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.9.tgz", + "integrity": "sha512-BT+zPJXss8Hg/oEMRmHl17Q97bPACG4ufFSfGEdhiE96jOyR5Dz1ty7ZWt1fVGR0y1p+sSgEwQT/MNZQmoWDCw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6" @@ -2111,7 +2121,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^7.3.8", + "@mui/material": "^7.3.9", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -2122,16 +2132,16 @@ } }, "node_modules/@mui/material": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.8.tgz", - "integrity": "sha512-QKd1RhDXE1hf2sQDNayA9ic9jGkEgvZOf0tTkJxlBPG8ns8aS4rS8WwYURw2x5y3739p0HauUXX9WbH7UufFLw==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.9.tgz", + "integrity": "sha512-I8yO3t4T0y7bvDiR1qhIN6iBWZOTBfVOnmLlM7K6h3dx5YX2a7rnkuXzc2UkZaqhxY9NgTnEbdPlokR1RxCNRQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", - "@mui/core-downloads-tracker": "^7.3.8", - "@mui/system": "^7.3.8", - "@mui/types": "^7.4.11", - "@mui/utils": "^7.3.8", + "@mui/core-downloads-tracker": "^7.3.9", + "@mui/system": "^7.3.9", + "@mui/types": "^7.4.12", + "@mui/utils": "^7.3.9", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", @@ -2150,7 +2160,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^7.3.8", + "@mui/material-pigment-css": "^7.3.9", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -2171,13 +2181,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.8.tgz", - "integrity": "sha512-du5dlPZ9XL3xW2apHoGDXBI+QLtyVJGrXNCfcNYfP/ojkz1RQ0rRV6VG9Rkm1DqEFRG8mjjTL7zmE1Bvn1eR4A==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.9.tgz", + "integrity": "sha512-ErIyRQvsiQEq7Yvcvfw9UDHngaqjMy9P3JDPnRAaKG5qhpl2C4tX/W1S4zJvpu+feihmZJStjIyvnv6KDbIrlw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", - "@mui/utils": "^7.3.8", + "@mui/utils": "^7.3.9", "prop-types": "^15.8.1" }, "engines": { @@ -2198,9 +2208,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.8.tgz", - "integrity": "sha512-JHAeXQzS0tJ+Fq3C6J4TVDsW+yKhO4uuxuiLaopNStJeQYBIUCXpKYyUCcgXym4AmhbznQnv9RlHywSH6b0FOg==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.9.tgz", + "integrity": "sha512-JqujWt5bX4okjUPGpVof/7pvgClqh7HvIbsIBIOOlCh2u3wG/Bwp4+E1bc1dXSwkrkp9WUAoNdI5HEC+5HKvMw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", @@ -2232,16 +2242,16 @@ } }, "node_modules/@mui/system": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.8.tgz", - "integrity": "sha512-hoFRj4Zw2Km8DPWZp/nKG+ao5Jw5LSk2m/e4EGc6M3RRwXKEkMSG4TgtfVJg7dS2homRwtdXSMW+iRO0ZJ4+IA==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.9.tgz", + "integrity": "sha512-aL1q9am8XpRrSabv9qWf5RHhJICJql34wnrc1nz0MuOglPRYF/liN+c8VqZdTvUn9qg+ZjRVbKf4sJVFfIDtmg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", - "@mui/private-theming": "^7.3.8", - "@mui/styled-engine": "^7.3.8", - "@mui/types": "^7.4.11", - "@mui/utils": "^7.3.8", + "@mui/private-theming": "^7.3.9", + "@mui/styled-engine": "^7.3.9", + "@mui/types": "^7.4.12", + "@mui/utils": "^7.3.9", "clsx": "^2.1.1", "csstype": "^3.2.3", "prop-types": "^15.8.1" @@ -2272,9 +2282,9 @@ } }, "node_modules/@mui/types": { - "version": "7.4.11", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.11.tgz", - "integrity": "sha512-fZ2xO9D08IKOxO2oUBi1nnVKH6oJUD+64cnv4YAaFoC0E5+i1+S5AHbNqqvZlYYsbPEQ6qEVwuBqY3jl5W4G+Q==", + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz", + "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6" @@ -2289,13 +2299,13 @@ } }, "node_modules/@mui/utils": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.8.tgz", - "integrity": "sha512-kZRcE2620CBGr+XI8YMmwPj6WIPwSF7uMJjvSfqd8zXVvlz0MCJbzRRUGNf8NgflCLthdji2DdS643TeyJ3+nA==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.9.tgz", + "integrity": "sha512-U6SdZaGbfb65fqTsH3V5oJdFj9uYwyLE2WVuNvmbggTSDBb8QHrFsqY8BN3taK9t3yJ8/BPHD/kNvLNyjwM7Yw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", - "@mui/types": "^7.4.11", + "@mui/types": "^7.4.12", "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -2319,9 +2329,9 @@ } }, "node_modules/@napi-rs/canvas": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.95.tgz", - "integrity": "sha512-lkg23ge+rgyhgUwXmlbkPEhuhHq/hUi/gXKH+4I7vO+lJrbNfEYcQdJLIGjKyXLQzgFiiyDAwh5vAe/tITAE+w==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.97.tgz", + "integrity": "sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==", "license": "MIT", "optional": true, "workspaces": [ @@ -2335,23 +2345,23 @@ "url": "https://github.com/sponsors/Brooooooklyn" }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.95", - "@napi-rs/canvas-darwin-arm64": "0.1.95", - "@napi-rs/canvas-darwin-x64": "0.1.95", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.95", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.95", - "@napi-rs/canvas-linux-arm64-musl": "0.1.95", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.95", - "@napi-rs/canvas-linux-x64-gnu": "0.1.95", - "@napi-rs/canvas-linux-x64-musl": "0.1.95", - "@napi-rs/canvas-win32-arm64-msvc": "0.1.95", - "@napi-rs/canvas-win32-x64-msvc": "0.1.95" + "@napi-rs/canvas-android-arm64": "0.1.97", + "@napi-rs/canvas-darwin-arm64": "0.1.97", + "@napi-rs/canvas-darwin-x64": "0.1.97", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.97", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.97", + "@napi-rs/canvas-linux-arm64-musl": "0.1.97", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.97", + "@napi-rs/canvas-linux-x64-gnu": "0.1.97", + "@napi-rs/canvas-linux-x64-musl": "0.1.97", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.97", + "@napi-rs/canvas-win32-x64-msvc": "0.1.97" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.95.tgz", - "integrity": "sha512-SqTh0wsYbetckMXEvHqmR7HKRJujVf1sYv1xdlhkifg6TlCSysz1opa49LlS3+xWuazcQcfRfmhA07HxxxGsAA==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.97.tgz", + "integrity": "sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==", "cpu": [ "arm64" ], @@ -2369,9 +2379,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.95.tgz", - "integrity": "sha512-F7jT0Syu+B9DGBUBcMk3qCRIxAWiDXmvEjamwbYfbZl7asI1pmXZUnCOoIu49Wt0RNooToYfRDxU9omD6t5Xuw==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.97.tgz", + "integrity": "sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==", "cpu": [ "arm64" ], @@ -2389,9 +2399,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.95.tgz", - "integrity": "sha512-54eb2Ho15RDjYGXO/harjRznBrAvu+j5nQ85Z4Qd6Qg3slR8/Ja+Yvvy9G4yo7rdX6NR9GPkZeSTf2UcKXwaXw==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.97.tgz", + "integrity": "sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==", "cpu": [ "x64" ], @@ -2409,9 +2419,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.95.tgz", - "integrity": "sha512-hYaLCSLx5bmbnclzQc3ado3PgZ66blJWzjXp0wJmdwpr/kH+Mwhj6vuytJIomgksyJoCdIqIa4N6aiqBGJtJ5Q==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.97.tgz", + "integrity": "sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==", "cpu": [ "arm" ], @@ -2429,9 +2439,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.95.tgz", - "integrity": "sha512-J7VipONahKsmScPZsipHVQBqpbZx4favaD8/enWzzlGcjiwycOoymL7f4tNeqdjK0su19bDOUt6mjp9gsPWYlw==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.97.tgz", + "integrity": "sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==", "cpu": [ "arm64" ], @@ -2449,9 +2459,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.95.tgz", - "integrity": "sha512-PXy0UT1J/8MPG8UAkWp6Fd51ZtIZINFzIjGH909JjQrtCuJf3X6nanHYdz1A+Wq9o4aoPAw1YEUpFS1lelsVlg==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.97.tgz", + "integrity": "sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==", "cpu": [ "arm64" ], @@ -2469,9 +2479,9 @@ } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.95.tgz", - "integrity": "sha512-2IzCkW2RHRdcgF9W5/plHvYFpc6uikyjMb5SxjqmNxfyDFz9/HB89yhi8YQo0SNqrGRI7yBVDec7Pt+uMyRWsg==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.97.tgz", + "integrity": "sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==", "cpu": [ "riscv64" ], @@ -2489,9 +2499,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.95.tgz", - "integrity": "sha512-OV/ol/OtcUr4qDhQg8G7SdViZX8XyQeKpPsVv/j3+7U178FGoU4M+yIocdVo1ih/A8GQ63+LjF4jDoEjaVU8Pw==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.97.tgz", + "integrity": "sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==", "cpu": [ "x64" ], @@ -2509,9 +2519,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.95.tgz", - "integrity": "sha512-Z5KzqBK/XzPz5+SFHKz7yKqClEQ8pOiEDdgk5SlphBLVNb8JFIJkxhtJKSvnJyHh2rjVgiFmvtJzMF0gNwwKyQ==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.97.tgz", + "integrity": "sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==", "cpu": [ "x64" ], @@ -2529,9 +2539,9 @@ } }, "node_modules/@napi-rs/canvas-win32-arm64-msvc": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.95.tgz", - "integrity": "sha512-aj0YbRpe8qVJ4OzMsK7NfNQePgcf9zkGFzNZ9mSuaxXzhpLHmlF2GivNdCdNOg8WzA/NxV6IU4c5XkXadUMLeA==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.97.tgz", + "integrity": "sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==", "cpu": [ "arm64" ], @@ -2549,9 +2559,9 @@ } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.95", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.95.tgz", - "integrity": "sha512-GA8leTTCfdjuHi8reICTIxU0081PhXvl3lzIniLUjeLACx9GubUiyzkwFb+oyeKLS5IAGZFLKnzAf4wm2epRlA==", + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.97.tgz", + "integrity": "sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==", "cpu": [ "x64" ], @@ -2677,12 +2687,12 @@ } }, "node_modules/@opentelemetry/resources": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.1.tgz", - "integrity": "sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.0.tgz", + "integrity": "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.5.1", + "@opentelemetry/core": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -2693,9 +2703,9 @@ } }, "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.1.tgz", - "integrity": "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz", + "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2806,9 +2816,9 @@ } }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz", - "integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", "license": "Apache-2.0", "engines": { "node": ">=14" @@ -2870,9 +2880,9 @@ } }, "node_modules/@posthog/core": { - "version": "1.23.1", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.23.1.tgz", - "integrity": "sha512-GViD5mOv/mcbZcyzz3z9CS0R79JzxVaqEz4sP5Dsea178M/j3ZWe6gaHDZB9yuyGfcmIMQ/8K14yv+7QrK4sQQ==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.24.1.tgz", + "integrity": "sha512-e8AciAnc6MRFws89ux8lJKFAaI03yEon0ASDoUO7yS91FVqbUGXYekObUUR3LHplcg+pmyiJBI0jolY0SFbGRA==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.6" @@ -2895,9 +2905,9 @@ } }, "node_modules/@posthog/types": { - "version": "1.354.0", - "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.354.0.tgz", - "integrity": "sha512-sfH1PiThX1YWkrZSls6zMuZcJWnvboCnZEJ3Z/OI8WgBmLDJfQpficbuLM3tgSLIchI22TPAkpwdT987iW6XIA==", + "version": "1.363.3", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.363.3.tgz", + "integrity": "sha512-Wslj6BrDwIEkqoahJFE0DbqgoGsB/F9BC3XtzBQdUzr04XhVNriGQ7/lves9eCFwrpSiOHv/5xfSShRwiP3ciA==", "license": "MIT" }, "node_modules/@protobufjs/aspromise": { @@ -3074,16 +3084,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", - "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", "cpu": [ "arm" ], @@ -3095,9 +3105,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", "cpu": [ "arm64" ], @@ -3109,9 +3119,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", "cpu": [ "arm64" ], @@ -3123,9 +3133,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", "cpu": [ "x64" ], @@ -3137,9 +3147,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", "cpu": [ "arm64" ], @@ -3151,9 +3161,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", "cpu": [ "x64" ], @@ -3165,9 +3175,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", "cpu": [ "arm" ], @@ -3179,9 +3189,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", "cpu": [ "arm" ], @@ -3193,9 +3203,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", "cpu": [ "arm64" ], @@ -3207,9 +3217,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", "cpu": [ "arm64" ], @@ -3221,9 +3231,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", "cpu": [ "loong64" ], @@ -3235,9 +3245,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", "cpu": [ "loong64" ], @@ -3249,9 +3259,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", "cpu": [ "ppc64" ], @@ -3263,9 +3273,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", "cpu": [ "ppc64" ], @@ -3277,9 +3287,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", "cpu": [ "riscv64" ], @@ -3291,9 +3301,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", "cpu": [ "riscv64" ], @@ -3305,9 +3315,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", "cpu": [ "s390x" ], @@ -3319,9 +3329,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", "cpu": [ "x64" ], @@ -3333,9 +3343,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", "cpu": [ "x64" ], @@ -3347,9 +3357,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", "cpu": [ "x64" ], @@ -3361,9 +3371,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", "cpu": [ "arm64" ], @@ -3375,9 +3385,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", "cpu": [ "arm64" ], @@ -3389,9 +3399,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", "cpu": [ "ia32" ], @@ -3403,9 +3413,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", "cpu": [ "x64" ], @@ -3417,9 +3427,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", "cpu": [ "x64" ], @@ -3493,9 +3503,9 @@ } }, "node_modules/@supabase/auth-js": { - "version": "2.97.0", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.97.0.tgz", - "integrity": "sha512-2Og/1lqp+AIavr8qS2X04aSl8RBY06y4LrtIAGxat06XoXYiDxKNQMQzWDAKm1EyZFZVRNH48DO5YvIZ7la5fQ==", + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.100.0.tgz", + "integrity": "sha512-pdT3ye3UVRN1Cg0wom6BmyY+XTtp5DiJaYnPi6j8ht5i8Lq8kfqxJMJz9GI9YDKk3w1nhGOPnh6Qz5qpyYm+1w==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -3505,9 +3515,9 @@ } }, "node_modules/@supabase/functions-js": { - "version": "2.97.0", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.97.0.tgz", - "integrity": "sha512-fSaA0ZeBUS9hMgpGZt5shIZvfs3Mvx2ZdajQT4kv/whubqDBAp3GU5W8iIXy21MRvKmO2NpAj8/Q6y+ZkZyF/w==", + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.100.0.tgz", + "integrity": "sha512-keLg79RPwP+uiwHuxFPTFgDRxPV46LM4j/swjyR2GKJgWniTVSsgiBHfbIBDcrQwehLepy09b/9QSHUywtKRWQ==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -3516,10 +3526,16 @@ "node": ">=20.0.0" } }, + "node_modules/@supabase/phoenix": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.0.tgz", + "integrity": "sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==", + "license": "MIT" + }, "node_modules/@supabase/postgrest-js": { - "version": "2.97.0", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.97.0.tgz", - "integrity": "sha512-g4Ps0eaxZZurvfv/KGoo2XPZNpyNtjth9aW8eho9LZWM0bUuBtxPZw3ZQ6ERSpEGogshR+XNgwlSPIwcuHCNww==", + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.100.0.tgz", + "integrity": "sha512-xYNvNbBJaXOGcrZ44wxwp5830uo1okMHGS8h8dm3u4f0xcZ39yzbryUsubTJW41MG2gbL/6U57cA4Pi6YMZ9pA==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -3529,12 +3545,12 @@ } }, "node_modules/@supabase/realtime-js": { - "version": "2.97.0", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.97.0.tgz", - "integrity": "sha512-37Jw0NLaFP0CZd7qCan97D1zWutPrTSpgWxAw6Yok59JZoxp4IIKMrPeftJ3LZHmf+ILQOPy3i0pRDHM9FY36Q==", + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.100.0.tgz", + "integrity": "sha512-2AZs00zzEF0HuCKY8grz5eCYlwEfVi5HONLZFoNR6aDfxQivl8zdQYNjyFoqN2MZiVhQHD7u6XV/xHwM8mCEHw==", "license": "MIT", "dependencies": { - "@types/phoenix": "^1.6.6", + "@supabase/phoenix": "^0.4.0", "@types/ws": "^8.18.1", "tslib": "2.8.1", "ws": "^8.18.2" @@ -3544,9 +3560,9 @@ } }, "node_modules/@supabase/storage-js": { - "version": "2.97.0", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.97.0.tgz", - "integrity": "sha512-9f6NniSBfuMxOWKwEFb+RjJzkfMdJUwv9oHuFJKfe/5VJR8cd90qw68m6Hn0ImGtwG37TUO+QHtoOechxRJ1Yg==", + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.100.0.tgz", + "integrity": "sha512-d4EeuK6RNIgYNA2MU9kj8lQrLm5AzZ+WwpWjGkii6SADQNIGTC/uiaTRu02XJ5AmFALQfo8fLl9xuCkO6Xw+iQ==", "license": "MIT", "dependencies": { "iceberg-js": "^0.8.1", @@ -3557,16 +3573,16 @@ } }, "node_modules/@supabase/supabase-js": { - "version": "2.97.0", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.97.0.tgz", - "integrity": "sha512-kTD91rZNO4LvRUHv4x3/4hNmsEd2ofkYhuba2VMUPRVef1RCmnHtm7rIws38Fg0yQnOSZOplQzafn0GSiy6GVg==", + "version": "2.100.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.100.0.tgz", + "integrity": "sha512-r0tlcukejJXJ1m/2eG/Ya5eYs4W8AC7oZfShpG3+SIo/eIU9uIt76ZeYI1SoUwUmcmzlAbgch+HDZDR/toVQPQ==", "license": "MIT", "dependencies": { - "@supabase/auth-js": "2.97.0", - "@supabase/functions-js": "2.97.0", - "@supabase/postgrest-js": "2.97.0", - "@supabase/realtime-js": "2.97.0", - "@supabase/storage-js": "2.97.0" + "@supabase/auth-js": "2.100.0", + "@supabase/functions-js": "2.100.0", + "@supabase/postgrest-js": "2.100.0", + "@supabase/realtime-js": "2.100.0", + "@supabase/storage-js": "2.100.0" }, "engines": { "node": ">=20.0.0" @@ -3583,9 +3599,9 @@ } }, "node_modules/@swc/core": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.13.tgz", - "integrity": "sha512-0l1gl/72PErwUZuavcRpRAQN9uSst+Nk++niC5IX6lmMWpXoScYx3oq/narT64/sKv/eRiPTaAjBFGDEQiWJIw==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.21.tgz", + "integrity": "sha512-fkk7NJcBscrR3/F8jiqlMptRHP650NxqDnspBMrRe5d8xOoCy9MLL5kOBLFXjFLfMo3KQQHhk+/jUULOMlR1uQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -3601,16 +3617,18 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.13", - "@swc/core-darwin-x64": "1.15.13", - "@swc/core-linux-arm-gnueabihf": "1.15.13", - "@swc/core-linux-arm64-gnu": "1.15.13", - "@swc/core-linux-arm64-musl": "1.15.13", - "@swc/core-linux-x64-gnu": "1.15.13", - "@swc/core-linux-x64-musl": "1.15.13", - "@swc/core-win32-arm64-msvc": "1.15.13", - "@swc/core-win32-ia32-msvc": "1.15.13", - "@swc/core-win32-x64-msvc": "1.15.13" + "@swc/core-darwin-arm64": "1.15.21", + "@swc/core-darwin-x64": "1.15.21", + "@swc/core-linux-arm-gnueabihf": "1.15.21", + "@swc/core-linux-arm64-gnu": "1.15.21", + "@swc/core-linux-arm64-musl": "1.15.21", + "@swc/core-linux-ppc64-gnu": "1.15.21", + "@swc/core-linux-s390x-gnu": "1.15.21", + "@swc/core-linux-x64-gnu": "1.15.21", + "@swc/core-linux-x64-musl": "1.15.21", + "@swc/core-win32-arm64-msvc": "1.15.21", + "@swc/core-win32-ia32-msvc": "1.15.21", + "@swc/core-win32-x64-msvc": "1.15.21" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -3622,9 +3640,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.13.tgz", - "integrity": "sha512-ztXusRuC5NV2w+a6pDhX13CGioMLq8CjX5P4XgVJ21ocqz9t19288Do0y8LklplDtwcEhYGTNdMbkmUT7+lDTg==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.21.tgz", + "integrity": "sha512-SA8SFg9dp0qKRH8goWsax6bptFE2EdmPf2YRAQW9WoHGf3XKM1bX0nd5UdwxmC5hXsBUZAYf7xSciCler6/oyA==", "cpu": [ "arm64" ], @@ -3639,9 +3657,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.13.tgz", - "integrity": "sha512-cVifxQUKhaE7qcO/y9Mq6PEhoyvN9tSLzCnnFZ4EIabFHBuLtDDO6a+vLveOy98hAs5Qu1+bb5Nv0oa1Pihe3Q==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.21.tgz", + "integrity": "sha512-//fOVntgowz9+V90lVsNCtyyrtbHp3jWH6Rch7MXHXbcvbLmbCTmssl5DeedUWLLGiAAW1wksBdqdGYOTjaNLw==", "cpu": [ "x64" ], @@ -3656,9 +3674,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.13.tgz", - "integrity": "sha512-t+xxEzZ48enl/wGGy7SRYd7kImWQ/+wvVFD7g5JZo234g6/QnIgZ+YdfIyjHB+ZJI3F7a2IQHS7RNjxF29UkWw==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.21.tgz", + "integrity": "sha512-meNI4Sh6h9h8DvIfEc0l5URabYMSuNvyisLmG6vnoYAS43s8ON3NJR8sDHvdP7NJTrLe0q/x2XCn6yL/BeHcZg==", "cpu": [ "arm" ], @@ -3673,9 +3691,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.13.tgz", - "integrity": "sha512-VndeGvKmTXFn6AGwjy0Kg8i7HccOCE7Jt/vmZwRxGtOfNZM1RLYRQ7MfDLo6T0h1Bq6eYzps3L5Ma4zBmjOnOg==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.21.tgz", + "integrity": "sha512-QrXlNQnHeXqU2EzLlnsPoWEh8/GtNJLvfMiPsDhk+ht6Xv8+vhvZ5YZ/BokNWSIZiWPKLAqR0M7T92YF5tmD3g==", "cpu": [ "arm64" ], @@ -3690,9 +3708,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.13.tgz", - "integrity": "sha512-SmZ9m+XqCB35NddHCctvHFLqPZDAs5j8IgD36GoutufDJmeq2VNfgk5rQoqNqKmAK3Y7iFdEmI76QoHIWiCLyw==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.21.tgz", + "integrity": "sha512-8/yGCMO333ultDaMQivE5CjO6oXDPeeg1IV4sphojPkb0Pv0i6zvcRIkgp60xDB+UxLr6VgHgt+BBgqS959E9g==", "cpu": [ "arm64" ], @@ -3706,10 +3724,44 @@ "node": ">=10" } }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.21.tgz", + "integrity": "sha512-ucW0HzPx0s1dgRvcvuLSPSA/2Kk/VYTv9st8qe1Kc22Gu0Q0rH9+6TcBTmMuNIp0Xs4BPr1uBttmbO1wEGI49Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.21.tgz", + "integrity": "sha512-ulTnOGc5I7YRObE/9NreAhQg94QkiR5qNhhcUZ1iFAYjzg/JGAi1ch+s/Ixe61pMIr8bfVrF0NOaB0f8wjaAfA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.13.tgz", - "integrity": "sha512-5rij+vB9a29aNkHq72EXI2ihDZPszJb4zlApJY4aCC/q6utgqFA6CkrfTfIb+O8hxtG3zP5KERETz8mfFK6A0A==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.21.tgz", + "integrity": "sha512-D0RokxtM+cPvSqJIKR6uja4hbD+scI9ezo95mBhfSyLUs9wnPPl26sLp1ZPR/EXRdYm3F3S6RUtVi+8QXhT24Q==", "cpu": [ "x64" ], @@ -3724,9 +3776,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.13.tgz", - "integrity": "sha512-OlSlaOK9JplQ5qn07WiBLibkOw7iml2++ojEXhhR3rbWrNEKCD7sd8+6wSavsInyFdw4PhLA+Hy6YyDBIE23Yw==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.21.tgz", + "integrity": "sha512-nER8u7VeRfmU6fMDzl1NQAbbB/G7O2avmvCOwIul1uGkZ2/acbPH+DCL9h5+0yd/coNcxMBTL6NGepIew+7C2w==", "cpu": [ "x64" ], @@ -3741,9 +3793,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.13.tgz", - "integrity": "sha512-zwQii5YVdsfG8Ti9gIKgBKZg8qMkRZxl+OlYWUT5D93Jl4NuNBRausP20tfEkQdAPSRrMCSUZBM6FhW7izAZRg==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.21.tgz", + "integrity": "sha512-+/AgNBnjYugUA8C0Do4YzymgvnGbztv7j8HKSQLvR/DQgZPoXQ2B3PqB2mTtGh/X5DhlJWiqnunN35JUgWcAeQ==", "cpu": [ "arm64" ], @@ -3758,9 +3810,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.13.tgz", - "integrity": "sha512-hYXvyVVntqRlYoAIDwNzkS3tL2ijP3rxyWQMNKaxcCxxkCDto/w3meOK/OB6rbQSkNw0qTUcBfU9k+T0ptYdfQ==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.21.tgz", + "integrity": "sha512-IkSZj8PX/N4HcaFhMQtzmkV8YSnuNoJ0E6OvMwFiOfejPhiKXvl7CdDsn1f4/emYEIDO3fpgZW9DTaCRMDxaDA==", "cpu": [ "ia32" ], @@ -3775,9 +3827,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.13.tgz", - "integrity": "sha512-XTzKs7c/vYCcjmcwawnQvlHHNS1naJEAzcBckMI5OJlnrcgW8UtcX9NHFYvNjGtXuKv0/9KvqL4fuahdvlNGKw==", + "version": "1.15.21", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.21.tgz", + "integrity": "sha512-zUyWso7OOENB6e1N1hNuNn8vbvLsTdKQ5WKLgt/JcBNfJhKy/6jmBmqI3GXk/MyvQKd5SLvP7A0F36p7TeDqvw==", "cpu": [ "x64" ], @@ -3809,47 +3861,47 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", - "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", - "lightningcss": "1.31.1", + "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.2.1" + "tailwindcss": "4.2.2" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", - "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.1", - "@tailwindcss/oxide-darwin-arm64": "4.2.1", - "@tailwindcss/oxide-darwin-x64": "4.2.1", - "@tailwindcss/oxide-freebsd-x64": "4.2.1", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", - "@tailwindcss/oxide-linux-x64-musl": "4.2.1", - "@tailwindcss/oxide-wasm32-wasi": "4.2.1", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", - "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", "cpu": [ "arm64" ], @@ -3863,9 +3915,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", - "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", "cpu": [ "arm64" ], @@ -3879,9 +3931,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", - "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", "cpu": [ "x64" ], @@ -3895,9 +3947,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", - "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", "cpu": [ "x64" ], @@ -3911,9 +3963,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", - "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", "cpu": [ "arm" ], @@ -3927,9 +3979,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", - "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", "cpu": [ "arm64" ], @@ -3943,9 +3995,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", - "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", "cpu": [ "arm64" ], @@ -3959,9 +4011,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", - "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", "cpu": [ "x64" ], @@ -3975,9 +4027,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", - "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", "cpu": [ "x64" ], @@ -3991,9 +4043,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", - "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -4020,9 +4072,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", - "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", "cpu": [ "arm64" ], @@ -4036,9 +4088,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", - "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", "cpu": [ "x64" ], @@ -4052,25 +4104,25 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", - "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", + "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.2.1", - "@tailwindcss/oxide": "4.2.1", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", - "tailwindcss": "4.2.1" + "tailwindcss": "4.2.2" } }, "node_modules/@tanstack/react-virtual": { - "version": "3.13.19", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.19.tgz", - "integrity": "sha512-KzwmU1IbE0IvCZSm6OXkS+kRdrgW2c2P3Ho3NC+zZXWK6oObv/L+lcV/2VuJ+snVESRlMJ+w/fg4WXI/JzoNGQ==", + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.23.tgz", + "integrity": "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==", "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.13.19" + "@tanstack/virtual-core": "3.13.23" }, "funding": { "type": "github", @@ -4082,9 +4134,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.13.19", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.19.tgz", - "integrity": "sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g==", + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz", + "integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==", "license": "MIT", "funding": { "type": "github", @@ -4102,9 +4154,9 @@ } }, "node_modules/@tauri-apps/cli": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.0.tgz", - "integrity": "sha512-ZwT0T+7bw4+DPCSWzmviwq5XbXlM0cNoleDKOYPFYqcZqeKY31KlpoMW/MOON/tOFBPgi31a2v3w9gliqwL2+Q==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", + "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", "dev": true, "license": "Apache-2.0 OR MIT", "bin": { @@ -4118,23 +4170,23 @@ "url": "https://opencollective.com/tauri" }, "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.10.0", - "@tauri-apps/cli-darwin-x64": "2.10.0", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.0", - "@tauri-apps/cli-linux-arm64-gnu": "2.10.0", - "@tauri-apps/cli-linux-arm64-musl": "2.10.0", - "@tauri-apps/cli-linux-riscv64-gnu": "2.10.0", - "@tauri-apps/cli-linux-x64-gnu": "2.10.0", - "@tauri-apps/cli-linux-x64-musl": "2.10.0", - "@tauri-apps/cli-win32-arm64-msvc": "2.10.0", - "@tauri-apps/cli-win32-ia32-msvc": "2.10.0", - "@tauri-apps/cli-win32-x64-msvc": "2.10.0" + "@tauri-apps/cli-darwin-arm64": "2.10.1", + "@tauri-apps/cli-darwin-x64": "2.10.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", + "@tauri-apps/cli-linux-arm64-musl": "2.10.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-musl": "2.10.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", + "@tauri-apps/cli-win32-x64-msvc": "2.10.1" } }, "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.0.tgz", - "integrity": "sha512-avqHD4HRjrMamE/7R/kzJPcAJnZs0IIS+1nkDP5b+TNBn3py7N2aIo9LIpy+VQq0AkN8G5dDpZtOOBkmWt/zjA==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", + "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", "cpu": [ "arm64" ], @@ -4149,9 +4201,9 @@ } }, "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.0.tgz", - "integrity": "sha512-keDmlvJRStzVFjZTd0xYkBONLtgBC9eMTpmXnBXzsHuawV2q9PvDo2x6D5mhuoMVrJ9QWjgaPKBBCFks4dK71Q==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", + "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", "cpu": [ "x64" ], @@ -4166,9 +4218,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.0.tgz", - "integrity": "sha512-e5u0VfLZsMAC9iHaOEANumgl6lfnJx0Dtjkd8IJpysZ8jp0tJ6wrIkto2OzQgzcYyRCKgX72aKE0PFgZputA8g==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", + "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", "cpu": [ "arm" ], @@ -4183,9 +4235,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.0.tgz", - "integrity": "sha512-YrYYk2dfmBs5m+OIMCrb+JH/oo+4FtlpcrTCgiFYc7vcs6m3QDd1TTyWu0u01ewsCtK2kOdluhr/zKku+KP7HA==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", + "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", "cpu": [ "arm64" ], @@ -4200,9 +4252,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.0.tgz", - "integrity": "sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", + "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", "cpu": [ "arm64" ], @@ -4217,9 +4269,9 @@ } }, "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.0.tgz", - "integrity": "sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", + "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", "cpu": [ "riscv64" ], @@ -4234,9 +4286,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.0.tgz", - "integrity": "sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", + "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", "cpu": [ "x64" ], @@ -4251,9 +4303,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.0.tgz", - "integrity": "sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", + "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", "cpu": [ "x64" ], @@ -4268,9 +4320,9 @@ } }, "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.0.tgz", - "integrity": "sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", + "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", "cpu": [ "arm64" ], @@ -4285,9 +4337,9 @@ } }, "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.0.tgz", - "integrity": "sha512-EHyQ1iwrWy1CwMalEm9z2a6L5isQ121pe7FcA2xe4VWMJp+GHSDDGvbTv/OPdkt2Lyr7DAZBpZHM6nvlHXEc4A==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", + "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", "cpu": [ "ia32" ], @@ -4302,9 +4354,9 @@ } }, "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.0.tgz", - "integrity": "sha512-NTpyQxkpzGmU6ceWBTY2xRIEaS0ZLbVx1HE1zTA3TY/pV3+cPoPPOs+7YScr4IMzXMtOw7tLw5LEXo5oIG3qaQ==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", + "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", "cpu": [ "x64" ], @@ -4931,9 +4983,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", - "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -4945,12 +4997,6 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, - "node_modules/@types/phoenix": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", - "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", - "license": "MIT" - }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -5018,17 +5064,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", - "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/type-utils": "8.56.1", - "@typescript-eslint/utils": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -5041,22 +5087,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.1", + "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", - "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "engines": { @@ -5072,14 +5118,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", - "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.1", - "@typescript-eslint/types": "^8.56.1", + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "engines": { @@ -5094,14 +5140,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", - "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5112,9 +5158,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", - "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", "dev": true, "license": "MIT", "engines": { @@ -5129,15 +5175,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", - "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -5154,10 +5200,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", - "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", - "dev": true, + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5168,16 +5213,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", - "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.1", - "@typescript-eslint/tsconfig-utils": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -5196,16 +5241,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", - "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5220,13 +5265,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", - "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -5257,20 +5302,20 @@ "license": "MIT" }, "node_modules/@vitejs/plugin-react-swc": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.2.3.tgz", - "integrity": "sha512-QIluDil2prhY1gdA3GGwxZzTAmLdi8cQ2CcuMW4PB/Wu4e/1pzqrwhYWVd09LInCRlDUidQjd0B70QWbjWtLxA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.3.0.tgz", + "integrity": "sha512-mOkXCII839dHyAt/gpoSlm28JIVDwhZ6tnG6wJxUy2bmOx7UaPjvOyIDf3SFv5s7Eo7HVaq6kRcu6YMEzt5Z7w==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.2", + "@rolldown/pluginutils": "1.0.0-rc.7", "@swc/core": "^1.15.11" }, "engines": { "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4 || ^5 || ^6 || ^7" + "vite": "^4 || ^5 || ^6 || ^7 || ^8" } }, "node_modules/@vitest/coverage-v8": { @@ -5423,13 +5468,13 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", - "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", + "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.29", + "@vue/shared": "3.5.30", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" @@ -5454,29 +5499,29 @@ "license": "MIT" }, "node_modules/@vue/compiler-dom": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", - "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", - "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", + "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", - "@vue/compiler-core": "3.5.29", - "@vue/compiler-dom": "3.5.29", - "@vue/compiler-ssr": "3.5.29", - "@vue/shared": "3.5.29", + "@vue/compiler-core": "3.5.30", + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.6", + "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, @@ -5487,67 +5532,67 @@ "license": "MIT" }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", - "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", + "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-dom": "3.5.30", + "@vue/shared": "3.5.30" } }, "node_modules/@vue/reactivity": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz", - "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", + "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", "license": "MIT", "peer": true, "dependencies": { - "@vue/shared": "3.5.29" + "@vue/shared": "3.5.30" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz", - "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", + "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", "license": "MIT", "peer": true, "dependencies": { - "@vue/reactivity": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/reactivity": "3.5.30", + "@vue/shared": "3.5.30" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz", - "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", + "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", "license": "MIT", "peer": true, "dependencies": { - "@vue/reactivity": "3.5.29", - "@vue/runtime-core": "3.5.29", - "@vue/shared": "3.5.29", + "@vue/reactivity": "3.5.30", + "@vue/runtime-core": "3.5.30", + "@vue/shared": "3.5.30", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz", - "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", + "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", "license": "MIT", "peer": true, "dependencies": { - "@vue/compiler-ssr": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30" }, "peerDependencies": { - "vue": "3.5.29" + "vue": "3.5.30" } }, "node_modules/@vue/shared": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", - "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", "license": "MIT" }, "node_modules/abbrev": { @@ -5728,9 +5773,9 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", - "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", "dev": true, "license": "MIT", "dependencies": { @@ -5762,9 +5807,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.26", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.26.tgz", - "integrity": "sha512-c6Hxv5eR12gQmANICaAGM967LGOXZ4SVAuwkiDrqPqZ5oReOnj/ZBtj3dyfwAnEV5qbzspNzjMM8lZENDK8f5A==", + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", "funding": [ { "type": "opencollective", @@ -5798,14 +5843,23 @@ } }, "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" } }, "node_modules/axobject-query": { @@ -5874,12 +5928,11 @@ } }, "node_modules/bare-fs": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz", - "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz", + "integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", @@ -5900,12 +5953,11 @@ } }, "node_modules/bare-os": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.0.tgz", - "integrity": "sha512-64Rcwj8qlnTZU8Ps6JJEdSmxBEUGgI7g8l+lMtsJLl4IsfTcHMTfJ188u2iGV6P6YPRZrtv72B2kjn+hp+Yv3g==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz", + "integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "bare": ">=1.14.0" } @@ -5916,20 +5968,18 @@ "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-os": "^3.0.1" } }, "node_modules/bare-stream": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz", - "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.10.0.tgz", + "integrity": "sha512-DOPZF/DDcDruKDA43cOw6e9Quq5daua7ygcAwJE/pKJsRWhgSSemi7qVNGE5kyDIxIeN1533G/zfbvWX7Wcb9w==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { - "streamx": "^2.21.0", + "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { @@ -5946,12 +5996,11 @@ } }, "node_modules/bare-url": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", - "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", + "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", "dev": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-path": "^3.0.0" } @@ -5978,9 +6027,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -6056,9 +6105,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { @@ -6240,9 +6289,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001774", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", - "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", "funding": [ { "type": "opencollective", @@ -6577,9 +6626,9 @@ } }, "node_modules/core-js": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", - "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -6610,9 +6659,9 @@ } }, "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "license": "ISC", "engines": { "node": ">= 6" @@ -6639,14 +6688,14 @@ "license": "MIT" }, "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" @@ -7130,9 +7179,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "license": "MIT", "peer": true }, @@ -7247,9 +7296,9 @@ } }, "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" @@ -7275,14 +7324,14 @@ } }, "node_modules/dependency-tree": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/dependency-tree/-/dependency-tree-11.3.0.tgz", - "integrity": "sha512-T893F3p48rblazo45S/5jkFEvU8mzZ8obtNSyP2S1QCA8e9PpVH+hIakHnQYdnhitwQ8wo9btYJpQxnjiGm0Qg==", + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/dependency-tree/-/dependency-tree-11.4.0.tgz", + "integrity": "sha512-r4wZ1pfv8eQrnoWbIGdrJTVmlb0dkXdwBjKsotKO4gmfqrOsAMG+0+cfA5EZ3NO8umc85twXOl1eO27E5pjTzw==", "dev": true, "license": "MIT", "dependencies": { "commander": "^12.1.0", - "filing-cabinet": "^5.1.0", + "filing-cabinet": "^5.2.0", "precinct": "^12.2.0", "typescript": "^5.9.3" }, @@ -7348,9 +7397,9 @@ } }, "node_modules/detective-cjs": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/detective-cjs/-/detective-cjs-6.0.1.tgz", - "integrity": "sha512-tLTQsWvd2WMcmn/60T2inEJNhJoi7a//PQ7DwRKEj1yEeiQs4mrONgsUtEJKnZmrGWBBmE0kJ1vqOG/NAxwaJw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detective-cjs/-/detective-cjs-6.1.0.tgz", + "integrity": "sha512-Qt3S4IddVNDb+71lm+jmt5NznIsgcKlibTnrw9Zr91rT9vRwKp+73+ImqLTNrQj4YuOxnzrC7GwIAVwF7136XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7470,16 +7519,16 @@ } }, "node_modules/devalue": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", - "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", + "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", "license": "MIT", "peer": true }, "node_modules/devtools-protocol": { - "version": "0.0.1566079", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz", - "integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==", + "version": "0.0.1581282", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", + "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", "dev": true, "license": "BSD-3-Clause" }, @@ -7512,9 +7561,9 @@ } }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -7552,61 +7601,6 @@ "dpdm": "lib/bin/dpdm.js" } }, - "node_modules/dpdm/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/dpdm/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/dpdm/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/dpdm/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -7629,9 +7623,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.302", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", - "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -7652,9 +7646,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", - "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -7758,9 +7752,9 @@ } }, "node_modules/es-toolkit": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", - "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", "license": "MIT", "workspaces": [ "docs", @@ -7768,9 +7762,9 @@ ] }, "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7781,32 +7775,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" } }, "node_modules/escalade": { @@ -7864,18 +7858,18 @@ } }, "node_modules/eslint": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz", - "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", + "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.2", - "@eslint/config-helpers": "^0.5.2", - "@eslint/core": "^1.1.0", - "@eslint/plugin-kit": "^0.6.0", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.3", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -7884,9 +7878,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.1", + "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", - "espree": "^11.1.1", + "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -7897,7 +7891,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.1", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -7920,9 +7914,9 @@ } }, "node_modules/eslint-scope": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", - "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7982,9 +7976,9 @@ "peer": true }, "node_modules/espree": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", - "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -8040,13 +8034,14 @@ } }, "node_modules/esrap": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz", - "integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", + "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", "license": "MIT", "peer": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.4.15", + "@typescript-eslint/types": "^8.2.0" } }, "node_modules/esrecurse": { @@ -8225,17 +8220,17 @@ } }, "node_modules/filing-cabinet": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/filing-cabinet/-/filing-cabinet-5.1.0.tgz", - "integrity": "sha512-xA3nKuR0N762AtUloSEbq4T+tOqNf1rZ3vgPW8Sijurqz9rvArjTpZhfrV1OxSrhX6OUoDGAONXo6liKZTNXKQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/filing-cabinet/-/filing-cabinet-5.2.0.tgz", + "integrity": "sha512-eNrCJGdYQY0tV+ACNesQ7vb2aMxD76NM7THayMn0Z5XBt1Tonr4vbVN+FbhHfekKGQG9O5UaciDDR7+dw8P9ZA==", "dev": true, "license": "MIT", "dependencies": { "app-module-path": "^2.2.0", "commander": "^12.1.0", - "enhanced-resolve": "^5.19.0", + "enhanced-resolve": "^5.20.0", "module-definition": "^6.0.1", - "module-lookup-amd": "^9.1.0", + "module-lookup-amd": "^9.1.1", "resolve": "^1.22.11", "resolve-dependency-path": "^4.0.1", "sass-lookup": "^6.1.0", @@ -8323,9 +8318,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -8405,9 +8400,9 @@ } }, "node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", "dev": true, "license": "MIT", "dependencies": { @@ -8556,9 +8551,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8583,6 +8578,28 @@ "node": ">= 14" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -8596,10 +8613,43 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", - "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", "license": "MIT", "engines": { "node": ">=18" @@ -8848,26 +8898,26 @@ } }, "node_modules/i18next": { - "version": "25.8.13", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.13.tgz", - "integrity": "sha512-E0vzjBY1yM+nsFrtgkjLhST2NBkirkvOVoQa0MSldhsuZ3jUge7ZNpuwG0Cfc74zwo5ZwRzg3uOgT+McBn32iA==", + "version": "25.10.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.5.tgz", + "integrity": "sha512-jRnF7eRNsdcnh7AASSgaU3lj/8lJZuHkfsouetnLEDH0xxE1vVi7qhiJ9RhdSPUyzg4ltb7P7aXsFlTk9sxL2w==", "funding": [ { "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" + "url": "https://www.locize.com/i18next" }, { "type": "individual", "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" } ], "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.4" + "@babel/runtime": "^7.29.2" }, "peerDependencies": { "typescript": "^5" @@ -9010,12 +9060,12 @@ "license": "ISC" }, "node_modules/ini": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", - "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/internmap": { @@ -9578,19 +9628,19 @@ } }, "node_modules/license-report": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/license-report/-/license-report-6.8.1.tgz", - "integrity": "sha512-TxUUJNLTa+1kfYMK7uk7fLFtPrKMahX10K+pAY4UnGeNIJi+xWRzt/HrxpVlHj+9LG9Y2mhLzKFjHgAkj04ujg==", + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/license-report/-/license-report-6.8.2.tgz", + "integrity": "sha512-eWzJujDhPm5bKTrolTBt8mvL6YW3c5SY1kpqnt7GmTLU01rOtzjqe3sevOQLF2dPY7dV+VnSZIWV774Cgqz/Eg==", "license": "MIT", "dependencies": { "@kessler/tableify": "^1.0.2", "debug": "^4.4.3", "eol": "^0.10.0", "find-up-simple": "^1.0.1", - "got": "^14.6.0", - "ini": "^5.0.0", + "got": "^14.6.6", + "ini": "^6.0.0", "rc": "^1.2.8", - "semver": "^7.7.3", + "semver": "^7.7.4", "tablemark": "^4.1.0", "text-table": "^0.2.0", "visit-values": "^2.0.0" @@ -9612,9 +9662,9 @@ } }, "node_modules/lightningcss": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", - "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -9627,23 +9677,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.31.1", - "lightningcss-darwin-arm64": "1.31.1", - "lightningcss-darwin-x64": "1.31.1", - "lightningcss-freebsd-x64": "1.31.1", - "lightningcss-linux-arm-gnueabihf": "1.31.1", - "lightningcss-linux-arm64-gnu": "1.31.1", - "lightningcss-linux-arm64-musl": "1.31.1", - "lightningcss-linux-x64-gnu": "1.31.1", - "lightningcss-linux-x64-musl": "1.31.1", - "lightningcss-win32-arm64-msvc": "1.31.1", - "lightningcss-win32-x64-msvc": "1.31.1" + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", - "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "cpu": [ "arm64" ], @@ -9661,9 +9711,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", - "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -9681,9 +9731,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", - "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ "x64" ], @@ -9701,9 +9751,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", - "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "cpu": [ "x64" ], @@ -9721,9 +9771,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", - "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "cpu": [ "arm" ], @@ -9741,9 +9791,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", - "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "cpu": [ "arm64" ], @@ -9761,9 +9811,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", - "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "cpu": [ "arm64" ], @@ -9781,9 +9831,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", - "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], @@ -9801,9 +9851,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", - "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], @@ -9821,9 +9871,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", - "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], @@ -9841,9 +9891,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", - "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], @@ -9957,9 +10007,9 @@ } }, "node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -10062,9 +10112,9 @@ } }, "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true, "license": "CC0-1.0" }, @@ -10177,16 +10227,16 @@ } }, "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", - "ufo": "^1.6.1" + "ufo": "^1.6.3" } }, "node_modules/module-definition": { @@ -10292,9 +10342,9 @@ "optional": true }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "license": "MIT" }, "node_modules/node-source-walk": { @@ -10751,15 +10801,15 @@ } }, "node_modules/pdfjs-dist": { - "version": "5.4.624", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.624.tgz", - "integrity": "sha512-sm6TxKTtWv1Oh6n3C6J6a8odejb5uO4A4zo/2dgkHuC0iu8ZMAXOezEODkVaoVp8nX1Xzr+0WxFJJmUr45hQzg==", + "version": "5.5.207", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.5.207.tgz", + "integrity": "sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==", "license": "Apache-2.0", "engines": { - "node": ">=20.16.0 || >=22.3.0" + "node": ">=20.19.0 || >=22.13.0 || >=24" }, "optionalDependencies": { - "@napi-rs/canvas": "^0.1.88", + "@napi-rs/canvas": "^0.1.95", "node-readable-to-web-readable-stream": "^0.4.2" } }, @@ -10809,9 +10859,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -10831,6 +10881,18 @@ "node": ">=0.10.0" } }, + "node_modules/pixelmatch": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz", + "integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==", + "license": "ISC", + "dependencies": { + "pngjs": "^7.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, "node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", @@ -10885,10 +10947,19 @@ "node": ">=4" } }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -11160,9 +11231,9 @@ } }, "node_modules/posthog-js": { - "version": "1.354.0", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.354.0.tgz", - "integrity": "sha512-qrpToz7mN1PmEfo+Ob4Z8euX4z2p17LA0EAtFeyod3IVnlwnu+Ybea/oxVsPiq5YAPo+p5z73FcjF2yEJ7oZnA==", + "version": "1.363.3", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.363.3.tgz", + "integrity": "sha512-j1+MTbHO17kKXJMGDnaiW1EMOiA4AprE8EML6QnbSds+XbqHR2CdHa8T+/zIriZSoXlkZH4R+A4gY29lb5hdlA==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@opentelemetry/api": "^1.9.0", @@ -11170,10 +11241,10 @@ "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", - "@posthog/core": "1.23.1", - "@posthog/types": "1.354.0", + "@posthog/core": "1.24.1", + "@posthog/types": "1.363.3", "core-js": "^3.38.1", - "dompurify": "^3.3.1", + "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.28.2", "query-selector-shadow-dom": "^1.0.1", @@ -11181,9 +11252,9 @@ } }, "node_modules/preact": { - "version": "10.28.4", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz", - "integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==", + "version": "10.29.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.0.tgz", + "integrity": "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==", "license": "MIT", "funding": { "type": "opencollective", @@ -11240,6 +11311,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -11392,12 +11479,13 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, "license": "MIT" }, "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", "dependencies": { @@ -11416,9 +11504,9 @@ } }, "node_modules/puppeteer": { - "version": "24.37.5", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.37.5.tgz", - "integrity": "sha512-3PAOIQLceyEmn1Fi76GkGO2EVxztv5OtdlB1m8hMUZL3f8KDHnlvXbvCXv+Ls7KzF1R0KdKBqLuT/Hhrok12hQ==", + "version": "24.40.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.40.0.tgz", + "integrity": "sha512-IxQbDq93XHVVLWHrAkFP7F7iHvb9o0mgfsSIMlhHb+JM+JjM1V4v4MNSQfcRWJopx9dsNOr9adYv0U5fm9BJBQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -11426,9 +11514,9 @@ "@puppeteer/browsers": "2.13.0", "chromium-bidi": "14.0.0", "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1566079", - "puppeteer-core": "24.37.5", - "typed-query-selector": "^2.12.0" + "devtools-protocol": "0.0.1581282", + "puppeteer-core": "24.40.0", + "typed-query-selector": "^2.12.1" }, "bin": { "puppeteer": "lib/cjs/puppeteer/node/cli.js" @@ -11438,17 +11526,17 @@ } }, "node_modules/puppeteer-core": { - "version": "24.37.5", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.37.5.tgz", - "integrity": "sha512-ybL7iE78YPN4T6J+sPLO7r0lSByp/0NN6PvfBEql219cOnttoTFzCWKiBOjstXSqi/OKpwae623DWAsL7cn2MQ==", + "version": "24.40.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.40.0.tgz", + "integrity": "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag==", "dev": true, "license": "Apache-2.0", "dependencies": { "@puppeteer/browsers": "2.13.0", "chromium-bidi": "14.0.0", "debug": "^4.4.3", - "devtools-protocol": "0.0.1566079", - "typed-query-selector": "^2.12.0", + "devtools-protocol": "0.0.1581282", + "typed-query-selector": "^2.12.1", "webdriver-bidi-protocol": "0.4.1", "ws": "^8.19.0" }, @@ -11457,9 +11545,9 @@ } }, "node_modules/puppeteer/node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11576,12 +11664,12 @@ } }, "node_modules/react-draggable": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", - "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", + "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", "license": "MIT", "dependencies": { - "clsx": "^1.1.1", + "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { @@ -11589,15 +11677,6 @@ "react-dom": ">= 16.3.0" } }, - "node_modules/react-draggable/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/react-dropzone": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-15.0.0.tgz", @@ -11662,9 +11741,9 @@ "license": "MIT" }, "node_modules/react-number-format": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.4.tgz", - "integrity": "sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.5.tgz", + "integrity": "sha512-y8O2yHHj3w0aE9XO8d2BCcUOOdQTRSVq+WIuMlLVucAm5XNjJAy+BoOJiuQMldVYVOKTMyvVNfnbl2Oqp+YxGw==", "license": "MIT", "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", @@ -11742,13 +11821,13 @@ } }, "node_modules/react-rnd": { - "version": "10.5.2", - "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.5.2.tgz", - "integrity": "sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==", + "version": "10.5.3", + "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.5.3.tgz", + "integrity": "sha512-s/sIT3pGZnQ+57egijkTp9mizjIWrJz68Pq6yd+F/wniFY3IriML18dUXnQe/HP9uMiJ+9MAp44hljG99fZu6Q==", "license": "MIT", "dependencies": { - "re-resizable": "6.11.2", - "react-draggable": "4.4.6", + "re-resizable": "^6.11.2", + "react-draggable": "^4.5.0", "tslib": "2.6.2" }, "peerDependencies": { @@ -11763,9 +11842,9 @@ "license": "0BSD" }, "node_modules/react-router": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", - "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz", + "integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -11785,12 +11864,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", - "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.2.tgz", + "integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==", "license": "MIT", "dependencies": { - "react-router": "7.13.1" + "react-router": "7.13.2" }, "engines": { "node": ">=20.0.0" @@ -12004,15 +12083,15 @@ } }, "node_modules/recharts": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", - "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz", + "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==", "license": "MIT", "workspaces": [ "www" ], "dependencies": { - "@reduxjs/toolkit": "1.x.x || 2.x.x", + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", @@ -12220,15 +12299,15 @@ "license": "ISC" }, "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12242,31 +12321,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" } }, @@ -12289,14 +12368,14 @@ "license": "MIT" }, "node_modules/sass-lookup": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/sass-lookup/-/sass-lookup-6.1.0.tgz", - "integrity": "sha512-Zx+lVyoWqXZxHuYWlTA17Z5sczJ6braNT2C7rmClw+c4E7r/n911Zwss3h1uHI9reR5AgHZyNHF7c2+VIp5AUA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/sass-lookup/-/sass-lookup-6.1.1.tgz", + "integrity": "sha512-12dvZdQYTeKZ1ypjuiijZYuMZ1m0F+4+BkRX5yJi2WA9W3DBUrcdCt7bVuKlagHl11n8eYtalWDle+m98Ol2DA==", "dev": true, "license": "MIT", "dependencies": { "commander": "^12.1.0", - "enhanced-resolve": "^5.18.0" + "enhanced-resolve": "^5.20.0" }, "bin": { "sass-lookup": "bin/cli.js" @@ -12461,9 +12540,9 @@ } }, "node_modules/smol-toml": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", - "integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" @@ -12612,9 +12691,9 @@ } }, "node_modules/streamx": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", - "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", "dev": true, "license": "MIT", "dependencies": { @@ -12702,12 +12781,12 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -12875,9 +12954,9 @@ } }, "node_modules/svelte": { - "version": "5.53.5", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.5.tgz", - "integrity": "sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow==", + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.0.tgz", + "integrity": "sha512-SThllKq6TRMBwPtat7ASnm/9CDXnIhBR0NPGw0ujn2DVYx9rVwsPZxDaDQcYGdUz/3BYVsCzdq7pZarRQoGvtw==", "license": "MIT", "peer": true, "dependencies": { @@ -12890,7 +12969,7 @@ "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.3", + "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", @@ -13022,15 +13101,15 @@ } }, "node_modules/tailwindcss": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", - "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", "license": "MIT" }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", "license": "MIT", "engines": { "node": ">=6" @@ -13041,9 +13120,9 @@ } }, "node_modules/tar-fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", - "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", "dev": true, "license": "MIT", "dependencies": { @@ -13056,13 +13135,14 @@ } }, "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", "dev": true, "license": "MIT", "dependencies": { "b4a": "^1.6.4", + "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } @@ -13073,7 +13153,6 @@ "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { "streamx": "^2.12.5" } @@ -13093,61 +13172,6 @@ "node": ">=18" } }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/text-decoder": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", @@ -13185,9 +13209,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", "dev": true, "license": "MIT", "engines": { @@ -13230,9 +13254,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -13273,22 +13297,22 @@ } }, "node_modules/tldts": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", - "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.23" + "tldts-core": "^7.0.27" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", - "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", "dev": true, "license": "MIT" }, @@ -13306,9 +13330,9 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -13342,9 +13366,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -13483,9 +13507,9 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", - "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", + "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", "dev": true, "license": "MIT" }, @@ -13504,16 +13528,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", - "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.56.1", - "@typescript-eslint/parser": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1" + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -13838,9 +13862,9 @@ } }, "node_modules/vite-plugin-static-copy": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.2.0.tgz", - "integrity": "sha512-g2k9z8B/1Bx7D4wnFjPLx9dyYGrqWMLTpwTtPHhcU+ElNZP2O4+4OsyaficiDClus0dzVhdGvoGFYMJxoXZ12Q==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.4.0.tgz", + "integrity": "sha512-ekryzCw0ouAOE8tw4RvVL/dfqguXzumsV3FBKoKso4MQ1MUUrUXtl5RI4KpJQUNGqFEsg9kxl4EvDl02YtA9VQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13857,7 +13881,7 @@ "url": "https://github.com/sponsors/sapphi-red" }, "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/vite-tsconfig-paths": { @@ -13914,9 +13938,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -14000,9 +14024,9 @@ } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -14029,17 +14053,17 @@ } }, "node_modules/vue": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", - "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", + "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", "license": "MIT", "peer": true, "dependencies": { - "@vue/compiler-dom": "3.5.29", - "@vue/compiler-sfc": "3.5.29", - "@vue/runtime-dom": "3.5.29", - "@vue/server-renderer": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-sfc": "3.5.30", + "@vue/runtime-dom": "3.5.30", + "@vue/server-renderer": "3.5.30", + "@vue/shared": "3.5.30" }, "peerDependencies": { "typescript": "*" @@ -14287,9 +14311,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -14335,9 +14359,9 @@ } }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "bin": { diff --git a/frontend/package.json b/frontend/package.json index c53d89c9a4..a26b3315fd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -55,7 +55,7 @@ "@tauri-apps/plugin-shell": "^2.3.5", "@userback/widget": "^0.3.12", "autoprefixer": "^10.4.21", - "axios": "^1.13.2", + "axios": "^1.15.0", "d3": "^7.9.0", "globals": "^17.1.0", "i18next": "^25.5.2", @@ -64,6 +64,7 @@ "license-report": "^6.8.0", "pdfjs-dist": "^5.4.149", "peerjs": "^1.5.5", + "pixelmatch": "^7.1.0", "posthog-js": "^1.268.0", "qrcode.react": "^4.2.0", "react": "^19.1.1", @@ -79,52 +80,7 @@ "web-vitals": "^5.1.0" }, "scripts": { - "prep": "tsx scripts/setup-env.ts && npm run generate-icons", - "prep:saas": "tsx scripts/setup-env.ts --saas && npm run generate-icons", - "prep:desktop": "tsx scripts/setup-env.ts --desktop && npm run generate-icons", - "prep:desktop-build": "node scripts/build-provisioner.mjs && npm run prep:desktop", - "dev": "npm run prep && vite", - "dev:core": "npm run prep && vite --mode core", - "dev:proprietary": "npm run prep && vite --mode proprietary", - "dev:saas": "npm run prep:saas && vite --mode saas", - "dev:desktop": "npm run prep:desktop && vite --mode desktop", - "lint": "npm run lint:eslint && npm run lint:cycles", - "lint:eslint": "eslint --max-warnings=0", - "lint:cycles": "dpdm src --circular --no-warning --no-tree --exit-code circular:1", - "build": "npm run prep && vite build", - "build:core": "npm run prep && vite build --mode core", - "build:proprietary": "npm run prep && vite build --mode proprietary", - "build:saas": "npm run prep:saas && vite build --mode saas", - "build:desktop": "npm run prep:desktop && vite build --mode desktop", - "preview": "vite preview", - "tauri-dev": "npm run prep:desktop && tauri dev --no-watch", - "tauri-build": "npm run prep:desktop-build && tauri build", - "_tauri-build-dev": "npm run prep:desktop && tauri build", - "tauri-build-dev": "npm run _tauri-build-dev -- --no-bundle", - "tauri-build-dev-mac": "npm run _tauri-build-dev -- --bundles app", - "tauri-build-dev-windows": "npm run _tauri-build-dev -- --bundles nsis", - "tauri-build-dev-linux": "npm run _tauri-build-dev -- --bundles appimage", - "tauri-clean": "cd src-tauri && cargo clean && cd .. && rm -rf dist build", - "typecheck": "npm run typecheck:proprietary", - "typecheck:core": "tsc --noEmit --project src/core/tsconfig.json", - "typecheck:proprietary": "tsc --noEmit --project src/proprietary/tsconfig.json", - "typecheck:saas": "tsc --noEmit --project src/saas/tsconfig.json", - "typecheck:desktop": "tsc --noEmit --project src/desktop/tsconfig.json", - "typecheck:scripts": "tsc --noEmit --project scripts/tsconfig.json", - "typecheck:all": "npm run typecheck:core && npm run typecheck:proprietary && npm run typecheck:saas && npm run typecheck:desktop && npm run typecheck:scripts", - "check": "npm run typecheck && npm run lint && npm run test:run", - "generate-licenses": "node scripts/generate-licenses.js", - "generate-icons": "node scripts/generate-icons.js", - "generate-icons:verbose": "node scripts/generate-icons.js --verbose", - "generate-sample-pdf": "node scripts/sample-pdf/generate.mjs", - "test": "vitest", - "test:run": "vitest run", - "test:watch": "vitest --watch", - "test:coverage": "vitest --coverage", - "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --ui", - "test:e2e:install": "playwright install", - "update:minor": "npm outdated || npm update && npm audit fix && npm test", + "update:minor": "npm outdated || npm update --before=$(date -v-7d +%Y-%m-%d) && (npm audit fix --before=$(date -v-7d +%Y-%m-%d) || true) && npm test", "update:major": "npx npm-check-updates -u && npm install", "update:interactive": "npx npm-check-updates -i", "update:minor-strict": "npx npm-check-updates -u --target minor && npm install" @@ -173,6 +129,7 @@ "postcss-cli": "^11.0.1", "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", + "prettier": "^3.8.1", "puppeteer": "^24.25.0", "tsx": "^4.21.0", "typescript": "^5.9.2", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 14eb25f85c..58590adebf 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,11 +1,11 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; /** * @see https://playwright.dev/docs/test-configuration */ export default defineConfig({ - testDir: './src/core/tests', - testMatch: '**/*.spec.ts', + testDir: "./src/core/tests", + testMatch: "**/*.spec.ts", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -15,34 +15,34 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:5173', + baseURL: "http://localhost:5173", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: "on-first-retry", }, /* Configure projects for major browsers */ projects: [ { - name: 'chromium', - use: { - ...devices['Desktop Chrome'], - viewport: { width: 1920, height: 1080 } + name: "chromium", + use: { + ...devices["Desktop Chrome"], + viewport: { width: 1920, height: 1080 }, }, }, { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + name: "firefox", + use: { ...devices["Desktop Firefox"] }, }, { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, + name: "webkit", + use: { ...devices["Desktop Safari"] }, }, /* Test against mobile viewports. */ @@ -68,8 +68,8 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: 'npm run dev', - url: 'http://localhost:5173', + command: "npx vite", + url: "http://localhost:5173", reuseExistingServer: !process.env.CI, }, -}); \ No newline at end of file +}); diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 57e730c99a..7b8895cce8 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -1,6 +1,3 @@ module.exports = { - plugins: [ - require('@tailwindcss/postcss'), - require('autoprefixer'), - ], + plugins: [require("@tailwindcss/postcss"), require("autoprefixer")], }; diff --git a/frontend/public/css/cookieconsentCustomisation.css b/frontend/public/css/cookieconsentCustomisation.css index ec360c20be..fd1a8ff355 100644 --- a/frontend/public/css/cookieconsentCustomisation.css +++ b/frontend/public/css/cookieconsentCustomisation.css @@ -1,206 +1,205 @@ /* Light theme variables */ :root { - --cc-bg: #ffffff; - --cc-primary-color: #1c1c1c; - --cc-secondary-color: #666666; + --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-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-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-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-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-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-toggle-readonly-bg: #f1f3f4; + --cc-toggle-readonly-knob-bg: #79747e; + --cc-toggle-readonly-knob-icon-color: #f1f3f4; - --cc-section-category-border: #e0e0e0; + --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-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-footer-bg: #ffffff; - --cc-footer-color: #1c1c1c; - --cc-footer-border-color: #ffffff; + --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--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-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-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-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-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-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-toggle-readonly-bg: #555555; + --cc-toggle-readonly-knob-bg: #8e8e8e; + --cc-toggle-readonly-knob-icon-color: #555555; - --cc-section-category-border: #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-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-footer-bg: #2d2d2d; - --cc-footer-color: #e5e5e5; - --cc-footer-border-color: #2d2d2d; + --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__body { + max-width: 90% !important; + flex-direction: row !important; + align-items: center !important; } -.cm__desc{ - max-width: 70rem !important; +.cm__desc { + max-width: 70rem !important; } -.cm__btns{ - flex-direction: row-reverse !important; - gap:10px !important; - padding-top: 3.4rem !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__body { + max-width: 90% !important; + flex-direction: column !important; + align-items: normal !important; + } - .cm__btns{ - padding-top: 1rem !important; - } + .cm__btns { + padding-top: 1rem !important; + } } /* Toggle visibility fixes */ #cc-main .section__toggle { - opacity: 0 !important; /* Keep invisible but functional */ + opacity: 0 !important; /* Keep invisible but functional */ } #cc-main .toggle__icon { - display: flex !important; - align-items: center !important; - justify-content: flex-start !important; + 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; + 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; + 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; + 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; + 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; + color: var(--cc-primary-color) !important; } #cc-main .pm__section-desc { - color: var(--cc-secondary-color) !important; + 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; + 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; + 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; + background: var(--cc-bg) !important; + color: var(--cc-primary-color) !important; } #cc-main .cm__title { - color: var(--cc-primary-color) !important; + color: var(--cc-primary-color) !important; } #cc-main .cm__desc { - color: var(--cc-primary-color) !important; + color: var(--cc-primary-color) !important; } #cc-main .cm__footer { - color: var(--cc-primary-color) !important; + color: var(--cc-primary-color) !important; } #cc-main .cm__footer-links a, #cc-main .cm__link { - color: var(--cc-primary-color) !important; -} \ No newline at end of file + color: var(--cc-primary-color) !important; +} diff --git a/frontend/public/images/google-drive.svg b/frontend/public/images/google-drive.svg new file mode 100644 index 0000000000..03b2f21290 --- /dev/null +++ b/frontend/public/images/google-drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/locales/ar-AR/translation.toml b/frontend/public/locales/ar-AR/translation.toml index e3329dc08f..1bd56a9ae7 100644 --- a/frontend/public/locales/ar-AR/translation.toml +++ b/frontend/public/locales/ar-AR/translation.toml @@ -8,6 +8,7 @@ black = "أسود" blue = "أزرق" bored = "الانتظار بالملل؟" cancel = "إلغاء" +confirm = "تأكيد" changedCredsMessage = "تم تغيير بيانات الاعتماد!" chooseFile = "اختر ملÙًا" close = "إغلاق" @@ -146,6 +147,7 @@ insufficientCredits = "الأرصدة غير كاÙية. المطلوب: {{requi loadingCredits = "جار٠التحقق من الأرصدة..." loadingProStatus = "جار٠التحقق من حالة الاشتراك..." noticeTopUpOrPlan = "لا توجد أرصدة كاÙية، يرجى إعادة الشحن أو الترقية إلى خطة" +accessInvite = "دعوة" [account] accountSettings = "إعدادات الحساب" @@ -1427,6 +1429,34 @@ title = "المعالجة" description = "المدة القصوى لانتظار مهمة معالجة قبل الإبلاغ عن خطأ." label = "مهلة المعالجة (ثوانÙ)" +[admin.settings.storage] +description = "التحكم ÙÙŠ تخزين الخادم وخيارات المشاركة." +title = "تخزين Ø§Ù„Ù…Ù„ÙØ§Øª والمشاركة" + +[admin.settings.storage.enabled] +description = "السماح للمستخدمين بتخزين Ø§Ù„Ù…Ù„ÙØ§Øª على الخادم." +label = "تمكين تخزين Ù…Ù„ÙØ§Øª الخادم" + +[admin.settings.storage.sharing.email] +description = "السماح بالمشاركة عبر عناوين البريد الإلكتروني." +label = "تمكين المشاركة عبر البريد الإلكتروني" +mailLink = "ضبط إعدادات البريد" +mailNote = "يتطلب إعداد البريد. " + +[admin.settings.storage.sharing.enabled] +description = "السماح للمستخدمين بمشاركة Ø§Ù„Ù…Ù„ÙØ§Øª المخزنة." +label = "تمكين المشاركة" + +[admin.settings.storage.sharing.links] +description = "السماح بالمشاركة عبر روابط تتطلب تسجيل الدخول." +frontendUrlLink = "الضبط ÙÙŠ إعدادات النظام" +frontendUrlNote = "يتطلب عنوان URL للواجهة الأمامية. " +label = "تمكين روابط المشاركة" + +[admin.settings.storage.signing.enabled] +description = "السماح للمستخدمين بإنشاء جلسات توقيع بمشاركة عدة أطراÙ. يتطلب تمكين تخزين Ù…Ù„ÙØ§Øª الخادم." +label = "تمكين التوقيع الجماعي (Ø£Ù„ÙØ§)" + [admin.settings.unsavedChanges] cancel = "متابعة التحرير" discard = "تجاهل التغييرات" @@ -2059,7 +2089,19 @@ numbers = "الأرقام/النطاقات: 5, 10-20" progressions = "التقدّم: 3n, 4n+1" [certSign] +allSigned = "وقّع جميع المشاركين. جاهز للإنهاء." +awaitingSignatures = "بانتظار التواقيع" +signatureProgress = "{{signedCount}}/{{totalCount}} توقيعات" chooseCertificate = "اختر مل٠الشهادة" +declined = "مرÙوض" +fetchFailed = "ÙØ´Ù„ ÙÙŠ تحميل بيانات التوقيع" +finalized = "تم الإنهاء" +notified = "قيد الانتظار" +partialNote = "يمكنك الإنهاء مبكرًا بالتواقيع الحالية. سيتم استبعاد المشاركين غير الموقّعين." +pending = "قيد الانتظار" +readyToFinalize = "جاهز للإنهاء" +signed = "موقّع" +viewed = "تمت المشاهدة" chooseJksFile = "اختر مل٠JKS" chooseP12File = "اختر مل٠PKCS12" choosePfxFile = "اختر مل٠PFX" @@ -2082,6 +2124,7 @@ title = "توقيع الشهادة" invisible = "غير مرئي" stepTitle = "مظهر التوقيع" visible = "مرئي" +visibility = "الظهور" [certSign.appearance.options] title = "ØªÙØ§ØµÙŠÙ„ التوقيع" @@ -2188,6 +2231,252 @@ bullet4 = "يمكن استخدام شهادات مخصّصة للتحقق" text = "عند التحقق من التواقيع، ØªÙØ¸Ù‡Ø± الأداة صلاحيتها، ومن وقّع ومتى، وما إذا تم تعديل المستند بعد التوقيع." title = "التحقق من التواقيع" +[certSign.collab.finalize] +button = "إنهاء وتحميل مل٠PDF الموقّع" +early = "إنهاء بالتواقيع الحالية" + +[certSign.collab.sessionDetail] +addButton = "Ø¥Ø¶Ø§ÙØ© مشاركين" +addParticipants = "Ø¥Ø¶Ø§ÙØ© مشاركين" +addParticipantsError = "تعذّر Ø¥Ø¶Ø§ÙØ© المشاركين" +backToList = "العودة إلى الجلسات" +deleteConfirm = "هل أنت متأكد؟ لا يمكن التراجع عن ذلك." +deleteError = "ÙØ´Ù„ حذ٠الجلسة" +deleted = "تم حذ٠الجلسة" +deleteSession = "حذ٠الجلسة" +dueDate = "تاريخ الاستحقاق" +finalizeError = "تعذّر إنهاء الجلسة" +loadPdfError = "تعذّر تحميل مل٠PDF الموقّع" +loadSignedPdf = "تحميل مل٠PDF الموقّع إلى Ø§Ù„Ù…Ù„ÙØ§Øª النشطة" +messageLabel = "رسالة" +noAdditionalInfo = "لا توجد معلومات إضاÙية" +owner = "المالك" +participantRemoved = "تمت إزالة المشارك" +participants = "المشاركون" +participantsAdded = "تمت Ø¥Ø¶Ø§ÙØ© المشاركين بنجاح" +removeParticipant = "إزالة" +removeParticipantError = "تعذّر إزالة المشارك" +selectUsers = "اختر المستخدمين..." +sessionInfo = "معلومات الجلسة" +workbenchTitle = "إدارة الجلسة" + +[certSign.collab.signRequest] +addedToFiles = "تمت Ø¥Ø¶Ø§ÙØ© المستند إلى Ø§Ù„Ù…Ù„ÙØ§Øª النشطة" +addSignature = "أض٠توقيعك" +addToFiles = "Ø¥Ø¶Ø§ÙØ© إلى Ø§Ù„Ù…Ù„ÙØ§Øª النشطة" +advancedSettings = "إعدادات متقدمة" +backToList = "العودة إلى طلبات التوقيع" +certificateChoice = "اختر شهادة للتوقيع بها" +changeSignature = "تغيير التوقيع" +clearSignature = "مسح التوقيع" +completeAndSign = "إكمال والتوقيع" +createNewSignature = "إنشاء توقيع جديد" +declineButton = "Ø±ÙØ¶" +decline = "Ø±ÙØ¶ الطلب" +deleteSelected = "حذ٠التوقيع المحدد" +drawSignature = "ارسم توقيعك أدناه" +dueDate = "تاريخ الاستحقاق" +fileTooLarge = "يجب أن يكون حجم المل٠أقل من 5MB" +fontFamily = "عائلة الخط" +fontSize = "حجم الخط: {{size}}px" +fontSizePlaceholder = "الحجم" +from = "من" +invalidCertFile = "يرجى اختيار مل٠شهادة P12 أو PFX" +invalidFileType = "يرجى اختيار مل٠صورة" +location = "الموقع (اختياري)" +locationPlaceholder = "من أين توقّع؟" +message = "رسالة" +noCertificate = "يرجى اختيار مل٠شهادة" +noSignatures = "يرجى وضع توقيع واحد على الأقل على مل٠PDF" +p12File = "مل٠شهادة P12/PFX" +password = "كلمة مرور الشهادة" +passwordPlaceholder = "أدخل كلمة المرور..." +penColor = "لون القلم" +penSize = "حجم القلم: {{size}}px" +placementActive = "انقر على مل٠PDF للوضع" +placeSignatureButton = "وضع التوقيع على مل٠PDF" +reason = "السبب (اختياري)" +reasonPlaceholder = "لماذا توقّع؟" +removeImage = "إزالة الصورة" +removeCertFile = "إزالة الملÙ" +savedSignatures = "التواقيع المحÙوظة" +selectFile = "اختر مل٠صورة" +selectSignatureTitle = "اختر أو أنشئ توقيعًا" +signButton = "توقيع المستند" +signatureInfo = "يتم ضبط هذه الإعدادات بواسطة مالك المستند" +signaturePlaced = "تم وضع التوقيع على Ø§Ù„ØµÙØ­Ø©" +signatureSettings = "إعدادات التوقيع" +signatureText = "نص التوقيع" +signatureTextPlaceholder = "أدخل اسمك..." +signatureTypeLabel = "نوع التوقيع" +signingTitle = "التوقيع" +textColor = "لون النص" +typeSignature = "اكتب اسمك لإنشاء توقيع" +uploadCert = "شهادة مخصصة" +uploadCertDesc = "استخدم شهادة P12/PFX الخاصة بك" +uploadSignature = "Ø§Ø±ÙØ¹ صورة توقيعك" +usePersonalCert = "شهادة شخصية" +usePersonalCertDesc = "تÙنشأ تلقائيًا لحسابك" +useServerCert = "شهادة المؤسسة" +useServerCertDesc = "شهادة مؤسسة مشتركة" +workbenchTitle = "طلب توقيع" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "اختر لون القلم" +continue = "متابعة" + +[certSign.collab.signRequest.certModal] +description = "لقد وضعت {{count}} توقيع(ات). اختر شهادتك لإكمال التوقيع." +sign = "توقيع المستند" +certValidating = "جار٠التحقق من الشهادة..." +certValidUntil = "الشهادة صالحة حتى {{date}}" +certInvalid = "الشهادة غير صالحة: {{error}}" +certInvalidFallback = "شهادة غير صالحة" +certNetworkError = "تعذّر التحقق من الشهادة" +title = "تهيئة الشهادة" + +[certSign.collab.signRequest.image] +hint = "حمّل صورة توقيعك بصيغة PNG أو JPG" + +[certSign.collab.signRequest.mode] +move = "نقل التوقيع" +place = "وضع التوقيع" +title = "وضع التوقيع أو النقل" + +[certSign.collab.signRequest.modeTabs] +draw = "رسم" +image = "Ø±ÙØ¹" +text = "كتابة" + +[certSign.collab.signRequest.placeSignature] +message = "انقر على مل٠PDF لوضع توقيعك" +title = "وضع التوقيع" + +[certSign.collab.signRequest.preview] +imageAlt = "التوقيع المحدد" +missing = "لا معاينة" +textFallback = "توقيع" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "توقيع مرسوم" +defaultImageLabel = "توقيع مرÙوع" +defaultLabel = "توقيع" +defaultTextLabel = "توقيع مكتوب" +delete = "حذ٠التوقيع" +none = "لا توجد تواقيع محÙوظة" + +[certSign.collab.signRequest.signatureType] +draw = "رسم" +type = "كتابة" +upload = "Ø±ÙØ¹" + +[certSign.collab.signRequest.steps] +back = "رجوع" +cancelPlacement = "إلغاء الوضع" +certificate = "الشهادة" +clickMultipleTimes = "انقر على مل٠PDF عدة مرات لوضع التواقيع. اسحب أي توقيع لتحريكه أو تغيير حجمه." +clickToPlace = "انقر على مل٠PDF حيث ترغب أن يظهر توقيعك." +continue = "المتابعة لاختيار الشهادة" +continueToPlacement = "المتابعة إلى الوضع" +continueToReview = "المتابعة إلى المراجعة" +createSignature = "إنشاء توقيع" +invisible = "غير مرئي" +location = "الموقع:" +multipleSignatures = "{{count}} توقيعات سيتم تطبيقها على مل٠PDF" +oneSignature = "سيتم تطبيق توقيع واحد على مل٠PDF" +placeOnPdf = "وضع على مل٠PDF" +reason = "السبب:" +reviewTitle = "مراجعة قبل التوقيع" +signaturePlaced = "تم وضع التوقيع على Ø§Ù„ØµÙØ­Ø© {{page}}. يمكنك ضبط الموضع بالنقر مرة أخرى أو المتابعة إلى المراجعة." +visible = "مرئي" +visibility = "الظهور:" +yourSignatures = "تواقيعك ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "اللون" +fontLabel = "الخط" +fontSizeLabel = "الحجم" +fontSizePlaceholder = "16" +label = "نص التوقيع" +modalHint = "أدخل اسمك، ثم انقر متابعة لوضعه على مل٠PDF." +placeholder = "أدخل اسمك..." + +[certSign.collab.participant] +certValidating = "جار٠التحقق من الشهادة..." +certValid = "✓ الشهادة صالحة" +certValidUntil = " حتى {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "شهادة غير صالحة" +certNetworkError = "تعذّر التحقق من الشهادة" + +[certSign.collab.addParticipants] +add = "Ø¥Ø¶Ø§ÙØ© {{count}} مشارك" +back = "رجوع" +configureSignatures = "تهيئة إعدادات التوقيع" +continue = "المتابعة إلى إعدادات التوقيع" +reasonHelp = "تحديد سبب توقيع مسبق لهؤلاء المشاركين (اختياري، يمكنهم تغييره عند التوقيع)" +reasonPlaceholder = "مثال: مواÙقة، مراجعة..." +selectUsers = "اختيار المستخدمين" + +[certSign.collab.sessionCreation] +includeSummaryPage = "تضمين ØµÙØ­Ø© ملخص التواقيع" +includeSummaryPageHelp = "Ø³ØªÙØ¶Ø§Ù ØµÙØ­Ø© ملخص ÙÙŠ النهاية تضم جميع بيانات التوقيع الوصÙية. سيتم Ø¥Ø®ÙØ§Ø¡ مربعات توقيع الشهادة الرقمية على Ø§Ù„ØµÙØ­Ø§Øª Ø§Ù„ÙØ±Ø¯ÙŠØ© (لا تتأثر التواقيع اليدوية)." + +[certSign.collab.sessionList] +active = "نشطة" +finalized = "تم الإنهاء" + +[certSign.collab.signatureSettings] +description = "تهيئة كيÙية ظهور التواقيع لجميع المشاركين" +title = "مظهر التوقيع" + +[certSign.collab.userSelector] +inviteUsers = "Ø¥Ø¶Ø§ÙØ© مستخدمين" +loadError = "ÙØ´Ù„ ÙÙŠ تحميل المستخدمين" +noTeam = "لا يوجد ÙØ±ÙŠÙ‚" +noUsers = "لم يتم العثور على مستخدمين آخرين." +placeholder = "اختر المستخدمين..." + +[certSign.mobile] +panelActions = "إجراءات" +panelDocument = "مستند" +panelPeople = "أشخاص" + +[certSign.sessions] +deleted = "تم حذ٠الجلسة" +fetchFailed = "ÙØ´Ù„ تحميل ØªÙØ§ØµÙŠÙ„ الجلسة" +finalized = "تم إنهاء الجلسة" +loaded = "تم تحميل مل٠PDF الموقّع" +pdfNotReady = "مل٠PDF غير جاهز" +pdfNotReadyDesc = "يجري إنشاء مل٠PDF الموقّع. يرجى المحاولة بعد قليل." + +[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 مخصص" + [changeCreds] changePassword = "أنت تستخدم بيانات تسجيل الدخول Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ©. يرجى إدخال كلمة مرور جديدة" changeUsername = "حدّث اسم المستخدم. سيتم تسجيل خروجك بعد التحديث." @@ -3242,6 +3531,46 @@ totalSelected = "الإجمالي المحدد" unsupported = "غير مدعوم" unzip = "ÙÙƒ الضغط" uploadError = "ÙØ´Ù„ تحميل بعض Ø§Ù„Ù…Ù„ÙØ§Øª." +copyCreated = "تم Ø­ÙØ¸ نسخة على هذا الجهاز." +copyFailed = "تعذّر إنشاء نسخة." +leaveShare = "إزالة من قائمتي" +leaveShareFailed = "تعذّر إزالة المل٠المشترك." +leaveShareSuccess = "تمت الإزالة من قائمتك المشتركة." +removeBoth = "إزالة من كليهما" +removeFilePrompt = "هذا المل٠محÙوظ على هذا الجهاز وعلى خادمك. من أين ترغب ÙÙŠ إزالته؟" +removeFileTitle = "إزالة الملÙ" +removeLocalOnly = "هذا الجهاز Ùقط" +removeServerFailed = "تعذّر إزالة المل٠من الخادم." +removeServerOnly = "الخادم Ùقط" +removeServerOnlyPrompt = "هذا المل٠مخزّن Ùقط على خادمك. هل ترغب ÙÙŠ إزالته من الخادم؟" +removeServerSuccess = "تمت الإزالة من الخادم." +removeSharedPrompt = "هذا المل٠مشترك معك. يمكنك إزالته من هذا الجهاز أو من قائمتك المشتركة." +removeSharedServerOnlyBlockedPrompt = "هذا المل٠مشترك معك ومخزّن Ùقط على الخادم." +removeSharedServerOnlyPrompt = "هذا المل٠مشترك معك ومخزّن Ùقط على الخادم. هل تود إزالته من قائمتك؟" +changesNotUploaded = "لم ØªÙØ±Ùع التغييرات" +cloudFile = "مل٠سحابي" +filterAll = "الكل" +filterLocal = "محلي" +filterSharedByMe = "مشترك بواسطتي" +filterSharedWithMe = "مشترك معي" +lastSynced = "آخر مزامنة" +localOnly = "محلي Ùقط" +makeCopy = "إنشاء نسخة" +owner = "المالك" +ownerUnknown = "غير معروÙ" +share = "مشاركة" +shareSelected = "مشاركة المحدد" +sharedByYou = "مشترك بواسطتك" +sharedEditNoticeBody = "ليس لديك حقوق تحرير لنسخة الخادم من هذا الملÙ. سيتم Ø­ÙØ¸ أي تعديلات تجريها كنسخة محلية." +sharedEditNoticeConfirm = "حسنًا" +sharedEditNoticeTitle = "نسخة خادم للعرض Ùقط" +sharedWithYou = "مشترك معك" +sharing = "مشاركة" +storageState = "التخزين" +synced = "تمت المزامنة" +updateOnServer = "تحديث على الخادم" +uploadSelected = "Ø±ÙØ¹ المحدد" +uploadToServer = "Ø±ÙØ¹ إلى الخادم" [files] addFiles = "Ø¥Ø¶Ø§ÙØ© Ù…Ù„ÙØ§Øª" @@ -3367,6 +3696,77 @@ title = "حول تسطيح Ù…Ù„ÙØ§Øª PDF" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "نبذة عن التوقيع الجماعي" + +[groupSigning.tooltip.finalization] +bullet1 = "ØªÙØ·Ø¨Ù‘ÙŽÙ‚ جميع التواقيع حسب ترتيب المشاركين الذي حددته" +bullet2 = "يمكنك الإنهاء بتواقيع جزئية عند الحاجة" +bullet3 = "بعد الإنهاء، لا يمكن تعديل الجلسة" +description = "عند توقيع جميع المشاركين (أو إذا اخترت الإنهاء مبكرًا)ØŒ يمكنك إنشاء مل٠PDF النهائي الموقّع." +title = "عملية الإنهاء" + +[groupSigning.tooltip.roles] +bullet1 = "المالك (أنت): ينشئ الجلسة، يضبط Ø§ÙØªØ±Ø§Ø¶Ø§Øª التوقيع، ينهي المستند" +bullet2 = "المشاركون: ينشئون توقيعهم، يختارون الشهادة، يضعونها على مل٠PDF" +bullet3 = "لا يمكن للمشاركين تعديل إعدادات الظهور أو السبب أو الموقع للتوقيع" +description = "أنت تتحكم ÙÙŠ إعدادات مظهر التوقيع لجميع المشاركين." +title = "أدوار المشاركين" + +[groupSigning.tooltip.sequential] +bullet1 = "يجب أن يوقّع المشارك الأول قبل أن يتمكن الثاني من الوصول إلى المستند" +bullet2 = "يضمن ترتيب توقيع مناسبًا للامتثال القانوني" +bullet3 = "يمكنك إعادة ترتيب المشاركين بسحبهم ÙÙŠ القائمة" +description = "يوقّع المشاركون المستندات بالترتيب الذي تحدده. يتلقى كل موقّع إشعارًا عندما يحين دوره." +title = "التوقيع المتسلسل" + +[groupSigning.steps] +back = "رجوع" +completed = "مكتمل" +current = "حالي" +stepLabel = "الخطوة {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "المتابعة إلى المراجعة" +invisible = "ستكون التواقيع غير مرئية (بيانات وصÙية Ùقط)" +locationLabel = "الموقع:" +preview = "معاينة" +reasonLabel = "السبب:" +title = "تهيئة إعدادات التوقيع" +visible = "ستكون التواقيع مرئية على Ø§Ù„ØµÙØ­Ø© {{page}}" + +[groupSigning.steps.review] +document = "المستند" +dueDate = "تاريخ الاستحقاق (اختياري)" +dueDatePlaceholder = "اختر تاريخ الاستحقاق..." +invisible = "غير مرئي (بيانات وصÙية Ùقط)" +location = "الموقع:" +logo = "الشعار:" +logoHidden = "بدون شعار" +logoShown = "يظهر شعار Stirling PDF" +participants = "المشاركون" +reason = "السبب:" +send = "إرسال طلبات التوقيع" +signatureSettings = "إعدادات التوقيع" +title = "مراجعة ØªÙØ§ØµÙŠÙ„ الجلسة" +titleShort = "مراجعة وإرسال" +visibility = "الظهور:" +visible = "مرئي على Ø§Ù„ØµÙØ­Ø© {{page}}" +participantCount = "{{count}} مشارك سيوقّعون بالترتيب" + +[groupSigning.steps.selectDocument] +continue = "المتابعة لاختيار المشاركين" +noFile = "يرجى اختيار مل٠PDF واحد من Ù…Ù„ÙØ§ØªÙƒ النشطة لإنشاء جلسة توقيع." +selectedFile = "المستند المحدد" +title = "اختيار مستند" + +[groupSigning.steps.selectParticipants] +continue = "المتابعة إلى إعدادات التوقيع" +count = "تم اختيار {{count}} مشارك" +label = "اختر المشاركين" +placeholder = "اختر المشاركين للتوقيع..." +title = "اختيار المشاركين" + [getPdfInfo] downloadJson = "تحميل JSON" downloads = "التنزيلات" @@ -4460,7 +4860,10 @@ zoomOut = "تصغير" [viewer] cannotPreviewFile = "لا يمكن معاينة الملÙ" +disableColorFilter = "تعطيل مرشح الألوان" dualPageView = "عرض ØµÙØ­ØªÙŠÙ†" +enableDarkFilter = "تمكين المرشح الداكن" +enableSepiaFilter = "تمكين مرشح سيبيا" firstPage = "Ø§Ù„ØµÙØ­Ø© الأولى" lastPage = "Ø§Ù„ØµÙØ­Ø© الأخيرة" nextPage = "Ø§Ù„ØµÙØ­Ø© التالية" @@ -4470,6 +4873,22 @@ singlePageView = "عرض ØµÙØ­Ø© واحدة" unknownFile = "مل٠غير معروÙ" zoomIn = "تكبير" zoomOut = "تصغير" +resetZoom = "إعادة ضبط التكبير" + +[viewer.nonPdf] +fileTypeBadge = "مل٠{{type}}" +convertToPdf = "التحويل إلى PDF" +loading = "جار٠التحميل..." +emptyFile = "Ù…Ù„Ù ÙØ§Ø±Øº" +csvStats = "{{rows}} صÙو٠· {{columns}} أعمدة · {{size}}" +sortedBy = "مرتّب حسب: {{column}}" +columnDefault = "العمود {{index}}" +htmlPreviewWarning = "معاينة HTML — قد لا يتم تحميل الموارد الخارجية · {{size}}" +htmlPreview = "معاينة HTML" +invalidJson = "JSON غير صالح — عرض المحتوى الخام" +textStats = "{{lines}} أسطر · {{size}}" +lineNumbers = "أرقام الأسطر" +renderMarkdown = "عرض Markdown" [viewer.attachments] title = "المرÙقات" @@ -4531,6 +4950,7 @@ toggleAttachments = "إظهار/Ø¥Ø®ÙØ§Ø¡ المرÙقات" toggleTheme = "تبديل السÙمة" language = "اللغة" toggleAnnotations = "تبديل ظهور التعليقات التوضيحية" +toggleLayers = "تبديل الطبقات" search = "بحث ÙÙŠ PDF" panMode = "وضع السحب" applyRedactionsFirst = "طبّق التنقيحات أولًا" @@ -5407,20 +5827,72 @@ title = "طباعة ملÙ" 2 = "ادخل اسم الطابعة" [quickAccess] +access = "وصول" +accessAddPerson = "Ø¥Ø¶Ø§ÙØ© شخص آخر" +accessBack = "رجوع" +accessCopyLink = "نسخ الرابط" +accessEmail = "عنوان البريد الإلكتروني" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "ملÙ" +accessGeneral = "وصول عام" +accessInviteTitle = "دعوة أشخاص" +accessOwner = "المالك" +accessPanel = "وصول المستند" +accessPeople = "الأشخاص الذين لديهم وصول" +accessRemove = "إزالة" +accessRestricted = "مقيّد" +accessRestrictedHint = "لا يمكن Ø§Ù„ÙØªØ­ إلا لمن لديهم وصول" +accessRole = "الدور" +accessRoleCommenter = "معلّق" +accessRoleEditor = "محرر" +accessRoleViewer = "عارض" +accessSelectedFile = "المل٠المحدد" +accessSendInvite = "إرسال الدعوة" +accessTitle = "وصول المستند" +accessYou = "أنت" account = "الحساب" +activeSessions = "جلسات نشطة" +activeTab = "نشطة" activity = "النشاط" adminSettings = "إعدادات المشرÙ" +allSessions = "كل الجلسات" allTools = "كل الأدوات" automate = "أتمتة" +back = "رجوع" +certSign = "توقيع بالشهادة" +completedSessions = "جلسات مكتملة" +completedTab = "مكتملة" config = "الإعداد" +createNew = "إنشاء طلب جديد" +createSession = "إنشاء طلب توقيع" +dueDate = "تاريخ الاستحقاق (اختياري)" files = "Ø§Ù„Ù…Ù„ÙØ§Øª" help = "مساعدة" +noActiveSessions = "لا توجد طلبات توقيع معلّقة أو جلسات نشطة" +noCompletedSessions = "لا توجد جلسات مكتملة" +noFile = "لم يتم اختيار ملÙ" read = "قراءة" reader = "القارئ" +refresh = "تحديث" +requestSignatures = "طلب تواقيع" +selectSingleFileToRequest = "اختر مل٠PDF واحدًا لطلب التواقيع" +selectedFile = "المل٠المحدد" +selectUsers = "اختر مستخدمين للتوقيع" +selectUsersPlaceholder = "اختر المشاركين..." +sendingRequest = "جار٠الإرسال..." settings = "إعدادات" showMeAround = "أرني جولة" sign = "توقيع" +signatureRequests = "طلبات التوقيع" +signYourself = "وقّع Ø¨Ù†ÙØ³Ùƒ" +newRequest = "طلب جديد" tours = "جولات" +wetSign = "Ø¥Ø¶Ø§ÙØ© توقيع" +filterMine = "الخاصة بي" +filterOverdue = "متأخرة" +filterSigned = "موقّع" +filterDeclined = "مرÙوض" +searchDocuments = "ابحث ÙÙŠ المستندات…" [quickAccess.helpMenu] adminTour = "جولة المسؤول" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "خادم Stirling-PDF الخاص بك غير متصل expired = "لقد انتهت جلستك. يرجى تحديث Ø§Ù„ØµÙØ­Ø© والمحاولة مرة أخرى" refreshPage = "تحديث Ø§Ù„ØµÙØ­Ø©" +[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 = "ترتيب التوقيع" + +[signatureSettings.tooltip] +header = "إعدادات مظهر التوقيع" + +[signatureSettings.tooltip.location] +bullet1 = "أمثلة: \"New York, USA\"ØŒ \"London Office\"ØŒ \"Remote\"" +bullet2 = "ليست Ù†ÙØ³ موضع Ø§Ù„ØµÙØ­Ø©" +bullet3 = "قد يكون مطلوبًا ÙÙŠ بعض الولايات القضائية القانونية" +description = "موقع جغراÙÙŠ اختياري حيث تم تطبيق التوقيع. ÙŠÙØ®Ø²Ù‘Ù† ÙÙŠ بيانات الشهادة الوصÙية." +title = "موقع التوقيع" + +[signatureSettings.tooltip.logo] +bullet1 = "ÙŠÙØ¹Ø±Ø¶ بجانب التوقيع والنص" +bullet2 = "يدعم صيغ PNG ÙˆJPG" +bullet3 = "يعزّز المظهر الاحتراÙÙŠ" +description = "أض٠شعار الشركة إلى التواقيع المرئية للعلامة التجارية والمصداقية." +title = "شعار الشركة" + +[signatureSettings.tooltip.reason] +bullet1 = "أمثلة: \"Approval\"ØŒ \"Contract Agreement\"ØŒ \"Review Complete\"" +bullet2 = "يظهر ÙÙŠ خصائص توقيع PDF" +bullet3 = "Ù…Ùيد لسجلات التدقيق والامتثال" +description = "نص اختياري يشرح سبب توقيع المستند. ÙŠÙØ®Ø²Ù‘Ù† ÙÙŠ بيانات الشهادة الوصÙية." +title = "سبب التوقيع" + +[signatureSettings.tooltip.visibility] +bullet1 = "مرئي: يظهر التوقيع على مل٠PDF بمظهر مخصص" +bullet2 = "غير مرئي: ØªÙØ¶Ù…Ù‘ÙŽÙ† الشهادة دون علامة مرئية" +bullet3 = "لا تزال التواقيع غير المرئية توÙّر تحققًا تشÙيريًا" +description = "يتحكم Ùيما إذا كان التوقيع مرئيًا على المستند أو Ù…ÙØ¶Ù…ّنًا بشكل غير مرئي." +title = "ظهور التوقيع" + [settings.configuration] advanced = "متقدم" database = "قاعدة البيانات" endpoints = "نقاط النهاية" features = "الميزات" +storageSharing = "تخزين Ø§Ù„Ù…Ù„ÙØ§Øª والمشاركة" systemSettings = "إعدادات النظام" title = "التهيئة" @@ -6332,10 +6868,13 @@ title = "سجّل الدخول إلى Stirling" [setup.selfhosted] link = "أو الاتصال بحساب Ù…ÙØ³ØªØ¶Ø§Ù ذاتيًا" subtitle = "أدخل بيانات اعتماد الخادم" +changeServerLocked = "لقد قيّدت مؤسستك هذا التطبيق بخادم محدد" switchToLocal = "استخدام الأدوات المحلية بدلًا من ذلك" title = "سجّل الدخول إلى الخادم" [setup.selfhosted.unreachable] +changeServer = "الاتصال بخادم آخر" +changeServerLocked = "لقد قيّدت مؤسستك هذا التطبيق بخادم محدد" continueOffline = "استخدام الأدوات المحلية بدلًا من ذلك" message = "تعذّر الوصول إلى {{url}}. تحقّق من أن الخادم يعمل وقابل للوصول." retry = "إعادة المحاولة" @@ -6529,6 +7068,15 @@ saved = "محÙوظ" text = "نص" title = "نوع التوقيع" +[signRequest] +declined = "تم Ø±ÙØ¶ طلب التوقيع" +fetchFailed = "تعذّر تحميل طلب التوقيع" +signed = "تم توقيع المستند بنجاح" + +[signSession] +createFailed = "تعذّر إنشاء طلب التوقيع" +created = "تم إرسال طلب التوقيع" + [signup] accountCreatedSuccessfully = "تم إنشاء الحساب بنجاح! يمكنك الآن تسجيل الدخول." alreadyHaveAccount = "هل لديك حساب Ø¨Ø§Ù„ÙØ¹Ù„ØŸ سجّل الدخول" @@ -6807,6 +7355,106 @@ title = "تجزئة المستند حسب Ø§Ù„ÙØµÙˆÙ„" [splitPdfByChapters] tags = "تجزئة، ÙØµÙˆÙ„ØŒ علامات تبويب، تنظيم" +[storageShare] +accessed = "تم الوصول" +accessDenied = "ليس لديك حق الوصول إلى هذا المل٠المشترك. اطلب من المالك مشاركته معك." +accessFailed = "تعذّر تحميل النشاط." +accessDeniedBody = "ليس لديك حق الوصول إلى هذا الملÙ. اطلب من المالك مشاركته معك." +accessDeniedTitle = "لا وصول" +accessLimitedCommenter = "وصول التعليق قادم قريبًا. اطلب من المالك صلاحية المحرر إذا كنت بحاجة إلى التنزيل." +accessLimitedTitle = "وصول محدود" +accessLimitedViewer = "هذا الرابط للعرض Ùقط. اطلب من المالك صلاحية المحرر إذا كنت بحاجة إلى التنزيل." +createdAt = "تم الإنشاء" +download = "تنزيل" +downloadFailed = "تعذّر تنزيل هذا الملÙ." +expiredBody = "رابط المشاركة هذا غير صالح أو انتهت صلاحيته." +expiredTitle = "انتهت صلاحية الرابط" +goToLogin = "الانتقال إلى تسجيل الدخول" +loadFailed = "تعذّر ÙØªØ­ المل٠المشترك." +loading = "جار٠تحميل رابط المشاركة..." +loginPrompt = "سجّل الدخول للوصول إلى هذا المل٠المشترك." +loginRequired = "مطلوب تسجيل الدخول" +openInApp = "ÙØªØ­ ÙÙŠ Stirling PDF" +ownerLabel = "المالك" +ownerUnknown = "غير معروÙ" +requiresLogin = "هذا المل٠المشترك يتطلب تسجيل الدخول." +roleCommenter = "معلّق" +roleEditor = "محرر" +roleViewer = "عارض" +shareHeading = "مل٠مشترك" +titleDefault = "مل٠مشترك" +tryAgain = "يرجى المحاولة لاحقًا." +addUser = "Ø¥Ø¶Ø§ÙØ©" +commenterHint = "التعليق قادم قريبًا." +copied = "تم نسخ الرابط إلى Ø§Ù„Ø­Ø§ÙØ¸Ø©" +copy = "نسخ" +copyFailed = "ÙØ´Ù„ النسخ" +description = "أنشئ رابط مشاركة لهذا الملÙ. يمكن للمستخدمين المسجّلين الدخول والذين لديهم الرابط الوصول إليه." +downloadsCount = "عمليات التنزيل: {{count}}" +emailWarningBody = "يبدو هذا عنوان بريد إلكتروني. إذا لم يكن هذا الشخص مستخدم Stirling PDF Ø¨Ø§Ù„ÙØ¹Ù„ØŒ Ùلن يتمكن من الوصول إلى الملÙ." +emailWarningConfirm = "المشاركة على أي حال" +emailWarningTitle = "عنوان بريد إلكتروني" +errorTitle = "ÙØ´Ù„ت المشاركة" +failure = "تعذّر إنشاء رابط مشاركة. يرجى المحاولة مرة أخرى." +fileLabel = "الملÙ" +generate = "إنشاء رابط" +generated = "تم إنشاء رابط المشاركة" +hideActivity = "Ø¥Ø®ÙØ§Ø¡ النشاط" +invalidUsername = "أدخل اسم مستخدم أو عنوان بريد إلكتروني صالحًا." +lastAccessed = "آخر وصول" +linkAccessTitle = "وصول رابط المشاركة" +linkLabel = "رابط المشاركة" +linksDisabled = "روابط المشاركة معطّلة." +linksDisabledBody = "روابط المشاركة معطّلة بواسطة إعدادات الخادم." +manage = "إدارة المشاركة" +manageDescription = "إنشاء وإدارة روابط لمشاركة هذا الملÙ." +manageLoadFailed = "تعذّر تحميل روابط المشاركة." +manageTitle = "إدارة المشاركة" +noActivity = "لا يوجد نشاط بعد." +noLinks = "لا توجد روابط مشاركة نشطة بعد." +noSharedUsers = "لا يملك أي مستخدم وصولًا بعد." +removeLink = "إزالة الرابط" +removeUser = "إزالة" +revokeFailed = "تعذّر إزالة رابط المشاركة." +revoked = "تمت إزالة رابط المشاركة" +roleLabel = "الدور" +sharingDisabled = "المشاركة معطّلة." +sharingDisabledBody = "تم تعطيل المشاركة بواسطة إعدادات الخادم." +sharedUsersTitle = "المستخدمون الذين تمت المشاركة معهم" +title = "مشاركة الملÙ" +unknownUser = "مستخدم غير معروÙ" +userAddFailed = "يتعذر المشاركة مع هذا المستخدم." +userAdded = "تمت Ø¥Ø¶Ø§ÙØ© المستخدم إلى قائمة المشاركة." +usernameLabel = "اسم المستخدم أو البريد الإلكتروني" +usernamePlaceholder = "أدخل اسم مستخدم أو بريدًا إلكترونيًا" +userRemoveFailed = "يتعذر إزالة هذا المستخدم." +userRemoved = "تمت إزالة المستخدم من قائمة المشاركة." +viewActivity = "عرض النشاط" +viewed = "تمت المشاهدة" +viewsCount = "المشاهدات: {{count}}" +downloaded = "تم التنزيل" +bulkDescription = "أنشئ رابطًا واحدًا لمشاركة جميع Ø§Ù„Ù…Ù„ÙØ§Øª المحددة مع المستخدمين المسجّلين الدخول." +bulkTitle = "مشاركة Ø§Ù„Ù…Ù„ÙØ§Øª المحددة" +copyLink = "نسخ رابط المشاركة" +fileCount = "تم تحديد {{count}} ملÙًا" +ownerOnly = "يمكن للمالك Ùقط إدارة المشاركة." +selectSingleFile = "حدّد ملÙًا واحدًا لإدارة المشاركة." + +[storageUpload] +description = "يقوم هذا بتحميل المل٠الحالي إلى تخزين الخادم لاستخدامك الخاص." +errorTitle = "ÙØ´Ù„ التحميل" +failure = "ÙØ´Ù„ التحميل. يرجى التحقق من تسجيل الدخول وإعدادات التخزين." +fileLabel = "الملÙ" +hint = "تتحكم إعدادات الخادم ÙÙŠ الروابط العامة وأوضاع الوصول." +success = "تم التحميل إلى الخادم" +title = "التحميل إلى الخادم" +updateButton = "تحديث على الخادم" +uploadButton = "تحميل إلى الخادم" +bulkDescription = "يقوم هذا بتحميل Ø§Ù„Ù…Ù„ÙØ§Øª المحددة إلى تخزين الخادم." +bulkTitle = "تحميل Ø§Ù„Ù…Ù„ÙØ§Øª المحددة" +fileCount = "تم تحديد {{count}} ملÙًا" +more = " +{{count}} أخرى" + [storage] approximateSize = "الحجم التقريبي" fileTooLarge = "المل٠كبير جدًا. الحد الأقصى للحجم لكل مل٠هو" @@ -7153,6 +7801,30 @@ title = "عرض/تحرير PDF" [warning] tooltipTitle = "تحذير" +[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 = "تحميل صورة التوقيع" + [watermark] completed = "تمت Ø¥Ø¶Ø§ÙØ© العلامة المائية" desc = "أض٠علامات مائية نصية أو صورية إلى Ù…Ù„ÙØ§Øª PDF" @@ -7333,6 +8005,7 @@ activeSession = "جلسة نشطة" addMembers = "Ø¥Ø¶Ø§ÙØ© أعضاء" admin = "مسؤول" confirmDelete = "هل أنت متأكد أنك تريد حذ٠هذا المستخدم؟ لا يمكن التراجع عن هذا الإجراء." +confirmUnlock = "هل أنت متأكد من أنك تريد ÙØªØ­ Ù‚ÙÙ„ حساب هذا المستخدم؟" deleteUser = "حذ٠المستخدم" deleteUserError = "ÙØ´Ù„ حذ٠المستخدم" deleteUserSuccess = "تم حذ٠المستخدم بنجاح" @@ -7341,6 +8014,8 @@ disable = "تعطيل" disabled = "معطّل" editRole = "تحرير الدور" enable = "تمكين" +locked = "مقÙÙ„" +lockedBadge = "مقÙÙ„" loading = "جار٠تحميل الأشخاص..." loginRequired = "ÙØ¹Ù‘Ù„ وضع تسجيل الدخول أولاً" member = "عضو" @@ -7350,6 +8025,9 @@ searchMembers = "ابحث عن الأعضاء..." status = "الحالة" team = "Ø§Ù„ÙØ±ÙŠÙ‚" title = "الأشخاص" +unlockAccount = "ÙØªØ­ Ù‚ÙÙ„ الحساب" +unlockUserError = "ÙØ´Ù„ ÙØªØ­ Ù‚ÙÙ„ حساب المستخدم" +unlockUserSuccess = "تم ÙØªØ­ Ù‚ÙÙ„ حساب المستخدم بنجاح" user = "مستخدم" [workspace.people.actions] diff --git a/frontend/public/locales/az-AZ/translation.toml b/frontend/public/locales/az-AZ/translation.toml index 3e33ecb6be..67972e61a9 100644 --- a/frontend/public/locales/az-AZ/translation.toml +++ b/frontend/public/locales/az-AZ/translation.toml @@ -8,6 +8,7 @@ black = "Qara" blue = "Mavi" bored = "GözlÉ™mÉ™kdÉ™n Sıxıldınız?" cancel = "Ləğv et" +confirm = "TÉ™sdiqlÉ™" changedCredsMessage = "EtibarnamÉ™lÉ™r dÉ™yiÅŸdirildi!" chooseFile = "Fayl seç" close = "BaÄŸla" @@ -146,6 +147,7 @@ insufficientCredits = "KreditlÉ™r kifayÉ™t deyil. TÉ™lÉ™b olunur: {{requiredCred loadingCredits = "KreditlÉ™r yoxlanılır..." loadingProStatus = "AbunÉ™lik statusu yoxlanılır..." noticeTopUpOrPlan = "KreditlÉ™r kifayÉ™t deyil, xahiÅŸ edirik balansı artırın vÉ™ ya planı yüksÉ™ldin" +accessInvite = "DÉ™vÉ™t et" [account] accountSettings = "Hesab ParametrlÉ™ri" @@ -1427,6 +1429,34 @@ title = "Emal" description = "XÉ™tanı bildirmÉ™zdÉ™n É™vvÉ™l emal iÅŸini gözlÉ™mÉ™k üçün maksimum vaxt." label = "Emal vaxt limiti (saniyÉ™)" +[admin.settings.storage] +description = "Server yaddaşını vÉ™ paylaÅŸma seçimlÉ™rini idarÉ™ edin." +title = "Fayl Saxlama vÉ™ PaylaÅŸma" + +[admin.settings.storage.enabled] +description = "İstifadəçilÉ™rÉ™ faylları serverdÉ™ saxlamaÄŸa icazÉ™ verin." +label = "ServerdÉ™ Fayl Saxlamanı Aktiv et" + +[admin.settings.storage.sharing.email] +description = "E-poçt ünvanları ilÉ™ paylaÅŸmaÄŸa icazÉ™ verin." +label = "E-poçt ilÉ™ PaylaÅŸmanı Aktiv et" +mailLink = "Poçt TÉ™nzimlÉ™mÉ™lÉ™rini Qur" +mailNote = "Poçt konfiqurasiyası tÉ™lÉ™b olunur. " + +[admin.settings.storage.sharing.enabled] +description = "İstifadəçilÉ™rÉ™ saxlanan faylları paylaÅŸmaÄŸa icazÉ™ verin." +label = "PaylaÅŸmanı Aktiv et" + +[admin.settings.storage.sharing.links] +description = "GiriÅŸ tÉ™lÉ™b edÉ™n linklÉ™rlÉ™ paylaÅŸmaÄŸa icazÉ™ verin." +frontendUrlLink = "Sistem TÉ™nzimlÉ™mÉ™lÉ™rindÉ™ qur" +frontendUrlNote = "Frontend URL tÉ™lÉ™b olunur. " +label = "PaylaÅŸma LinklÉ™rini Aktiv et" + +[admin.settings.storage.signing.enabled] +description = "İstifadəçilÉ™rÉ™ çox iÅŸtirakçılı sÉ™nÉ™d imzalama sessiyaları yaratmaÄŸa icazÉ™ verin. Server fayl saxlaması aktiv olmalıdır." +label = "Qrup İmzalanmasını Aktiv et (Alfa)" + [admin.settings.unsavedChanges] cancel = "RedaktÉ™yÉ™ davam et" discard = "DÉ™yiÅŸikliklÉ™ri ləğv et" @@ -2059,7 +2089,19 @@ numbers = "NömrÉ™lÉ™r/aralıqlar: 5, 10-20" progressions = "Proqressiyalar: 3n, 4n+1" [certSign] +allSigned = "Bütün iÅŸtirakçılar imzalayıb. YekunlaÅŸdırmaÄŸa hazırdır." +awaitingSignatures = "İmzalar gözlÉ™nir" +signatureProgress = "{{signedCount}}/{{totalCount}} imza" chooseCertificate = "Sertifikat faylını seçin" +declined = "İmtina edildi" +fetchFailed = "İmzalama mÉ™lumatlarını yüklÉ™mÉ™k mümkün olmadı" +finalized = "YekunlaÅŸdırılıb" +notified = "GözlÉ™mÉ™dÉ™" +partialNote = "Mövcud imzalarla erkÉ™n yekunlaÅŸdıra bilÉ™rsiniz. İmzalamayan iÅŸtirakçılar çıxarılacaq." +pending = "GözlÉ™mÉ™dÉ™" +readyToFinalize = "YekunlaÅŸdırmaÄŸa hazır" +signed = "İmzalanıb" +viewed = "Baxılıb" chooseJksFile = "JKS faylını seçin" chooseP12File = "PKCS12 faylını seçin" choosePfxFile = "PFX faylını seçin" @@ -2082,6 +2124,7 @@ title = "Sertifikatla İmzala" invisible = "GörünmÉ™z" stepTitle = "İmza görünüşü" visible = "GörünÉ™n" +visibility = "Görünürlük" [certSign.appearance.options] title = "İmza detalları" @@ -2188,6 +2231,252 @@ 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ı" +[certSign.collab.finalize] +button = "YekunlaÅŸdır vÉ™ İmzalanmış PDF-i YüklÉ™" +early = "Mövcud imzalarla yekunlaÅŸdır" + +[certSign.collab.sessionDetail] +addButton = "İştirakçı É™lavÉ™ et" +addParticipants = "İştirakçı É™lavÉ™ et" +addParticipantsError = "İştirakçıları É™lavÉ™ etmÉ™k mümkün olmadı" +backToList = "Sessiyalara qayıt" +deleteConfirm = "Æminsiniz? Bu É™mÉ™liyyatı geri qaytarmaq olmaz." +deleteError = "Sessiyanı silmÉ™k mümkün olmadı" +deleted = "Sessiya silindi" +deleteSession = "Sessiyanı sil" +dueDate = "Son tarix" +finalizeError = "Sessiyanı yekunlaÅŸdırmaq mümkün olmadı" +loadPdfError = "İmzalanmış PDF-i yüklÉ™mÉ™k mümkün olmadı" +loadSignedPdf = "İmzalanmış PDF-i Aktiv Fayllara YüklÉ™" +messageLabel = "Mesaj" +noAdditionalInfo = "ÆlavÉ™ mÉ™lumat yoxdur" +owner = "Sahib" +participantRemoved = "İştirakçı silindi" +participants = "İştirakçılar" +participantsAdded = "İştirakçılar uÄŸurla É™lavÉ™ olundu" +removeParticipant = "Sil" +removeParticipantError = "İştirakçını silmÉ™k mümkün olmadı" +selectUsers = "İstifadəçilÉ™ri seçin..." +sessionInfo = "Sessiya mÉ™lumatı" +workbenchTitle = "Sessiya İdarÉ™etmÉ™si" + +[certSign.collab.signRequest] +addedToFiles = "SÉ™nÉ™d aktiv fayllara É™lavÉ™ olundu" +addSignature = "İmzanızı É™lavÉ™ edin" +addToFiles = "Aktiv Fayllara ÆlavÉ™ et" +advancedSettings = "Ætraflı tÉ™nzimlÉ™mÉ™lÉ™r" +backToList = "İmza SorÄŸularına qayıt" +certificateChoice = "İmzalamaq üçün sertifikat seçin" +changeSignature = "İmzanı dÉ™yiÅŸ" +clearSignature = "İmzanı sil" +completeAndSign = "Tamamla vÉ™ İmzala" +createNewSignature = "Yeni İmza Yarat" +declineButton = "İmtina et" +decline = "SorÄŸudan imtina et" +deleteSelected = "SeçilmiÅŸ imzanı sil" +drawSignature = "AÅŸağıda imzanızı çəkin" +dueDate = "Son tarix" +fileTooLarge = "Fayl ölçüsü 5MB-dan az olmalıdır" +fontFamily = "Åžrift ailÉ™si" +fontSize = "Åžrift ölçüsü: {{size}}px" +fontSizePlaceholder = "Ölçü" +from = "KimdÉ™n" +invalidCertFile = "ZÉ™hmÉ™t olmasa P12 vÉ™ ya PFX sertifikat faylı seçin" +invalidFileType = "ZÉ™hmÉ™t olmasa ÅŸÉ™kil faylı seçin" +location = "MÉ™kan (Seçim üzrÉ™)" +locationPlaceholder = "Haradan imzalayırsınız?" +message = "Mesaj" +noCertificate = "ZÉ™hmÉ™t olmasa sertifikat faylı seçin" +noSignatures = "ZÉ™hmÉ™t olmasa PDF üzÉ™rinÉ™ É™n azı bir imza yerləşdirin" +p12File = "P12/PFX Sertifikat Faylı" +password = "Sertifikat ÅŸifrÉ™si" +passwordPlaceholder = "ÅžifrÉ™ni daxil edin..." +penColor = "QÉ™lÉ™m rÉ™ngi" +penSize = "QÉ™lÉ™m ölçüsü: {{size}}px" +placementActive = "YerləşdirmÉ™k üçün PDF-É™ kliklÉ™yin" +placeSignatureButton = "İmzanı PDF üzÉ™rinÉ™ yerləşdir" +reason = "SÉ™bÉ™b (Seçim üzrÉ™)" +reasonPlaceholder = "NiyÉ™ imzalayırsınız?" +removeImage = "Şəkli sil" +removeCertFile = "Faylı sil" +savedSignatures = "Yadda saxlanılan imzalar" +selectFile = "Şəkil faylı seçin" +selectSignatureTitle = "İmza seçin vÉ™ ya yaradın" +signButton = "SÉ™nÉ™di imzala" +signatureInfo = "Bu tÉ™nzimlÉ™mÉ™lÉ™r sÉ™nÉ™d sahibi tÉ™rÉ™findÉ™n qurulub" +signaturePlaced = "İmza sÉ™hifÉ™yÉ™ yerləşdirildi" +signatureSettings = "İmza tÉ™nzimlÉ™mÉ™lÉ™ri" +signatureText = "İmza mÉ™tni" +signatureTextPlaceholder = "Adınızı daxil edin..." +signatureTypeLabel = "İmza növü" +signingTitle = "İmzalama" +textColor = "MÉ™tn rÉ™ngi" +typeSignature = "İmza yaratmaq üçün adınızı yazın" +uploadCert = "FÉ™rdi sertifikat" +uploadCertDesc = "Öz P12/PFX sertifikatınızdan istifadÉ™ edin" +uploadSignature = "İmzanızın ÅŸÉ™klini yüklÉ™yin" +usePersonalCert = "Şəxsi sertifikat" +usePersonalCertDesc = "Hesabınız üçün avtomatik yaradılır" +useServerCert = "Təşkilat sertifikatı" +useServerCertDesc = "Paylaşılan təşkilat sertifikatı" +workbenchTitle = "İmza SorÄŸusu" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "XÉ™tt rÉ™ngini seçin" +continue = "Davam et" + +[certSign.collab.signRequest.certModal] +description = "{{count}} imza yerləşdirmisiniz. İmzalamayı tamamlamak üçün sertifikatınızı seçin." +sign = "SÉ™nÉ™di imzala" +certValidating = "Sertifikat yoxlanılır..." +certValidUntil = "Sertifikatın qüvvÉ™ müddÉ™ti {{date}} tarixÉ™dÉ™k" +certInvalid = "Sertifikat etibarsızdır: {{error}}" +certInvalidFallback = "Etibarsız sertifikat" +certNetworkError = "Sertifikatı yoxlamaq mümkün olmadı" +title = "Sertifikatı tÉ™nzimlÉ™" + +[certSign.collab.signRequest.image] +hint = "İmzanızın PNG vÉ™ ya JPG ÅŸÉ™klini yüklÉ™yin" + +[certSign.collab.signRequest.mode] +move = "İmzanı hÉ™rÉ™kÉ™t etdir" +place = "İmzanı yerləşdir" +title = "İmzalama vÉ™ ya hÉ™rÉ™kÉ™t etdirmÉ™ rejimi" + +[certSign.collab.signRequest.modeTabs] +draw = "Çək" +image = "YüklÉ™" +text = "Yaz" + +[certSign.collab.signRequest.placeSignature] +message = "İmzanızı yerləşdirmÉ™k üçün PDF-É™ kliklÉ™yin" +title = "İmzanı yerləşdir" + +[certSign.collab.signRequest.preview] +imageAlt = "SeçilmiÅŸ imza" +missing = "Ön baxış yoxdur" +textFallback = "İmza" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "ÇəkilmiÅŸ imza" +defaultImageLabel = "YüklÉ™nmiÅŸ imza" +defaultLabel = "İmza" +defaultTextLabel = "Yazılmış imza" +delete = "İmzanı sil" +none = "Yadda saxlanmış imza yoxdur" + +[certSign.collab.signRequest.signatureType] +draw = "Çək" +type = "Yaz" +upload = "YüklÉ™" + +[certSign.collab.signRequest.steps] +back = "Geri" +cancelPlacement = "YerləşdirmÉ™ni ləğv et" +certificate = "Sertifikat" +clickMultipleTimes = "İmzaları yerləşdirmÉ™k üçün PDF-É™ bir neçə dÉ™fÉ™ kliklÉ™yin. HÉ™r hansı imzanı daşıyaraq yerini dÉ™yiÅŸin vÉ™ ya ölçülÉ™ndirin." +clickToPlace = "İmzanızın görünmÉ™sini istÉ™diyiniz yerÉ™ PDF üzÉ™rindÉ™ kliklÉ™yin." +continue = "Sertifikat seçiminÉ™ davam et" +continueToPlacement = "YerləşdirmÉ™yÉ™ davam et" +continueToReview = "İcmala davam et" +createSignature = "İmza yarat" +invisible = "GörünmÉ™z" +location = "MÉ™kan:" +multipleSignatures = "{{count}} imza PDF-É™ tÉ™tbiq olunacaq" +oneSignature = "PDF-É™ 1 imza tÉ™tbiq olunacaq" +placeOnPdf = "PDF üzÉ™rinÉ™ yerləşdir" +reason = "SÉ™bÉ™b:" +reviewTitle = "İmzalamadan É™vvÉ™l baxış" +signaturePlaced = "İmza {{page}}-ci sÉ™hifÉ™yÉ™ yerləşdirildi. YenidÉ™n kliklÉ™yÉ™rÉ™k yerini düzÉ™ldÉ™ vÉ™ ya icmala davam edÉ™ bilÉ™rsiniz." +visible = "GörünÉ™n" +visibility = "Görünürlük:" +yourSignatures = "İmzalarınız ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "RÉ™ng" +fontLabel = "Åžrift" +fontSizeLabel = "Ölçü" +fontSizePlaceholder = "16" +label = "İmza mÉ™tni" +modalHint = "Adınızı daxil edin, sonra PDF üzÉ™rindÉ™ yerləşdirmÉ™k üçün Davam et düymÉ™sini kliklÉ™yin." +placeholder = "Adınızı daxil edin..." + +[certSign.collab.participant] +certValidating = "Sertifikat yoxlanılır..." +certValid = "✓ Sertifikat etibarlıdır" +certValidUntil = " {{date}} tarixinÉ™dÉ™k" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Etibarsız sertifikat" +certNetworkError = "Sertifikatı yoxlamaq mümkün olmadı" + +[certSign.collab.addParticipants] +add = "{{count}} iÅŸtirakçı É™lavÉ™ et" +back = "Geri" +configureSignatures = "İmza tÉ™nzimlÉ™mÉ™lÉ™rini tÉ™nzimlÉ™" +continue = "İmza tÉ™nzimlÉ™mÉ™lÉ™rinÉ™ davam et" +reasonHelp = "Bu iÅŸtirakçılar üçün imzalama sÉ™bÉ™bini É™vvÉ™lcÉ™dÉ™n tÉ™yin edin (seçim üzrÉ™, imzalayarkÉ™n dÉ™yiÅŸÉ™ bilÉ™rlÉ™r)" +reasonPlaceholder = "mÉ™s., TÉ™sdiq, İcmal..." +selectUsers = "İstifadəçilÉ™ri seçin" + +[certSign.collab.sessionCreation] +includeSummaryPage = "İmza xülasÉ™ sÉ™hifÉ™sini daxil et" +includeSummaryPageHelp = "Sonda bütün imza metadata-sı ilÉ™ xülasÉ™ sÉ™hifÉ™si É™lavÉ™ olunacaq. Ayrı-ayrı sÉ™hifÉ™lÉ™rdÉ™ki rÉ™qÉ™msal sertifikat imza çərçivÉ™lÉ™ri gizlÉ™dilÉ™cÉ™k (É™l ilÉ™ imzalar tÉ™sirlÉ™nmir)." + +[certSign.collab.sessionList] +active = "Aktiv" +finalized = "YekunlaÅŸdırılıb" + +[certSign.collab.signatureSettings] +description = "İmzaların bütün iÅŸtirakçılar üçün necÉ™ görünÉ™cÉ™yini tÉ™nzimlÉ™yin" +title = "İmzanın görünüşü" + +[certSign.collab.userSelector] +inviteUsers = "İstifadəçi É™lavÉ™ et" +loadError = "İstifadəçilÉ™ri yüklÉ™mÉ™k mümkün olmadı" +noTeam = "Komanda yoxdur" +noUsers = "BaÅŸqa istifadəçi tapılmadı." +placeholder = "İstifadəçilÉ™ri seçin..." + +[certSign.mobile] +panelActions = "FÉ™aliyyÉ™tlÉ™r" +panelDocument = "SÉ™nÉ™d" +panelPeople = "ŞəxslÉ™r" + +[certSign.sessions] +deleted = "Sessiya silindi" +fetchFailed = "Sessiya detalları yüklÉ™nÉ™ bilmÉ™di" +finalized = "Sessiya yekunlaÅŸdırıldı" +loaded = "İmzalanmış PDF yüklÉ™ndi" +pdfNotReady = "PDF Hazır Deyil" +pdfNotReadyDesc = "İmzalanmış PDF yaradılır. ZÉ™hmÉ™t olmasa bir qÉ™dÉ™r sonra yenidÉ™n cÉ™hd edin." + +[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É™" + [changeCreds] changePassword = "Siz standart giriÅŸ mÉ™lumatlarından istifadÉ™ edirsiniz. ZÉ™hmÉ™t olmasa, yeni ÅŸifr daxil edin" changeUsername = "İstifadəçi adınızı yenilÉ™yin. YenilÉ™dikdÉ™n sonra çıxış edilÉ™cÉ™ksiniz." @@ -3242,6 +3531,46 @@ totalSelected = "CÉ™mi seçilib" unsupported = "DÉ™stÉ™klÉ™nmir" unzip = "ArxivdÉ™n çıxar" uploadError = "BÉ™zi faylları yüklÉ™mÉ™k alınmadı." +copyCreated = "SurÉ™t bu cihazda saxlandı." +copyFailed = "SurÉ™t yaratmaq mümkün olmadı." +leaveShare = "Siyahımdan çıxar" +leaveShareFailed = "Paylaşılan faylı siyahıdan çıxarmaq mümkün olmadı." +leaveShareSuccess = "Paylaşılan siyahınızdan çıxarıldı." +removeBoth = "HÉ™r ikisindÉ™n sil" +removeFilePrompt = "Bu fayl bu cihazda vÉ™ serverinizdÉ™ saxlanılır. Hansından silmÉ™k istÉ™yirsiniz?" +removeFileTitle = "Faylı sil" +removeLocalOnly = "Yalnız bu cihazda" +removeServerFailed = "Faylı serverdÉ™n silmÉ™k mümkün olmadı." +removeServerOnly = "Yalnız serverdÉ™" +removeServerOnlyPrompt = "Bu fayl yalnız serverinizdÉ™ saxlanılır. ServerdÉ™n silmÉ™k istÉ™yirsiniz?" +removeServerSuccess = "ServerdÉ™n silindi." +removeSharedPrompt = "Bu fayl sizinlÉ™ paylaşılıb. Onu bu cihazdan vÉ™ ya paylaşılan siyahınızdan silÉ™ bilÉ™rsiniz." +removeSharedServerOnlyBlockedPrompt = "Bu fayl sizinlÉ™ paylaşılıb vÉ™ yalnız serverdÉ™ saxlanılır." +removeSharedServerOnlyPrompt = "Bu fayl sizinlÉ™ paylaşılıb vÉ™ yalnız serverdÉ™ saxlanılır. Siyahınızdan çıxarmaq istÉ™yirsiniz?" +changesNotUploaded = "DÉ™yiÅŸikliklÉ™r yüklÉ™nmÉ™yib" +cloudFile = "Bulud faylı" +filterAll = "Hamısı" +filterLocal = "Lokal" +filterSharedByMe = "MÉ™nim paylaÅŸdıqlarım" +filterSharedWithMe = "MÉ™nimlÉ™ paylaşılanlar" +lastSynced = "Son sinxronlaÅŸdırma" +localOnly = "Yalnız lokal" +makeCopy = "SurÉ™t yarat" +owner = "Sahib" +ownerUnknown = "NamÉ™lum" +share = "PaylaÅŸ" +shareSelected = "SeçilÉ™nlÉ™ri paylaÅŸ" +sharedByYou = "Sizin paylaÅŸdıqlarınız" +sharedEditNoticeBody = "Bu faylın server versiyasını redaktÉ™ etmÉ™ hüququnuz yoxdur. Etdiyiniz dÉ™yiÅŸikliklÉ™r lokal surÉ™t kimi saxlanılacaq." +sharedEditNoticeConfirm = "BaÅŸa düşdüm" +sharedEditNoticeTitle = "Server nüsxÉ™si yalnız oxumaq üçündür" +sharedWithYou = "SizinlÉ™ paylaşılanlar" +sharing = "PaylaÅŸma" +storageState = "Saxlama" +synced = "SinxronlaÅŸdırılıb" +updateOnServer = "ServerdÉ™ yenilÉ™" +uploadSelected = "SeçilÉ™nlÉ™ri yüklÉ™" +uploadToServer = "ServerÉ™ yüklÉ™" [files] addFiles = "Fayllar É™lavÉ™ et" @@ -3367,6 +3696,77 @@ title = "PDF-lÉ™rin yastılaÅŸdırılması haqqında" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Qrup imzalanması haqqında" + +[groupSigning.tooltip.finalization] +bullet1 = "Bütün imzalar göstÉ™rdiyiniz iÅŸtirakçı ardıcıllığına görÉ™ tÉ™tbiq olunur" +bullet2 = "Lazım olsa qismÉ™n imzalarla yekunlaÅŸdıra bilÉ™rsiniz" +bullet3 = "YekunlaÅŸdırıldıqdan sonra sessiya dÉ™yiÅŸdirilÉ™ bilmÉ™z" +description = "Bütün iÅŸtirakçılar imzaladıqdan sonra (vÉ™ ya erkÉ™n yekunlaÅŸdırma seçsÉ™niz) yekun imzalanmış PDF-i yarada bilÉ™rsiniz." +title = "YekunlaÅŸdırma Prosesi" + +[groupSigning.tooltip.roles] +bullet1 = "Sahib (siz): Sessiya yaradır, imza standartlarını qurur, sÉ™nÉ™di yekunlaÅŸdırır" +bullet2 = "İştirakçılar: Öz imzalarını yaradır, sertifikatı seçir, PDF üzÉ™rinÉ™ yerləşdirir" +bullet3 = "İştirakçılar imzanın görünürlüyü, sÉ™bÉ™b vÉ™ mÉ™kan tÉ™nzimlÉ™mÉ™lÉ™rini dÉ™yiÅŸÉ™ bilmÉ™z" +description = "Bütün iÅŸtirakçılar üçün imza görünüşü tÉ™nzimlÉ™mÉ™lÉ™rinÉ™ siz nÉ™zarÉ™t edirsiniz." +title = "İştirakçı Rolları" + +[groupSigning.tooltip.sequential] +bullet1 = "İkinci ÅŸÉ™xs sÉ™nÉ™dÉ™ çıxmadan É™vvÉ™l birinci iÅŸtirakçı imzalamalıdır" +bullet2 = "Hüquqi uyÄŸunluq üçün düzgün imzalama sırasını tÉ™min edir" +bullet3 = "İştirakçıları siyahıda sürüklÉ™yÉ™rÉ™k sıralamanı dÉ™yiÅŸÉ™ bilÉ™rsiniz" +description = "İştirakçılar sÉ™nÉ™dlÉ™ri sizin verdiyiniz ardıcıllıqla imzalayır. NövbÉ™si çatdıqda hÉ™r imzalayana bildiriÅŸ göndÉ™rilir." +title = "Ardıcıllıqla İmzalama" + +[groupSigning.steps] +back = "Geri" +completed = "Tamamlandı" +current = "Cari" +stepLabel = "Addım {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "İcmala davam et" +invisible = "İmzalar görünmÉ™yÉ™cÉ™k (yalnız metadata)" +locationLabel = "MÉ™kan:" +preview = "Ön baxış" +reasonLabel = "SÉ™bÉ™b:" +title = "İmza tÉ™nzimlÉ™mÉ™lÉ™rini tÉ™nzimlÉ™" +visible = "İmzalar {{page}}-ci sÉ™hifÉ™dÉ™ görünÉ™cÉ™k" + +[groupSigning.steps.review] +document = "SÉ™nÉ™d" +dueDate = "Son tarix (Seçim üzrÉ™)" +dueDatePlaceholder = "Son tarixi seçin..." +invisible = "GörünmÉ™z (yalnız metadata)" +location = "MÉ™kan:" +logo = "Loqo:" +logoHidden = "Loqo yoxdur" +logoShown = "Stirling PDF loqosu göstÉ™rilir" +participants = "İştirakçılar" +reason = "SÉ™bÉ™b:" +send = "İmzalama SorÄŸularını GöndÉ™r" +signatureSettings = "İmza tÉ™nzimlÉ™mÉ™lÉ™ri" +title = "Sessiya Detalları İcmalı" +titleShort = "İcmal vÉ™ GöndÉ™r" +visibility = "Görünürlük:" +visible = "{{page}}-ci sÉ™hifÉ™dÉ™ görünÉ™n" +participantCount = "{{count}} iÅŸtirakçı ardıcıllıqla imzalayacaq" + +[groupSigning.steps.selectDocument] +continue = "İştirakçı seçiminÉ™ davam et" +noFile = "İmzalama sessiyası yaratmaq üçün aktiv fayllarınızdan tÉ™k bir PDF faylı seçin." +selectedFile = "SeçilmiÅŸ sÉ™nÉ™d" +title = "SÉ™nÉ™di seçin" + +[groupSigning.steps.selectParticipants] +continue = "İmza tÉ™nzimlÉ™mÉ™lÉ™rinÉ™ davam et" +count = "{{count}} iÅŸtirakçı seçildi" +label = "İştirakçıları seçin" +placeholder = "İştirakçıları imzalamaq üçün seçin..." +title = "İştirakçıları seçin" + [getPdfInfo] downloadJson = "JSON yüklÉ™" downloads = "YüklÉ™mÉ™lÉ™r" @@ -4460,7 +4860,10 @@ zoomOut = "Kiçilt" [viewer] cannotPreviewFile = "Faylın önizlÉ™nmÉ™si mümkün deyil" +disableColorFilter = "RÉ™ng filtrini söndür" dualPageView = "İki SÉ™hifÉ™ Görünüşü" +enableDarkFilter = "Qaranlıq filtrini aktiv et" +enableSepiaFilter = "Sepiya filtrini aktiv et" firstPage = "Birinci sÉ™hifÉ™" lastPage = "Son sÉ™hifÉ™" nextPage = "NövbÉ™ti sÉ™hifÉ™" @@ -4470,6 +4873,22 @@ singlePageView = "TÉ™k SÉ™hifÉ™ Görünüşü" unknownFile = "NamÉ™lum fayl" zoomIn = "Böyüt" zoomOut = "Kiçilt" +resetZoom = "Miqyası sıfırla" + +[viewer.nonPdf] +fileTypeBadge = "{{type}} Faylı" +convertToPdf = "PDF-É™ çevir" +loading = "YüklÉ™nir..." +emptyFile = "BoÅŸ fayl" +csvStats = "{{rows}} sÉ™tir · {{columns}} sütun · {{size}}" +sortedBy = "Sıralanıb: {{column}}" +columnDefault = "Sütun {{index}}" +htmlPreviewWarning = "HTML ön baxış — xarici resurslar yüklÉ™nmÉ™yÉ™ bilÉ™r · {{size}}" +htmlPreview = "HTML ön baxış" +invalidJson = "Etibarsız JSON — xam mÉ™zmun göstÉ™rilir" +textStats = "{{lines}} sÉ™tir · {{size}}" +lineNumbers = "SÉ™tir nömrÉ™lÉ™ri" +renderMarkdown = "Markdown-u göstÉ™r" [viewer.attachments] title = "QoÅŸmalar" @@ -4531,6 +4950,7 @@ toggleAttachments = "QoÅŸmaları göstÉ™r/gizlÉ™t" toggleTheme = "Mövzunu dÉ™yiÅŸ" language = "Dil" toggleAnnotations = "Annotasiyaların görünmÉ™sini dÉ™yiÅŸ" +toggleLayers = "Qatları aç/baÄŸla" search = "PDF-dÉ™ axtar" panMode = "SürüşdürmÉ™ rejimi" applyRedactionsFirst = "ÆvvÉ™lcÉ™ mÉ™xfiləşdirmÉ™lÉ™ri tÉ™tbiq edin" @@ -5407,20 +5827,72 @@ title = "Faylı çap edin" 2 = "Printer adını daxil edin" [quickAccess] +access = "GiriÅŸ" +accessAddPerson = "BaÅŸqa ÅŸÉ™xs É™lavÉ™ et" +accessBack = "Geri" +accessCopyLink = "Linki kopyala" +accessEmail = "E-poçt ünvanı" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Fayl" +accessGeneral = "Ümumi giriÅŸ" +accessInviteTitle = "İnsanları dÉ™vÉ™t et" +accessOwner = "Sahib" +accessPanel = "SÉ™nÉ™dÉ™ giriÅŸ" +accessPeople = "GiriÅŸi olanlar" +accessRemove = "Sil" +accessRestricted = "MÉ™hdudlaÅŸdırılıb" +accessRestrictedHint = "Yalnız giriÅŸi olanlar aça bilÉ™r" +accessRole = "Rol" +accessRoleCommenter = "Şərhçi" +accessRoleEditor = "Redaktor" +accessRoleViewer = "İzlÉ™yici" +accessSelectedFile = "SeçilmiÅŸ fayl" +accessSendInvite = "DÉ™vÉ™t göndÉ™r" +accessTitle = "SÉ™nÉ™dÉ™ giriÅŸ" +accessYou = "Siz" account = "Hesab" +activeSessions = "Aktiv sessiyalar" +activeTab = "Aktiv" activity = "Aktivlik" adminSettings = "Admin Ayarları" +allSessions = "Bütün sessiyalar" allTools = "All Tools" automate = "Auto" +back = "Geri" +certSign = "Sertifikatla imzalama" +completedSessions = "Tamamlanmış sessiyalar" +completedTab = "Tamamlanmış" config = "Konfiq" +createNew = "Yeni sorÄŸu yarat" +createSession = "İmzalama sorÄŸusu yarat" +dueDate = "Son tarix (seçim üzrÉ™)" files = "Fayllar" help = "KömÉ™k" +noActiveSessions = "GözlÉ™yÉ™n imza sorÄŸusu vÉ™ ya aktiv sessiya yoxdur" +noCompletedSessions = "Tamamlanmış sessiya yoxdur" +noFile = "Fayl seçilmÉ™yib" read = "Oxu" reader = "Oxuyucu" +refresh = "YenilÉ™" +requestSignatures = "İmzalar tÉ™lÉ™b et" +selectSingleFileToRequest = "İmzalar istÉ™mÉ™k üçün tÉ™k bir PDF faylı seçin" +selectedFile = "SeçilmiÅŸ fayl" +selectUsers = "İmzalama üçün istifadəçilÉ™ri seçin" +selectUsersPlaceholder = "İştirakçıları seçin..." +sendingRequest = "GöndÉ™rilir..." settings = "Ayarlar" showMeAround = "MÉ™ni gÉ™zdir" sign = "İmzala" +signatureRequests = "İmza sorÄŸuları" +signYourself = "Özünüz imzalayın" +newRequest = "Yeni sorÄŸu" tours = "Turlar" +wetSign = "İmza É™lavÉ™ et" +filterMine = "MÉ™nim" +filterOverdue = "MüddÉ™ti keçmiÅŸ" +filterSigned = "İmzalanıb" +filterDeclined = "İmtina edilib" +searchDocuments = "SÉ™nÉ™dlÉ™rdÉ™ axtar…" [quickAccess.helpMenu] adminTour = "Admin Turu" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Sizin Stirling-PDF serveriniz oflayndır vÉ™ \"{{endp expired = "Sessiyanızın vaxtı bitdi. SÉ™hifÉ™ni yenilÉ™yin vÉ™ yenidÉ™n cÉ™hd edin." refreshPage = "SÉ™hifÉ™ni YenilÉ™" +[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ı" + +[signatureSettings.tooltip] +header = "İmzanın Görünüşü TÉ™nzimlÉ™mÉ™lÉ™ri" + +[signatureSettings.tooltip.location] +bullet1 = "NümunÉ™lÉ™r: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "SÉ™hifÉ™dÉ™ki mövqedÉ™n fÉ™rqlidir" +bullet3 = "BÉ™zi hüquqi yurisdiksiyalar üçün tÉ™lÉ™b oluna bilÉ™r" +description = "İmzanın tÉ™tbiq olunduÄŸu ixtiyari coÄŸrafi mÉ™kan. Sertifikat metadata-sında saxlanılır." +title = "İmza MÉ™kanı" + +[signatureSettings.tooltip.logo] +bullet1 = "İmza vÉ™ mÉ™tnlÉ™ yanaşı göstÉ™rilir" +bullet2 = "PNG, JPG formatlarını dÉ™stÉ™klÉ™yir" +bullet3 = "PeÅŸÉ™kar görünüşü artırır" +description = "Brendinq vÉ™ autentiklik üçün görünÉ™n imzalara ÅŸirkÉ™t loqosu É™lavÉ™ edin." +title = "ÅžirkÉ™t Loqosu" + +[signatureSettings.tooltip.reason] +bullet1 = "NümunÉ™lÉ™r: \"Approval\", \"Contract Agreement\", \"Review Complete\"" +bullet2 = "PDF imza xüsusiyyÉ™tlÉ™rindÉ™ görünür" +bullet3 = "Audit izlÉ™ri vÉ™ uyÄŸunluq üçün faydalıdır" +description = "SÉ™nÉ™din niyÉ™ imzalandığını izah edÉ™n ixtiyari mÉ™tn. Sertifikat metadata-sında saxlanılır." +title = "İmzanın SÉ™bÉ™bi" + +[signatureSettings.tooltip.visibility] +bullet1 = "GörünÉ™n: İmza PDF-dÉ™ xüsusi görünüşlÉ™ görünür" +bullet2 = "GörünmÉ™z: Sertifikat vizual iÅŸarÉ™ olmadan yerləşdirilir" +bullet3 = "GörünmÉ™z imzalar da kriptoqrafik tÉ™sdiq tÉ™min edir" +description = "İmzanın sÉ™nÉ™ddÉ™ görünÉ™n olub-olmadığını vÉ™ ya gizli yerləşdirildiyini idarÉ™ edir." +title = "İmzanın Görünürlüyü" + [settings.configuration] advanced = "Qabaqcıl" database = "MÉ™lumat bazası" endpoints = "EndpointlÉ™r" features = "Funksiyalar" +storageSharing = "Fayl Saxlama vÉ™ PaylaÅŸma" systemSettings = "Sistem parametrlÉ™ri" title = "Konfiqurasiya" @@ -6332,10 +6868,13 @@ title = "Stirling-É™ daxil olun" [setup.selfhosted] link = "vÉ™ ya self-hosted hesaba qoÅŸulun" subtitle = "Server mÉ™lumatlarınızı daxil edin" +changeServerLocked = "Təşkilatınız bu tÉ™tbiqi konkret serverlÉ™ mÉ™hdudlaÅŸdırıb" switchToLocal = "ÆvÉ™zindÉ™ yerli alÉ™tlÉ™rdÉ™n istifadÉ™ et" title = "ServerÉ™ daxil olun" [setup.selfhosted.unreachable] +changeServer = "BaÅŸqa serverÉ™ qoÅŸul" +changeServerLocked = "Təşkilatınız bu tÉ™tbiqi konkret serverlÉ™ mÉ™hdudlaÅŸdırıb" continueOffline = "ÆvÉ™zindÉ™ yerli alÉ™tlÉ™rdÉ™n istifadÉ™ et" message = "{{url}} ünvanına qoÅŸulmaq mümkün olmadı. Serverin iÅŸlÉ™diyini vÉ™ É™lçatan olduÄŸunu yoxlayın." retry = "YenidÉ™n cÉ™hd et" @@ -6529,6 +7068,15 @@ saved = "Saxlanmış" text = "MÉ™tn" title = "İmza növü" +[signRequest] +declined = "İmza sorÄŸusundan imtina edildi" +fetchFailed = "İmza sorÄŸusunu yüklÉ™mÉ™k mümkün olmadı" +signed = "SÉ™nÉ™d uÄŸurla imzalandı" + +[signSession] +createFailed = "İmzalama sorÄŸusu yaratmaq mümkün olmadı" +created = "İmzalama sorÄŸusu göndÉ™rildi" + [signup] accountCreatedSuccessfully = "Hesab uÄŸurla yaradıldı! İndi daxil ola bilÉ™rsiniz." alreadyHaveAccount = "Artıq hesabınız var? Daxil olun" @@ -6807,6 +7355,106 @@ title = "PDF-i hissÉ™lÉ™rÉ™ bölün" [splitPdfByChapters] tags = "böl,fÉ™sillÉ™r,É™lfÉ™cinlÉ™r,nizamla" +[storageShare] +accessed = "Daxil olundu" +accessDenied = "Bu paylaşılan fayla giriÅŸiniz yoxdur. SahibdÉ™n sizinlÉ™ paylaÅŸmağı xahiÅŸ edin." +accessFailed = "FÉ™aliyyÉ™ti yüklÉ™mÉ™k mümkün olmadı." +accessDeniedBody = "Bu fayla giriÅŸiniz yoxdur. SahibdÉ™n sizinlÉ™ paylaÅŸmağı xahiÅŸ edin." +accessDeniedTitle = "GiriÅŸ yoxdur" +accessLimitedCommenter = "Şərh imkanı tezliklÉ™ gÉ™lÉ™cÉ™k. EndirmÉ™yÉ™ ehtiyacınız varsa, sahibdÉ™n redaktor giriÅŸi istÉ™yin." +accessLimitedTitle = "MÉ™hdud giriÅŸ" +accessLimitedViewer = "Bu link yalnız baxış üçündür. EndirmÉ™yÉ™ ehtiyacınız varsa, sahibdÉ™n redaktor giriÅŸi istÉ™yin." +createdAt = "Yaradılıb" +download = "Endir" +downloadFailed = "Bu faylı endirmÉ™k mümkün deyil." +expiredBody = "Bu paylaÅŸma linki etibarsızdır vÉ™ ya vaxtı bitib." +expiredTitle = "Linkin müddÉ™ti bitib" +goToLogin = "GiriÅŸ sÉ™hifÉ™sinÉ™ keç" +loadFailed = "Paylaşılan faylı açmaq mümkün deyil." +loading = "PaylaÅŸma linki yüklÉ™nir..." +loginPrompt = "Bu paylaşılan fayla daxil olmaq üçün daxil olun." +loginRequired = "GiriÅŸ tÉ™lÉ™b olunur" +openInApp = "Open in Stirling PDF" +ownerLabel = "Sahib" +ownerUnknown = "NamÉ™lum" +requiresLogin = "Bu paylaşılan fayl üçün giriÅŸ tÉ™lÉ™b olunur." +roleCommenter = "Şərhçi" +roleEditor = "Redaktor" +roleViewer = "İzlÉ™yici" +shareHeading = "Paylaşılan fayl" +titleDefault = "Paylaşılan fayl" +tryAgain = "ZÉ™hmÉ™t olmasa sonra yenidÉ™n cÉ™hd edin." +addUser = "ÆlavÉ™ et" +commenterHint = "Şərh É™lavÉ™ etmÉ™ funksiyası tezliklÉ™." +copied = "Link panoya kopyalandı" +copy = "Kopyala" +copyFailed = "Kopyalama uÄŸursuz oldu" +description = "Bu fayl üçün paylaÅŸma linki yaradın. LinkÉ™ malik daxil olmuÅŸ istifadəçilÉ™r ona çata bilÉ™r." +downloadsCount = "EndirmÉ™lÉ™r: {{count}}" +emailWarningBody = "Bu e-poçt ünvanına bÉ™nzÉ™yir. Bu ÅŸÉ™xs Stirling PDF istifadəçisi deyilsÉ™, fayla çata bilmÉ™yÉ™cÉ™k." +emailWarningConfirm = "YenÉ™ dÉ™ paylaÅŸ" +emailWarningTitle = "E-poçt ünvanı" +errorTitle = "PaylaÅŸma alınmadı" +failure = "PaylaÅŸma linki yaratmaq mümkün olmadı. ZÉ™hmÉ™t olmasa yenidÉ™n cÉ™hd edin." +fileLabel = "Fayl" +generate = "Link yarat" +generated = "PaylaÅŸma linki yaradıldı" +hideActivity = "FÉ™aliyyÉ™ti gizlÉ™t" +invalidUsername = "Etibarlı istifadəçi adı vÉ™ ya e-poçt ünvanı daxil edin." +lastAccessed = "Son daxilolma" +linkAccessTitle = "PaylaÅŸma linkinÉ™ giriÅŸ" +linkLabel = "PaylaÅŸma linki" +linksDisabled = "PaylaÅŸma linklÉ™ri deaktiv edilib." +linksDisabledBody = "PaylaÅŸma linklÉ™ri server tÉ™nzimlÉ™mÉ™lÉ™riniz tÉ™rÉ™findÉ™n deaktiv edilib." +manage = "PaylaÅŸmanı idarÉ™ et" +manageDescription = "Bu faylı paylaÅŸmaq üçün linklÉ™r yaradın vÉ™ idarÉ™ edin." +manageLoadFailed = "PaylaÅŸma linklÉ™rini yüklÉ™mÉ™k mümkün olmadı." +manageTitle = "PaylaÅŸmanın idarÉ™ edilmÉ™si" +noActivity = "HÉ™lÉ™ fÉ™aliyyÉ™t yoxdur." +noLinks = "HÉ™lÉ™ aktiv paylaÅŸma linki yoxdur." +noSharedUsers = "HÉ™lÉ™ heç bir istifadəçinin giriÅŸi yoxdur." +removeLink = "Linki sil" +removeUser = "Sil" +revokeFailed = "PaylaÅŸma linkini silmÉ™k mümkün olmadı." +revoked = "PaylaÅŸma keçidi silindi" +roleLabel = "Rol" +sharingDisabled = "Paylaşım deaktiv edilib." +sharingDisabledBody = "Paylaşım server tÉ™nzimlÉ™mÉ™lÉ™riniz tÉ™rÉ™findÉ™n deaktiv edilib." +sharedUsersTitle = "Paylaşılan istifadəçilÉ™r" +title = "Faylı paylaÅŸ" +unknownUser = "NamÉ™lum istifadəçi" +userAddFailed = "Bu istifadəçi ilÉ™ paylaÅŸmaq mümkün olmadı." +userAdded = "İstifadəçi paylaşım siyahısına É™lavÉ™ edildi." +usernameLabel = "İstifadəçi adı vÉ™ ya e-poçt" +usernamePlaceholder = "İstifadəçi adı vÉ™ ya e-poçt daxil edin" +userRemoveFailed = "Bu istifadəçini silmÉ™k mümkün olmadı." +userRemoved = "İstifadəçi paylaşım siyahısından silindi." +viewActivity = "AktivliyÉ™ bax" +viewed = "Baxılıb" +viewsCount = "Baxışlar: {{count}}" +downloaded = "Endirilib" +bulkDescription = "Daxil olmuÅŸ istifadəçilÉ™rlÉ™ seçilmiÅŸ bütün faylları paylaÅŸmaq üçün bir keçid yaradın." +bulkTitle = "SeçilmiÅŸ faylları paylaÅŸ" +copyLink = "PaylaÅŸma keçidini kopyala" +fileCount = "{{count}} fayl seçilib" +ownerOnly = "Paylaşımı yalnız sahibi idarÉ™ edÉ™ bilÉ™r." +selectSingleFile = "Paylaşımı idarÉ™ etmÉ™k üçün tÉ™k bir fayl seçin." + +[storageUpload] +description = "Bu, cari faylı öz giriÅŸiniz üçün server yaddaşına yüklÉ™yir." +errorTitle = "YüklÉ™mÉ™ uÄŸursuz oldu" +failure = "YüklÉ™mÉ™ uÄŸursuz oldu. ZÉ™hmÉ™t olmasa giriÅŸ vÉ™ yaddaÅŸ tÉ™nzimlÉ™mÉ™lÉ™rinizi yoxlayın." +fileLabel = "Fayl" +hint = "İctimai keçidlÉ™r vÉ™ giriÅŸ rejimlÉ™ri server tÉ™nzimlÉ™mÉ™lÉ™riniz tÉ™rÉ™findÉ™n idarÉ™ olunur." +success = "ServerÉ™ yüklÉ™ndi" +title = "ServerÉ™ yüklÉ™" +updateButton = "ServerdÉ™ yenilÉ™" +uploadButton = "ServerÉ™ yüklÉ™" +bulkDescription = "Bu, seçilmiÅŸ faylları server yaddaşınıza yüklÉ™yir." +bulkTitle = "SeçilmiÅŸ faylları yüklÉ™" +fileCount = "{{count}} fayl seçilib" +more = " +{{count}} daha" + [storage] approximateSize = "TÉ™xmini ölçü" fileTooLarge = "Fayl çox böyükdür. HÉ™r fayl üçün maksimum ölçü" @@ -7153,6 +7801,30 @@ title = "PDF-É™ Bax/RedaktÉ™ et" [warning] tooltipTitle = "XÉ™bÉ™rdarlıq" +[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É™" + [watermark] completed = "Su niÅŸanı É™lavÉ™ olundu" desc = "PDF fayllarına mÉ™tn vÉ™ ya ÅŸÉ™kil su niÅŸanları É™lavÉ™ edin" @@ -7333,6 +8005,7 @@ activeSession = "Aktiv sessiya" addMembers = "Üzv É™lavÉ™ et" admin = "Admin" confirmDelete = "Bu istifadəçini silmÉ™k istÉ™diyinizÉ™ É™minsiniz? Bu É™mÉ™liyyat geri alına bilmÉ™z." +confirmUnlock = "Bu istifadəçi hesabının kilidini açmaq istÉ™diyinizÉ™ É™minsiniz?" deleteUser = "İstifadəçini sil" deleteUserError = "İstifadəçini silmÉ™k alınmadı" deleteUserSuccess = "İstifadəçi uÄŸurla silindi" @@ -7341,6 +8014,8 @@ disable = "Deaktiv et" disabled = "Deaktiv" editRole = "Rolu redaktÉ™ et" enable = "Aktiv et" +locked = "kilidli" +lockedBadge = "Kilidli" loading = "İnsanlar yüklÉ™nir..." loginRequired = "ÆvvÉ™lcÉ™ giriÅŸ rejimini aktivləşdirin" member = "Üzv" @@ -7350,6 +8025,9 @@ searchMembers = "ÜzvlÉ™rdÉ™ axtar..." status = "Status" team = "Komanda" title = "İnsanlar" +unlockAccount = "Hesabın kilidini aç" +unlockUserError = "İstifadəçi hesabının kilidini açmaq mümkün olmadı" +unlockUserSuccess = "İstifadəçi hesabının kilidi uÄŸurla açıldı" user = "İstifadəçi" [workspace.people.actions] diff --git a/frontend/public/locales/bg-BG/translation.toml b/frontend/public/locales/bg-BG/translation.toml index 0dd69580a4..5db9df17fa 100644 --- a/frontend/public/locales/bg-BG/translation.toml +++ b/frontend/public/locales/bg-BG/translation.toml @@ -8,6 +8,7 @@ black = "Черно" blue = "Синьо" bored = "Отекчени Ñте да чакате?" cancel = "Отказ" +confirm = "Потвърди" changedCredsMessage = "Идентификационните данни Ñа променени!" chooseFile = "Изберете файл" close = "Затворете" @@ -146,6 +147,7 @@ insufficientCredits = "ÐедоÑтатъчен брой кредити. Ðео loadingCredits = "Проверка на кредитите..." loadingProStatus = "Проверка на ÑÑŠÑтоÑнието на абонамента..." noticeTopUpOrPlan = "ÐедоÑтатъчно кредити. МолÑ, заредете още или надградете до план" +accessInvite = "Покани" [account] accountSettings = "ÐаÑтройки на акаунта" @@ -1427,6 +1429,34 @@ title = "Обработка" description = "МакÑимално време за изчакване на задача за обработка преди да Ñе Ñъобщи грешка." label = "Таймаут за обработка (Ñекунди)" +[admin.settings.storage] +description = "Контролирайте Ñървърното Ñъхранение и опциите за ÑподелÑне." +title = "Съхранение на файлове и ÑподелÑне" + +[admin.settings.storage.enabled] +description = "ПозволÑва на потребителите да ÑъхранÑват файлове на Ñървъра." +label = "Ðктивиране на Ñървърно Ñъхранение на файлове" + +[admin.settings.storage.sharing.email] +description = "ПозволÑва ÑподелÑне Ñ Ð¸Ð¼ÐµÐ¹Ð» адреÑи." +label = "Ðктивиране на ÑподелÑне по имейл" +mailLink = "Конфигуриране на наÑтройки за поща" +mailNote = "ИзиÑква ÐºÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ð½Ð° поща. " + +[admin.settings.storage.sharing.enabled] +description = "ПозволÑва на потребителите да ÑподелÑÑ‚ Ñъхранени файлове." +label = "Ðктивиране на ÑподелÑне" + +[admin.settings.storage.sharing.links] +description = "ПозволÑва ÑподелÑне чрез връзки Ñ Ð¸Ð·Ð¸Ñкване за вход." +frontendUrlLink = "Конфигуриране в ÑиÑтемните наÑтройки" +frontendUrlNote = "ИзиÑква Frontend URL. " +label = "Ðктивиране на връзки за ÑподелÑне" + +[admin.settings.storage.signing.enabled] +description = "ПозволÑва Ñъздаване на ÑеÑии за подпиÑване Ñ Ð¼Ð½Ð¾Ð¶ÐµÑтво учаÑтници. ИзиÑква активирано Ñървърно Ñъхранение на файлове." +label = "Ðктивиране на групово подпиÑване (Alpha)" + [admin.settings.unsavedChanges] cancel = "Продължи редактирането" discard = "Отхвърли промените" @@ -2059,7 +2089,19 @@ numbers = "ЧиÑла/диапазони: 5, 10-20" progressions = "ПрогреÑии: 3n, 4n+1" [certSign] +allSigned = "Ð’Ñички учаÑтници Ñа подпиÑали. Готово за финализиране." +awaitingSignatures = "Изчакват Ñе подпиÑи" +signatureProgress = "{{signedCount}}/{{totalCount}} подпиÑа" chooseCertificate = "Изберете файл ÑÑŠÑ Ñертификат" +declined = "Отказано" +fetchFailed = "ÐеуÑпешно зареждане на данните за подпиÑване" +finalized = "Финализирано" +notified = "Ð’ изчакване" +partialNote = "Можете да финализирате по-рано Ñ Ñ‚ÐµÐºÑƒÑ‰Ð¸Ñ‚Ðµ подпиÑи. ÐеподпиÑалите учаÑтници ще бъдат изключени." +pending = "Ð’ изчакване" +readyToFinalize = "Готово за финализиране" +signed = "ПодпиÑано" +viewed = "Прегледано" chooseJksFile = "Изберете JKS файл" chooseP12File = "Изберете PKCS12 файл" choosePfxFile = "Изберете PFX файл" @@ -2082,6 +2124,7 @@ title = "ПодпиÑване ÑÑŠÑ Ñертификат" invisible = "Ðевидим" stepTitle = "Външен вид на подпиÑа" visible = "Видим" +visibility = "ВидимоÑÑ‚" [certSign.appearance.options] title = "Детайли на подпиÑа" @@ -2188,6 +2231,252 @@ bullet4 = "Може да използва потребителÑки Ñерти text = "При проверка инÑтрументът показва дали подпиÑите Ñа валидни, кой е подпиÑал документа, кога е подпиÑан и дали документът е променÑн Ñлед подпиÑване." title = "Проверка на подпиÑи" +[certSign.collab.finalize] +button = "Финализирай и зареди подпиÑÐ°Ð½Ð¸Ñ PDF" +early = "Финализирай Ñ Ñ‚ÐµÐºÑƒÑ‰Ð¸Ñ‚Ðµ подпиÑи" + +[certSign.collab.sessionDetail] +addButton = "Добави учаÑтници" +addParticipants = "Добави учаÑтници" +addParticipantsError = "ÐеуÑпешно добавÑне на учаÑтници" +backToList = "Ðазад към ÑеÑиите" +deleteConfirm = "Сигурни ли Ñте? Това не може да Ñе отмени." +deleteError = "ÐеуÑпешно изтриване на ÑеÑиÑта" +deleted = "СеÑиÑта е изтрита" +deleteSession = "Изтрий ÑеÑиÑ" +dueDate = "Краен Ñрок" +finalizeError = "ÐеуÑпешно финализиране на ÑеÑиÑта" +loadPdfError = "ÐеуÑпешно зареждане на подпиÑан PDF" +loadSignedPdf = "Зареди подпиÑÐ°Ð½Ð¸Ñ PDF в активните файлове" +messageLabel = "Съобщение" +noAdditionalInfo = "ÐÑма допълнителна информациÑ" +owner = "СобÑтвеник" +participantRemoved = "УчаÑтникът е премахнат" +participants = "УчаÑтници" +participantsAdded = "УчаÑтниците Ñа добавени уÑпешно" +removeParticipant = "Премахни" +removeParticipantError = "ÐеуÑпешно премахване на учаÑтник" +selectUsers = "Изберете потребители..." +sessionInfo = "Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° ÑеÑиÑта" +workbenchTitle = "Управление на ÑеÑиÑта" + +[certSign.collab.signRequest] +addedToFiles = "Документът е добавен към активните файлове" +addSignature = "Добави ÑÐ²Ð¾Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñ" +addToFiles = "Добави към активни файлове" +advancedSettings = "Разширени наÑтройки" +backToList = "Ðазад към заÑвките за подпиÑване" +certificateChoice = "Изберете Ñертификат, Ñ ÐºÐ¾Ð¹Ñ‚Ð¾ да подпишете" +changeSignature = "Промени подпиÑа" +clearSignature = "ИзчиÑти подпиÑа" +completeAndSign = "Завърши и подпиши" +createNewSignature = "Създай нов подпиÑ" +declineButton = "Откажи" +decline = "Откажи заÑвката" +deleteSelected = "Изтрий Ð¸Ð·Ð±Ñ€Ð°Ð½Ð¸Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñ" +drawSignature = "ÐариÑувайте ÑÐ²Ð¾Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñ Ð¿Ð¾-долу" +dueDate = "Краен Ñрок" +fileTooLarge = "Размерът на файла трÑбва да е под 5MB" +fontFamily = "СемейÑтво шрифтове" +fontSize = "Размер на шрифта: {{size}}px" +fontSizePlaceholder = "Размер" +from = "От" +invalidCertFile = "МолÑ, изберете P12 или PFX файл ÑÑŠÑ Ñертификат" +invalidFileType = "МолÑ, изберете файл Ñ Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ðµ" +location = "МеÑтоположение (незадължително)" +locationPlaceholder = "Откъде подпиÑвате?" +message = "Съобщение" +noCertificate = "МолÑ, изберете файл ÑÑŠÑ Ñертификат" +noSignatures = "МолÑ, поÑтавете поне един Ð¿Ð¾Ð´Ð¿Ð¸Ñ Ð²ÑŠÑ€Ñ…Ñƒ PDF файла" +p12File = "P12/PFX файл ÑÑŠÑ Ñертификат" +password = "Парола за Ñертификат" +passwordPlaceholder = "Въведете парола..." +penColor = "ЦвÑÑ‚ на пиÑалката" +penSize = "Размер на пиÑалката: {{size}}px" +placementActive = "Щракнете върху PDF, за да поÑтавите" +placeSignatureButton = "ПоÑтави Ð¿Ð¾Ð´Ð¿Ð¸Ñ Ð²ÑŠÑ€Ñ…Ñƒ PDF" +reason = "Причина (незадължително)" +reasonPlaceholder = "Защо подпиÑвате?" +removeImage = "Премахни изображението" +removeCertFile = "Премахни файла" +savedSignatures = "ЗапиÑани подпиÑи" +selectFile = "Изберете файл Ñ Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ðµ" +selectSignatureTitle = "Изберете или Ñъздайте подпиÑ" +signButton = "Подпиши документа" +signatureInfo = "Тези наÑтройки Ñе задават от ÑобÑтвеника на документа" +signaturePlaced = "ПодпиÑÑŠÑ‚ е поÑтавен на Ñтраница" +signatureSettings = "ÐаÑтройки на подпиÑа" +signatureText = "ТекÑÑ‚ на подпиÑа" +signatureTextPlaceholder = "Въведете вашето име..." +signatureTypeLabel = "Тип на подпиÑа" +signingTitle = "ПодпиÑване" +textColor = "ЦвÑÑ‚ на текÑта" +typeSignature = "Въведете името Ñи, за да Ñъздадете подпиÑ" +uploadCert = "ПотребителÑки Ñертификат" +uploadCertDesc = "Използвайте Ñвой P12/PFX Ñертификат" +uploadSignature = "Качете изображение на подпиÑа Ñи" +usePersonalCert = "Личен Ñертификат" +usePersonalCertDesc = "Ðвтоматично генериран за Ð²Ð°ÑˆÐ¸Ñ Ð°ÐºÐ°ÑƒÐ½Ñ‚" +useServerCert = "Сертификат на организациÑта" +useServerCertDesc = "Споделен организационен Ñертификат" +workbenchTitle = "ЗаÑвка за подпиÑване" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Изберете цвÑÑ‚ на щриха" +continue = "Продължи" + +[certSign.collab.signRequest.certModal] +description = "ПоÑтавили Ñте {{count}} подпиÑ(а). Изберете Ñертификат, за да завършите подпиÑването." +sign = "Подпиши документа" +certValidating = "Проверка на Ñертификата..." +certValidUntil = "Сертификатът е валиден до {{date}}" +certInvalid = "Сертификатът е невалиден: {{error}}" +certInvalidFallback = "Ðевалиден Ñертификат" +certNetworkError = "Ðе можа да Ñе валидира Ñертификатът" +title = "Конфигуриране на Ñертификат" + +[certSign.collab.signRequest.image] +hint = "Качете PNG или JPG изображение на Ð²Ð°ÑˆÐ¸Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñ" + +[certSign.collab.signRequest.mode] +move = "ПремеÑти подпиÑа" +place = "ПоÑтави подпиÑ" +title = "Режим подпиÑване или премеÑтване" + +[certSign.collab.signRequest.modeTabs] +draw = "РиÑуване" +image = "Качване" +text = "Въвеждане" + +[certSign.collab.signRequest.placeSignature] +message = "Щракнете върху PDF, за да поÑтавите подпиÑа Ñи" +title = "ПоÑтави подпиÑ" + +[certSign.collab.signRequest.preview] +imageAlt = "ИзбраниÑÑ‚ подпиÑ" +missing = "ÐÑма визуализациÑ" +textFallback = "ПодпиÑ" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "РиÑуван подпиÑ" +defaultImageLabel = "Качен подпиÑ" +defaultLabel = "ПодпиÑ" +defaultTextLabel = "Въведен подпиÑ" +delete = "Изтрий подпиÑа" +none = "ÐÑма запиÑани подпиÑи" + +[certSign.collab.signRequest.signatureType] +draw = "РиÑуване" +type = "Въвеждане" +upload = "Качване" + +[certSign.collab.signRequest.steps] +back = "Ðазад" +cancelPlacement = "Отмени поÑтавÑнето" +certificate = "Сертификат" +clickMultipleTimes = "Щракнете върху PDF нÑколко пъти, за да поÑтавите подпиÑи. Плъзнете който и да е подпиÑ, за да го премеÑтите или оразмерите." +clickToPlace = "Щракнете върху PDF, където желаете да Ñе поÑви подпиÑÑŠÑ‚ ви." +continue = "Продължи към избора на Ñертификат" +continueToPlacement = "Продължи към поÑтавÑне" +continueToReview = "Продължи към преглед" +createSignature = "Създай подпиÑ" +invisible = "Ðевидим" +location = "МеÑтоположение:" +multipleSignatures = "{{count}} подпиÑ(а) ще бъдат приложени към PDF файла" +oneSignature = "1 Ð¿Ð¾Ð´Ð¿Ð¸Ñ Ñ‰Ðµ бъде приложен към PDF файла" +placeOnPdf = "ПоÑтави върху PDF" +reason = "Причина:" +reviewTitle = "Преглед преди подпиÑване" +signaturePlaced = "ПодпиÑÑŠÑ‚ е поÑтавен на Ñтраница {{page}}. Можете да коригирате позициÑта Ñ Ð½Ð¾Ð²Ð¾ щракване или да продължите към преглед." +visible = "Видим" +visibility = "ВидимоÑÑ‚:" +yourSignatures = "Вашите подпиÑи ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "ЦвÑÑ‚" +fontLabel = "Шрифт" +fontSizeLabel = "Размер" +fontSizePlaceholder = "16" +label = "ТекÑÑ‚ на подпиÑа" +modalHint = "Въведете името Ñи, Ñлед това натиÑнете „Продължи“, за да го поÑтавите върху PDF." +placeholder = "Въведете вашето име..." + +[certSign.collab.participant] +certValidating = "Проверка на Ñертификата..." +certValid = "✓ Сертификатът е валиден" +certValidUntil = " до {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Ðевалиден Ñертификат" +certNetworkError = "Ðе можа да Ñе валидира Ñертификатът" + +[certSign.collab.addParticipants] +add = "Добави {{count}} учаÑтник(а)" +back = "Ðазад" +configureSignatures = "Конфигурирай наÑтройките на подпиÑа" +continue = "Продължи към наÑтройките на подпиÑа" +reasonHelp = "Предварително задайте причина за подпиÑване за тези учаÑтници (незадължително; могат да Ñ Ð¿Ñ€Ð¾Ð¼ÐµÐ½ÑÑ‚ при подпиÑване)" +reasonPlaceholder = "напр. Одобрение, Преглед..." +selectUsers = "Изберете потребители" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Включи Ñтраница Ñ Ð¾Ð±Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ðµ на подпиÑите" +includeSummaryPageHelp = "Ð’ ÐºÑ€Ð°Ñ Ñ‰Ðµ бъде добавена обобщаваща Ñтраница Ñ Ð²Ñички метаданни за подпиÑите. Полетата за цифров Ñертификат на отделните Ñтраници ще бъдат Ñкрити (ръчните подпиÑи не Ñе заÑÑгат)." + +[certSign.collab.sessionList] +active = "Ðктивни" +finalized = "Финализирани" + +[certSign.collab.signatureSettings] +description = "Конфигурирайте как ще изглеждат подпиÑите за вÑички учаÑтници" +title = "Външен вид на подпиÑа" + +[certSign.collab.userSelector] +inviteUsers = "Добави потребители" +loadError = "ÐеуÑпешно зареждане на потребители" +noTeam = "Без екип" +noUsers = "Ðе Ñа намерени други потребители." +placeholder = "Изберете потребители..." + +[certSign.mobile] +panelActions = "ДейÑтвиÑ" +panelDocument = "Документ" +panelPeople = "Хора" + +[certSign.sessions] +deleted = "СеÑиÑта е изтрита" +fetchFailed = "ÐеуÑпешно зареждане на подробноÑти за ÑеÑиÑта" +finalized = "СеÑиÑта е финализирана" +loaded = "ПодпиÑаниÑÑ‚ PDF е зареден" +pdfNotReady = "PDF не е готов" +pdfNotReadyDesc = "ПодпиÑаниÑÑ‚ PDF Ñе генерира. Опитайте отново Ñлед малко." + +[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" + [changeCreds] changePassword = "Използвате идентификационни данни за вход по подразбиране. МолÑ, въведете нова парола" changeUsername = "Ðктуализирайте потребителÑкото Ñи име. След актуализиране ще бъдете изведени от профила." @@ -3242,6 +3531,46 @@ totalSelected = "Общо избрани" unsupported = "Ðеподдържано" unzip = "Разархивирай" uploadError = "ÐеуÑпешно качване на нÑкои файлове." +copyCreated = "Копието е запиÑано на това уÑтройÑтво." +copyFailed = "Ðе можа да Ñе Ñъздаде копие." +leaveShare = "Премахни от Ð¼Ð¾Ñ ÑпиÑък" +leaveShareFailed = "Ðе можа да Ñе премахне ÑподелениÑÑ‚ файл." +leaveShareSuccess = "Премахнато от Ð²Ð°ÑˆÐ¸Ñ Ñподелен ÑпиÑък." +removeBoth = "Премахни и от двете" +removeFilePrompt = "Този файл е запиÑан на това уÑтройÑтво и на Ð²Ð°ÑˆÐ¸Ñ Ñървър. Откъде иÑкате да го премахнете?" +removeFileTitle = "Премахване на файл" +removeLocalOnly = "Само на това уÑтройÑтво" +removeServerFailed = "Ðе можа да Ñе премахне файлът от Ñървъра." +removeServerOnly = "Само от Ñървъра" +removeServerOnlyPrompt = "Този файл Ñе ÑъхранÑва Ñамо на Ð²Ð°ÑˆÐ¸Ñ Ñървър. ИÑкате ли да го премахнете от Ñървъра?" +removeServerSuccess = "Премахнат от Ñървъра." +removeSharedPrompt = "Този файл е Ñподелен Ñ Ð²Ð°Ñ. Можете да го премахнете от това уÑтройÑтво или от Ð²Ð°ÑˆÐ¸Ñ Ñподелен ÑпиÑък." +removeSharedServerOnlyBlockedPrompt = "Този файл е Ñподелен Ñ Ð²Ð°Ñ Ð¸ Ñе ÑъхранÑва Ñамо на Ñървъра." +removeSharedServerOnlyPrompt = "Този файл е Ñподелен Ñ Ð²Ð°Ñ Ð¸ Ñе ÑъхранÑва Ñамо на Ñървъра. Да бъде ли премахнат от Ð²Ð°ÑˆÐ¸Ñ ÑпиÑък?" +changesNotUploaded = "Промените не Ñа качени" +cloudFile = "Файл в облака" +filterAll = "Ð’Ñички" +filterLocal = "Локални" +filterSharedByMe = "Споделени от мен" +filterSharedWithMe = "Споделени Ñ Ð¼ÐµÐ½" +lastSynced = "ПоÑледно Ñинхронизирано" +localOnly = "Само локално" +makeCopy = "Създай копие" +owner = "СобÑтвеник" +ownerUnknown = "Ðепознат" +share = "Сподели" +shareSelected = "Сподели избраните" +sharedByYou = "Споделено от ваÑ" +sharedEditNoticeBody = "ÐÑмате права за редактиране на Ñървърната верÑÐ¸Ñ Ð½Ð° този файл. Ð’Ñички промени, които направите, ще бъдат запиÑани като локално копие." +sharedEditNoticeConfirm = "Разбрах" +sharedEditNoticeTitle = "Сървърното копие е Ñамо за четене" +sharedWithYou = "Споделено Ñ Ð²Ð°Ñ" +sharing = "СподелÑне" +storageState = "Съхранение" +synced = "Синхронизирано" +updateOnServer = "Ðктуализирай на Ñървъра" +uploadSelected = "Качи избраните" +uploadToServer = "Качи на Ñървъра" [files] addFiles = "ДобавÑне на файлове" @@ -3367,6 +3696,77 @@ title = "За ÑплеÑкването на PDF" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "За груповото подпиÑване" + +[groupSigning.tooltip.finalization] +bullet1 = "Ð’Ñички подпиÑи Ñе прилагат в Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð¸Ñ Ð¾Ñ‚ Ð²Ð°Ñ Ñ€ÐµÐ´ на учаÑтниците" +bullet2 = "При нужда можете да финализирате Ñ Ñ‡Ð°ÑÑ‚ от подпиÑите" +bullet3 = "След финализиране ÑеÑиÑта не може да бъде променÑна" +description = "След като вÑички учаÑтници подпишат (или изберете ранно финализиране), можете да генерирате ÐºÑ€Ð°Ð¹Ð½Ð¸Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñан PDF." +title = "ÐŸÑ€Ð¾Ñ†ÐµÑ Ð½Ð° финализиране" + +[groupSigning.tooltip.roles] +bullet1 = "СобÑтвеник (вие): Създава ÑеÑиÑ, конфигурира наÑтройки по подразбиране, финализира документа" +bullet2 = "УчаÑтници: Създават ÑÐ²Ð¾Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñ, избират Ñертификат, поÑтавÑÑ‚ го върху PDF" +bullet3 = "УчаÑтниците не могат да променÑÑ‚ наÑтройките за видимоÑÑ‚, причина или меÑтоположение на подпиÑа" +description = "Вие контролирате наÑтройките за външен вид на подпиÑите за вÑички учаÑтници." +title = "Роли на учаÑтниците" + +[groupSigning.tooltip.sequential] +bullet1 = "ПървиÑÑ‚ учаÑтник трÑбва да подпише, преди вториÑÑ‚ да получи доÑтъп до документа" +bullet2 = "Гарантира правилен ред на подпиÑване за правно ÑъответÑтвие" +bullet3 = "Можете да пренареждате учаÑтниците чрез плъзгане в ÑпиÑъка" +description = "УчаÑтниците подпиÑват документите в поÑÐ¾Ñ‡ÐµÐ½Ð¸Ñ Ð¾Ñ‚ Ð²Ð°Ñ Ñ€ÐµÐ´. Ð’Ñеки подпиÑващ получава извеÑтие, когато му дойде редът." +title = "ПоÑледователно подпиÑване" + +[groupSigning.steps] +back = "Ðазад" +completed = "Завършено" +current = "Текущо" +stepLabel = "Стъпка {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Продължи към преглед" +invisible = "ПодпиÑите ще бъдат невидими (Ñамо метаданни)" +locationLabel = "МеÑтоположение:" +preview = "ВизуализациÑ" +reasonLabel = "Причина:" +title = "Конфигурирай наÑтройките на подпиÑа" +visible = "ПодпиÑите ще Ñа видими на Ñтраница {{page}}" + +[groupSigning.steps.review] +document = "Документ" +dueDate = "Краен Ñрок (незадължително)" +dueDatePlaceholder = "Изберете краен Ñрок..." +invisible = "Ðевидими (Ñамо метаданни)" +location = "МеÑтоположение:" +logo = "Лого:" +logoHidden = "Без лого" +logoShown = "Показано е логото на Stirling PDF" +participants = "УчаÑтници" +reason = "Причина:" +send = "Изпрати заÑвки за подпиÑване" +signatureSettings = "ÐаÑтройки на подпиÑа" +title = "Преглед на подробноÑтите за ÑеÑиÑта" +titleShort = "Преглед и изпращане" +visibility = "ВидимоÑÑ‚:" +visible = "Видими на Ñтраница {{page}}" +participantCount = "{{count}} учаÑтник(а) ще подпиÑват поÑледователно" + +[groupSigning.steps.selectDocument] +continue = "Продължи към избор на учаÑтници" +noFile = "МолÑ, изберете един PDF файл от активните Ñи файлове, за да Ñъздадете ÑеÑÐ¸Ñ Ð·Ð° подпиÑване." +selectedFile = "Избран документ" +title = "Избери документ" + +[groupSigning.steps.selectParticipants] +continue = "Продължи към наÑтройките на подпиÑа" +count = "Избрани Ñа {{count}} учаÑтник(а)" +label = "Изберете учаÑтници" +placeholder = "Изберете учаÑтници за подпиÑ..." +title = "Изберете учаÑтници" + [getPdfInfo] downloadJson = "Изтеглете JSON" downloads = "ИзтеглÑниÑ" @@ -4460,7 +4860,10 @@ zoomOut = "Ðамали" [viewer] cannotPreviewFile = "Ðе може да Ñе визуализира файлът" +disableColorFilter = "Изключи цветови филтър" dualPageView = "Изглед: две Ñтраници" +enableDarkFilter = "Включи тъмен филтър" +enableSepiaFilter = "Включи ÑÐµÐ¿Ð¸Ñ Ñ„Ð¸Ð»Ñ‚ÑŠÑ€" firstPage = "Първа Ñтраница" lastPage = "ПоÑледна Ñтраница" nextPage = "Следваща Ñтраница" @@ -4470,6 +4873,22 @@ singlePageView = "Изглед: една Ñтраница" unknownFile = "Ðепознат файл" zoomIn = "Увеличи" zoomOut = "Ðамали" +resetZoom = "Ðулирай мащаба" + +[viewer.nonPdf] +fileTypeBadge = "{{type}} файл" +convertToPdf = "Конвертирай в PDF" +loading = "Зареждане..." +emptyFile = "Празен файл" +csvStats = "{{rows}} реда · {{columns}} колони · {{size}}" +sortedBy = "Сортирано по: {{column}}" +columnDefault = "Колона {{index}}" +htmlPreviewWarning = "HTML Ð²Ð¸Ð·ÑƒÐ°Ð»Ð¸Ð·Ð°Ñ†Ð¸Ñ â€” външните реÑурÑи може да не Ñе заредÑÑ‚ · {{size}}" +htmlPreview = "HTML визуализациÑ" +invalidJson = "Ðевалиден JSON — показване на Ñуровото Ñъдържание" +textStats = "{{lines}} реда · {{size}}" +lineNumbers = "Ðомера на редовете" +renderMarkdown = "Рендерирай Markdown" [viewer.attachments] title = "Прикачени файлове" @@ -4531,6 +4950,7 @@ toggleAttachments = "Превключване на прикачени файло toggleTheme = "Превключи тема" language = "Език" toggleAnnotations = "Показване/Ñкриване на анотациите" +toggleLayers = "Превключи Ñлоеве" search = "ТърÑене в PDF" panMode = "Режим на придвижване" applyRedactionsFirst = "Първо приложете заличаваниÑта" @@ -5407,20 +5827,72 @@ title = "Печат на файл" 2 = "Въведете име на принтер" [quickAccess] +access = "ДоÑтъп" +accessAddPerson = "Добави още човек" +accessBack = "Ðазад" +accessCopyLink = "Копирай връзка" +accessEmail = "Имейл адреÑ" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Файл" +accessGeneral = "Общ доÑтъп" +accessInviteTitle = "Покани хора" +accessOwner = "СобÑтвеник" +accessPanel = "ДоÑтъп до документа" +accessPeople = "Хора Ñ Ð´Ð¾Ñтъп" +accessRemove = "Премахни" +accessRestricted = "Ограничен" +accessRestrictedHint = "Само хора Ñ Ð´Ð¾Ñтъп могат да отварÑÑ‚" +accessRole = "РолÑ" +accessRoleCommenter = "Коментатор" +accessRoleEditor = "Редактор" +accessRoleViewer = "Преглеждащ" +accessSelectedFile = "Избран файл" +accessSendInvite = "Изпрати покана" +accessTitle = "ДоÑтъп до документа" +accessYou = "Вие" account = "Ðкаунт" +activeSessions = "Ðктивни ÑеÑии" +activeTab = "Ðктивни" activity = "ДейноÑÑ‚" adminSettings = "Ðдмин опции" +allSessions = "Ð’Ñички ÑеÑии" allTools = "All Tools" automate = "Ðвто" +back = "Ðазад" +certSign = "ÐŸÐ¾Ð´Ð¿Ð¸Ñ ÑÑŠÑ Ñертификат" +completedSessions = "Завършени ÑеÑии" +completedTab = "Завършени" config = "Конфиг" +createNew = "Създай нова заÑвка" +createSession = "Създай заÑвка за подпиÑване" +dueDate = "Краен Ñрок (незадължително)" files = "Файлове" help = "Помощ" +noActiveSessions = "ÐÑма чакащи заÑвки за подпиÑване или активни ÑеÑии" +noCompletedSessions = "ÐÑма завършени ÑеÑии" +noFile = "ÐÑма избран файл" read = "Четене" reader = "Четец" +refresh = "ОпреÑни" +requestSignatures = "ЗаÑви подпиÑи" +selectSingleFileToRequest = "Изберете един PDF файл, за да поиÑкате подпиÑи" +selectedFile = "Избран файл" +selectUsers = "Изберете потребители за подпиÑ" +selectUsersPlaceholder = "Изберете учаÑтници..." +sendingRequest = "Изпращане..." settings = "Опции" showMeAround = "Покажете ми наоколо" sign = "ПодпиÑ" +signatureRequests = "ЗаÑвки за подпиÑване" +signYourself = "Подпишете Ñами" +newRequest = "Ðова заÑвка" tours = "Турове" +wetSign = "Добави подпиÑ" +filterMine = "Моите" +filterOverdue = "ПроÑрочени" +filterSigned = "ПодпиÑано" +filterDeclined = "Отказано" +searchDocuments = "ТърÑене в документи…" [quickAccess.helpMenu] adminTour = "Обиколка за админи" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "ВашиÑÑ‚ Stirling-PDF Ñървър е офлай expired = "Вашата ÑеÑÐ¸Ñ Ðµ изтекла. МолÑ, опреÑнете Ñтраницата и опитайте отново." refreshPage = "Презареждане на Ñтраницата" +[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 = "Ред на подпиÑите" + +[signatureSettings.tooltip] +header = "ÐаÑтройки за външен вид на подпиÑа" + +[signatureSettings.tooltip.location] +bullet1 = "Примери: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Ðе е Ñъщото като Ð¿Ð¾Ð·Ð¸Ñ†Ð¸Ñ Ð½Ð° Ñтраницата" +bullet3 = "Може да Ñе изиÑква в определени юридичеÑки юриÑдикции" +description = "Ðезадължително географÑко меÑтоположение, където е приложен подпиÑÑŠÑ‚. СъхранÑва Ñе в метаданните на Ñертификата." +title = "МеÑтоположение на подпиÑа" + +[signatureSettings.tooltip.logo] +bullet1 = "Показва Ñе заедно Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñа и текÑта" +bullet2 = "Поддържа формати PNG, JPG" +bullet3 = "ПодобрÑва профеÑÐ¸Ð¾Ð½Ð°Ð»Ð½Ð¸Ñ Ð²Ð¸Ð´" +description = "Добавете фирмено лого към видимите подпиÑи за брандиране и автентичноÑÑ‚." +title = "Фирмено лого" + +[signatureSettings.tooltip.reason] +bullet1 = "Примери: \"Approval\", \"Contract Agreement\", \"Review Complete\"" +bullet2 = "Видимо в ÑвойÑтвата на подпиÑа в PDF" +bullet3 = "Полезно за одит и ÑъответÑтвие" +description = "Ðезадължителен текÑÑ‚, обÑÑнÑващ защо документът Ñе подпиÑва. СъхранÑва Ñе в метаданните на Ñертификата." +title = "Причина за подпиÑване" + +[signatureSettings.tooltip.visibility] +bullet1 = "Видим: ПодпиÑÑŠÑ‚ Ñе показва в PDF Ñ Ð¿ÐµÑ€Ñонализиран вид" +bullet2 = "Ðевидим: Сертификатът е вграден без видима маркировка" +bullet3 = "Ðевидимите подпиÑи пак оÑигурÑват криптографÑка валидациÑ" +description = "Контролира дали подпиÑÑŠÑ‚ е видим върху документа или е вграден невидимо." +title = "ВидимоÑÑ‚ на подпиÑа" + [settings.configuration] advanced = "Разширени" database = "База данни" endpoints = "Крайни точки" features = "Функции" +storageSharing = "Съхранение на файлове и ÑподелÑне" systemSettings = "СиÑтемни наÑтройки" title = "КонфигурациÑ" @@ -6332,10 +6868,13 @@ title = "Впишете Ñе в Stirling" [setup.selfhosted] link = "или Ñе Ñвържете ÑÑŠÑ ÑамоÑтоÑтелно хоÑтван акаунт" subtitle = "Въведете Ñвоите данни за Ñървъра" +changeServerLocked = "Вашата Ð¾Ñ€Ð³Ð°Ð½Ð¸Ð·Ð°Ñ†Ð¸Ñ Ðµ ограничила това приложение до конкретен Ñървър" switchToLocal = "Използване на локалните инÑтрументи" title = "Впишете Ñе в Ñървъра" [setup.selfhosted.unreachable] +changeServer = "Свържете Ñе Ñ Ð´Ñ€ÑƒÐ³ Ñървър" +changeServerLocked = "Вашата Ð¾Ñ€Ð³Ð°Ð½Ð¸Ð·Ð°Ñ†Ð¸Ñ Ðµ ограничила това приложение до конкретен Ñървър" continueOffline = "Използване на локалните инÑтрументи" message = "Ðе може да Ñе оÑъщеÑтви доÑтъп до {{url}}. Проверете дали Ñървърът работи и е доÑтъпен." retry = "Повторен опит" @@ -6529,6 +7068,15 @@ saved = "Запазени" text = "ТекÑÑ‚" title = "Тип подпиÑ" +[signRequest] +declined = "ЗаÑвката за подпиÑване е отказана" +fetchFailed = "ÐеуÑпешно зареждане на заÑвката за подпиÑване" +signed = "Документът е подпиÑан уÑпешно" + +[signSession] +createFailed = "ÐеуÑпешно Ñъздаване на заÑвка за подпиÑване" +created = "ЗаÑвката за подпиÑване е изпратена" + [signup] accountCreatedSuccessfully = "Ðкаунтът е Ñъздаден уÑпешно! Сега можете да влезете." alreadyHaveAccount = "Вече имате акаунт? Вход" @@ -6807,6 +7355,106 @@ title = "Разделете PDF по глави" [splitPdfByChapters] tags = "разделÑне, глави, отметки, организиране" +[storageShare] +accessed = "ДоÑтъпено" +accessDenied = "ÐÑмате доÑтъп до този Ñподелен файл. Помолете ÑобÑтвеника да го Ñподели Ñ Ð²Ð°Ñ." +accessFailed = "ÐеуÑпешно зареждане на активноÑтта." +accessDeniedBody = "ÐÑмате доÑтъп до този файл. Помолете ÑобÑтвеника да го Ñподели Ñ Ð²Ð°Ñ." +accessDeniedTitle = "ÐÑма доÑтъп" +accessLimitedCommenter = "ДоÑтъпът за коментиране Ñкоро ще бъде наличен. Помолете ÑобÑтвеника за доÑтъп като редактор, ако трÑбва да изтеглите." +accessLimitedTitle = "Ограничен доÑтъп" +accessLimitedViewer = "Тази връзка е Ñамо за преглед. Помолете ÑобÑтвеника за доÑтъп като редактор, ако трÑбва да изтеглите." +createdAt = "Създадено" +download = "Изтегли" +downloadFailed = "Файлът не може да бъде изтеглен." +expiredBody = "Тази връзка за ÑподелÑне е невалидна или е изтекла." +expiredTitle = "Връзката е изтекла" +goToLogin = "Към вход" +loadFailed = "ÐеуÑпешно отварÑне на Ñподелен файл." +loading = "Зареждане на връзка за ÑподелÑне..." +loginPrompt = "Впишете Ñе, за да получите доÑтъп до този Ñподелен файл." +loginRequired = "Ðеобходим е вход" +openInApp = "Отвори в Stirling PDF" +ownerLabel = "СобÑтвеник" +ownerUnknown = "Ðепознат" +requiresLogin = "Този Ñподелен файл изиÑква вход." +roleCommenter = "Коментатор" +roleEditor = "Редактор" +roleViewer = "Преглеждащ" +shareHeading = "Споделен файл" +titleDefault = "Споделен файл" +tryAgain = "МолÑ, опитайте отново по-къÑно." +addUser = "Добави" +commenterHint = "Коментирането Ñкоро ще бъде налично." +copied = "Връзката е копирана в клипборда" +copy = "Копирай" +copyFailed = "Копирането не бе уÑпешно" +description = "Създайте връзка за ÑподелÑне за този файл. ВпиÑаните потребители Ñ Ð²Ñ€ÑŠÐ·ÐºÐ°Ñ‚Ð° ще имат доÑтъп." +downloadsCount = "ИзтеглÑниÑ: {{count}}" +emailWarningBody = "Изглежда като имейл адреÑ. Ðко този човек не е потребител на Stirling PDF, нÑма да може да получи доÑтъп до файла." +emailWarningConfirm = "Сподели въпреки това" +emailWarningTitle = "Имейл адреÑ" +errorTitle = "ÐеуÑпешно ÑподелÑне" +failure = "Ðе може да Ñе генерира връзка за ÑподелÑне. МолÑ, опитайте отново." +fileLabel = "Файл" +generate = "Генерирай връзка" +generated = "Генерирана е връзка за ÑподелÑне" +hideActivity = "Скрий активноÑтта" +invalidUsername = "Въведете валидно потребителÑко име или имейл адреÑ." +lastAccessed = "ПоÑледен доÑтъп" +linkAccessTitle = "ДоÑтъп чрез връзка за ÑподелÑне" +linkLabel = "Връзка за ÑподелÑне" +linksDisabled = "Връзките за ÑподелÑне Ñа деактивирани." +linksDisabledBody = "Връзките за ÑподелÑне Ñа деактивирани от наÑтройките на Ð²Ð°ÑˆÐ¸Ñ Ñървър." +manage = "Управление на ÑподелÑнето" +manageDescription = "Създавайте и управлÑвайте връзки за ÑподелÑне на този файл." +manageLoadFailed = "ÐеуÑпешно зареждане на връзките за ÑподелÑне." +manageTitle = "Управление на ÑподелÑнето" +noActivity = "Ð’Ñе още нÑма активноÑÑ‚." +noLinks = "Ð’Ñе още нÑма активни връзки за ÑподелÑне." +noSharedUsers = "Ð’Ñе още нÑма потребители Ñ Ð´Ð¾Ñтъп." +removeLink = "Премахни връзката" +removeUser = "Премахни" +revokeFailed = "Ðе можа да Ñе премахне връзката за ÑподелÑне." +revoked = "Връзката за ÑподелÑне е премахната" +roleLabel = "РолÑ" +sharingDisabled = "СподелÑнето е деактивирано." +sharingDisabledBody = "СподелÑнето е деактивирано от наÑтройките на Ð²Ð°ÑˆÐ¸Ñ Ñървър." +sharedUsersTitle = "Потребители ÑÑŠÑ Ñподелен доÑтъп" +title = "СподелÑне на файл" +unknownUser = "ÐеизвеÑтен потребител" +userAddFailed = "Ðе може да Ñе Ñподели Ñ Ñ‚Ð¾Ð·Ð¸ потребител." +userAdded = "ПотребителÑÑ‚ е добавен към ÑпиÑъка за ÑподелÑне." +usernameLabel = "ПотребителÑко име или имейл" +usernamePlaceholder = "Въведете потребителÑко име или имейл" +userRemoveFailed = "Ðе може да Ñе премахне този потребител." +userRemoved = "ПотребителÑÑ‚ е премахнат от ÑпиÑъка за ÑподелÑне." +viewActivity = "Преглед на активноÑтта" +viewed = "Прегледано" +viewsCount = "Прегледи: {{count}}" +downloaded = "Изтеглено" +bulkDescription = "Създайте една връзка за ÑподелÑне на вÑички избрани файлове Ñ Ð²Ð»ÐµÐ·Ð»Ð¸ в ÑиÑтемата потребители." +bulkTitle = "СподелÑне на избраните файлове" +copyLink = "Копиране на връзката за ÑподелÑне" +fileCount = "{{count}} избрани файла" +ownerOnly = "Само ÑобÑтвеникът може да управлÑва ÑподелÑнето." +selectSingleFile = "Изберете един файл, за да управлÑвате ÑподелÑнето." + +[storageUpload] +description = "Това качва Ñ‚ÐµÐºÑƒÑ‰Ð¸Ñ Ñ„Ð°Ð¹Ð» в Ñървърното хранилище за ваш доÑтъп." +errorTitle = "ÐеуÑпешно качване" +failure = "Качването не бе уÑпешно. МолÑ, проверете данните Ñи за вход и наÑтройките за Ñъхранение." +fileLabel = "Файл" +hint = "Публичните връзки и режимите за доÑтъп Ñе контролират от наÑтройките на Ð²Ð°ÑˆÐ¸Ñ Ñървър." +success = "Качено на Ñървъра" +title = "Качване на Ñървъра" +updateButton = "Ðктуализиране на Ñървъра" +uploadButton = "Качване на Ñървъра" +bulkDescription = "Това качва избраните файлове в Ñървърното ви хранилище." +bulkTitle = "Качване на избраните файлове" +fileCount = "{{count}} избрани файла" +more = " +{{count}} още" + [storage] approximateSize = "Приблизителен размер" fileTooLarge = "Файлът е твърде голÑм. МакÑималниÑÑ‚ размер на файл е" @@ -7153,6 +7801,30 @@ title = "Преглед/Редактиране на PDF" [warning] tooltipTitle = "Предупреждение" +[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 = "Качване на изображение на подпиÑ" + [watermark] completed = "Добавен е воден знак" desc = "ДобавÑне на текÑтови или изображени водни знаци към PDF файлове" @@ -7333,6 +8005,7 @@ activeSession = "Ðктивна ÑеÑиÑ" addMembers = "ДобавÑне на членове" admin = "Ðдмин" confirmDelete = "Сигурни ли Ñте, че иÑкате да изтриете този потребител? Това дейÑтвие не може да бъде отменено." +confirmUnlock = "Сигурни ли Ñте, че иÑкате да отключите този потребителÑки акаунт?" deleteUser = "Изтрий потребител" deleteUserError = "ÐеуÑпешно изтриване на потребител" deleteUserSuccess = "ПотребителÑÑ‚ е изтрит уÑпешно" @@ -7341,6 +8014,8 @@ disable = "Деактивирай" disabled = "Деактивиран" editRole = "Редактиране на ролÑ" enable = "Ðктивирай" +locked = "заключен" +lockedBadge = "Заключен" loading = "Зареждане на хора..." loginRequired = "Първо активирайте режима за вход" member = "Член" @@ -7350,6 +8025,9 @@ searchMembers = "ТърÑене на членове..." status = "СтатуÑ" team = "Екип" title = "Хора" +unlockAccount = "Отключване на акаунт" +unlockUserError = "ÐеуÑпешно отключване на потребителÑки акаунт" +unlockUserSuccess = "ПотребителÑкиÑÑ‚ акаунт е отключен уÑпешно" user = "Потребител" [workspace.people.actions] diff --git a/frontend/public/locales/bo-CN/translation.toml b/frontend/public/locales/bo-CN/translation.toml index e284014a14..e611e0a324 100644 --- a/frontend/public/locales/bo-CN/translation.toml +++ b/frontend/public/locales/bo-CN/translation.toml @@ -8,6 +8,7 @@ black = "ནག་པོ" blue = "སྔོན་པོ" bored = "ཉོབ་སྣང་སà¾à¾±à½ºà½‘་པའི་སྒུག་བཟོà¼" cancel = "འདོར་བ" +confirm = "གà½à½“་འà½à½ºà½£" changedCredsMessage = "à½à½¼à¼‹à½ à½‚ོད་གནས་ཚུལ་བསྒྱུར་ཟིནà¼" chooseFile = "ཡིག་ཆ་འདེམསà¼" close = "སྒོ་རྒྱག" @@ -146,6 +147,7 @@ insufficientCredits = "བཀོལ་གྲངས་མི་འདངས༠loadingCredits = "བཀོལ་གྲངས་ཞིབ་བཤེར..." loadingProStatus = "མངག་འདོན་གནས་སྟངས་ཞིབ་བཤེར..." noticeTopUpOrPlan = "བཀོལ་གྲངས་མི་འདངས༠གྲངས་à½à¼‹à½¦à¾£à½¼à½“་བྱེད ཡང་ན་འཆར་གཞི་སྤེལ་རོགས་" +accessInvite = "མགྲོན་འབོད" [account] accountSettings = "རྩིས་à½à¾²à¼‹à½¦à¾’ྲིག་སྟངསà¼" @@ -1427,6 +1429,34 @@ title = "ལས་སྒྲུབ" description = "ལས་སྒྲུབ་ལས་ཀ་ཞིབ་བཤེར་བསྒུག་དགོས་པའི་དུས་ཚོད་མང་མà½à½ à¼" label = "ལས་སྒྲུབ་དུས་ཚོད་à½à½¼à½“་པ (སà¾à½¢à¼‹à½†)" +[admin.settings.storage] +description = "སར་བར་གསོག་འཇོག་དང་མཉམ་སྤྱོད་གདམ་ག་ཚོད་འཛིནà¼" +title = "ཡིག་ཆའི་གསོག་འཇོག་དང་མཉམ་སྤྱོད" + +[admin.settings.storage.enabled] +description = "མà½à½¼à¼‹à½¦à¾¤à¾²à½¼à½‘་པས་སར་བར་དུ་ཡིག་ཆ་ཉར་ཚགས་བྱེད་ཆོག." +label = "སར་བར་ཡིག་ཆ་ཉར་ཚགས་ལྕོགས་འགུལ" + +[admin.settings.storage.sharing.email] +description = "གློག་འཕྲིན་གྲངས་འཛིན་དང་མཉམ་སྤྱོད་ཆོག." +label = "གློག་འཕྲིན་མཉམ་སྤྱོད་ལྕོགས་འགུལ" +mailLink = "ཡིག་འཕྲིན་སྒྲིག་འགོད" +mailNote = "ཡིག་འཕྲིན་སྒྲིག་འགོད་དགོས༠" + +[admin.settings.storage.sharing.enabled] +description = "མà½à½¼à¼‹à½¦à¾¤à¾²à½¼à½‘་པས་ཉར་ཚགས་ཡིག་ཆ་མཉམ་སྤྱོད་ཆོག." +label = "མཉམ་སྤྱོད་ལྕོགས་འགུལ" + +[admin.settings.storage.sharing.links] +description = "ནང་འཇུག་བྱས་པའི་འབྲེལ་à½à½‚་བརྒྱུད་ནས་མཉམ་སྤྱོད་ཆོག." +frontendUrlLink = "མ་ལག་སྒྲིག་འགོད་ནང་སྒྲིག" +frontendUrlNote = "Frontend URL དགོས༠" +label = "མཉམ་སྤྱོད་འབྲེལ་à½à½‚་ལྕོགས་འགུལ" + +[admin.settings.storage.signing.enabled] +description = "མà½à½¼à¼‹à½¦à¾¤à¾²à½¼à½‘་པས་མི་སྣ་མང་མཉམ་ཞུགས་ཀྱི་ཡིག་ཆ་མཛུབ་འགན་ལས་འà½à½´à½¢à¼‹à½–ཟོ་ཆོག དགོས་ནས་སར་བར་ཡིག་ཆ་ཉར་ཚགས་ལྕོགས་འགུལ་ཡོད་དགོས་རེདà¼" +label = "ཚོགས་མི་མཛུབ་འགན (Alpha) ལྕོགས་འགུལ" + [admin.settings.unsavedChanges] cancel = "རྩོམ་སྒྲིགà¼" discard = "དཀའ་ངལ་གྱི་འགྱུར་བà¼" @@ -2059,7 +2089,19 @@ numbers = "ཨང་གྲངསà¼/à½à¾±à½–་à½à½¼à½„སà¼: ༥, ༡༠-༢ progressions = "ཡར་རྒྱས༠3n, 4n+1" [certSign] +allSigned = "ཞུགས་མà½à½“་ཚང་མས་མཛུབ་འགན་བཀལ་ཟིན༠མཇུག་བསྡུ་བཅས་ཡོདà¼" +awaitingSignatures = "མཛུབ་འགན་བསྒུག་བཞིན" +signatureProgress = "{{signedCount}}/{{totalCount}} མཛུབ་འགན" chooseCertificate = "ལག་འà½à¾±à½ºà½¢à¼‹à½¡à½²à½‚་ཆ་འདེམསà¼" +declined = "à½à½¦à¼‹à½£à½ºà½“་མ་བྱས" +fetchFailed = "མཛུབ་འགན་གནས་ཚུལ་འཇུག་མ་à½à½´à½–" +finalized = "མཇུག་བསྡུས" +notified = "བསྒུག་བཞིན" +partialNote = "ད་ལྟ་à½à½¼à½–་ཡོད་པའི་མཛུབ་འགན་དང་གཅིག་à½à½¢à¼‹à½¦à¾”ོན་དུ་མཇུག་བསྡུ་ཆོག མཛུབ་འགན་མ་བཀལ་མི་ཚང་མ་བུ་གà½à½¼à½¢à¼‹à½–ྱེདà¼" +pending = "བསྒུག་བཞིན" +readyToFinalize = "མཇུག་བསྡུ་བཅས" +signed = "མཛུབ་འགན་བཀལ་ཟིན" +viewed = "ལྟ་ཞིབ་བྱས" chooseJksFile = "JKSཡིག་ཆ་འདེམསà¼" chooseP12File = "PKCS12ཡིག་ཆ་འདེམསà¼" choosePfxFile = "PFXཡིག་ཆ་འདེམསà¼" @@ -2082,6 +2124,7 @@ title = "ལག་འà½à¾±à½ºà½¢à¼‹à½˜à½²à½„་རྟགས་བཀོད་ invisible = "མི་མངོན་པà¼" stepTitle = "མཚན་རྟགས་à½à½¼à½“་སà¾à¾±à½ºà½£à¼" visible = "མངོན་ཤེསà¼" +visibility = "མà½à½¼à½„་རུང་གནས" [certSign.appearance.options] title = "མཚན་རྟགས་ཞིབ་ཕྲà¼" @@ -2188,6 +2231,252 @@ bullet4 = "བདེན་དཔང་ཆེད་རང་སྒྲིག་ལ text = "à½à¾±à½ºà½‘་ཀྱིས་མིང་རྟགས་བརྟག་དཔྱད་བྱེད་སà¾à½–ས་ལག་ཆ་དེས་à½à¾±à½ºà½‘་ལ་དེ་དག་ནུས་ལྡན་ཡིན་མིན་དང་༠ཡིག་ཆ་དེ་ལ་མིང་རྟགས་བཀོད་པ༠མིང་རྟགས་བཀོད་པà¼" title = "མཚན་རྟགས་ལ་ཞིབ་བཤེར་བྱེད་པà¼" +[certSign.collab.finalize] +button = "མཇུག་བསྡུས་ནས་མཛུབ་འགན་PDF འཇུག" +early = "དང་à½à½¼à½‚་མའི་མཛུབ་འགན་དང་གཅིག་à½à½¢à¼‹à½˜à½‡à½´à½‚་བསྡུ" + +[certSign.collab.sessionDetail] +addButton = "ཞུགས་མà½à½“་à½à¼‹à½¦à¾£à½¼à½“" +addParticipants = "ཞུགས་མà½à½“་à½à¼‹à½¦à¾£à½¼à½“" +addParticipantsError = "ཞུགས་མà½à½“་à½à¼‹à½¦à¾£à½¼à½“་ཕམ་པ" +backToList = "ལས་འà½à½´à½¢à¼‹à½à½¼à½¢à¼‹à½£à½¼à½‚" +deleteConfirm = "ངེས་བརྟན་ཡིན་ནམ༠བསà¾à¾±à½¢à¼‹à½˜à½ºà½‘à¼" +deleteError = "ལས་འà½à½´à½¢à¼‹à½–སུབ་མ་à½à½´à½–" +deleted = "ལས་འà½à½´à½¢à¼‹à½–སུབ་ཟིན" +deleteSession = "ལས་འà½à½´à½¢à¼‹à½–སུབ" +dueDate = "དུས་བཀག" +finalizeError = "ལས་འà½à½´à½¢à¼‹à½˜à½‡à½´à½‚་བསྡུ་མ་à½à½´à½–" +loadPdfError = "མཛུབ་འགན་PDF འཇུག་མ་à½à½´à½–" +loadSignedPdf = "མཛུབ་འགན་PDF འགུལ་སྤྱོད་ཡིག་ཆ་ནང་འཇུག" +messageLabel = "འཕྲིན་དོན" +noAdditionalInfo = "ཟུར་དུ་གནས་ཚུལ་མེད" +owner = "བདག་པོ" +participantRemoved = "ཞུགས་མà½à½“་བསུབ་ཟིན" +participants = "ཞུགས་མà½à½“" +participantsAdded = "ཞུགས་མà½à½“་à½à¼‹à½¦à¾£à½¼à½“་བྱས་ཚར" +removeParticipant = "བསུབ" +removeParticipantError = "ཞུགས་མà½à½“་བསུབ་མ་à½à½´à½–" +selectUsers = "མི་སྣ་འདེམས..." +sessionInfo = "ལས་འà½à½´à½¢à¼‹à½‚ནས་ཚུལ" +workbenchTitle = "ལས་འà½à½´à½¢à¼‹à½‘ོ་དམ" + +[certSign.collab.signRequest] +addedToFiles = "ཡིག་ཆ་འགུལ་སྤྱོད་ཡིག་ཆར་à½à¼‹à½¦à¾£à½¼à½“་བྱས་ཟིན" +addSignature = "à½à¾±à½ºà½‘་ཀྱི་མཛུབ་འགན་à½à¼‹à½¦à¾£à½¼à½“" +addToFiles = "འགུལ་སྤྱོད་ཡིག་ཆར་à½à¼‹à½¦à¾£à½¼à½“" +advancedSettings = "མà½à½¼à¼‹à½¢à½²à½˜à¼‹à½¦à¾’ྲིག་འགོད" +backToList = "མཛུབ་འགན་ཞུ་གà½à½¼à½„་à½à½¼à½¢à¼‹à½£à½¼à½‚" +certificateChoice = "མཛུབ་འགན་ལ་སྤྱོད་པའི་ལག་འà½à¾±à½ºà½¢à¼‹à½ à½‘ེམས" +changeSignature = "མཛུབ་འགན་བརྗེ" +clearSignature = "མཛུབ་འགན་བསུབ" +completeAndSign = "མཇུག་སྒྲིལ་དང་མཛུབ་འགན" +createNewSignature = "མཛུབ་འགན་གསར་བ་བཟོ" +declineButton = "à½à½¦à¼‹à½£à½ºà½“་མ་བྱས" +decline = "ཞུ་གà½à½¼à½„་à½à½¦à¼‹à½£à½ºà½“་མ་བྱས" +deleteSelected = "མཛུབ་འགན་འདེམས་པ་བསུབ" +drawSignature = "མར་འོག་ལ་à½à¾±à½ºà½‘་ཀྱི་མཛུབ་འགན་འབྲི" +dueDate = "དུས་བཀག" +fileTooLarge = "ཡིག་ཆའི་ཆེ་ཆུང 5MB ལས་ཉུང་དགོས" +fontFamily = "ཡིག་གཟུགས" +fontSize = "ཡིག་གཟུགས་ཆེ་ཆུང: {{size}}px" +fontSizePlaceholder = "ཆེ་ཆུང" +from = "བརྒྱུད་ནས" +invalidCertFile = "P12 ཡང་ན PFX ལག་འà½à¾±à½ºà½¢à¼‹à½¡à½²à½‚་ཆ་འདེམས་རོགསà¼" +invalidFileType = "པར་ཡིག་ཆ་འདེམས་རོགསà¼" +location = "གནས་ཡུལ (གདམ་à½)" +locationPlaceholder = "à½à¾±à½ºà½‘་ཀྱིས་ག་པར་མཛུབ་འགན་བཀལ?" +message = "འཕྲིན་དོན" +noCertificate = "ལག་འà½à¾±à½ºà½¢à¼‹à½¡à½²à½‚་ཆ་འདེམས་རོགསà¼" +noSignatures = "PDF ཡི་ནང་ཉུང་མà½à½¢à¼‹à½¡à½„་མཛུབ་འགན་གཅིག་བཞག་རོགསà¼" +p12File = "P12/PFX ལག་འà½à¾±à½ºà½¢à¼‹à½¡à½²à½‚་ཆ" +password = "གསང་ཨང" +passwordPlaceholder = "གསང་ཨང་འཇུག..." +penColor = "སྨྱུག་གུའི་à½à¼‹à½‘ོག" +penSize = "སྨྱུག་གུ་ཆེ་ཆུང: {{size}}px" +placementActive = "PDF ལ་མནན་ནས་བཞག" +placeSignatureButton = "PDF སྟེང་མཛུབ་འགན་བཞག" +reason = "རྒྱུ་མཚན (གདམ་à½)" +reasonPlaceholder = "ཅིའི་ཕྱིར་མཛུབ་འགན་བཀལ?" +removeImage = "པར་བསུབ" +removeCertFile = "ཡིག་ཆ་བསུབ" +savedSignatures = "ཉར་ཚགས་བྱས་པའི་མཛུབ་འགན" +selectFile = "པར་ཡིག་ཆ་འདེམས" +selectSignatureTitle = "མཛུབ་འགན་འདེམས་ཡང་ན་གསར་བ་བཟོ" +signButton = "ཡིག་ཆར་མཛུབ་འགན" +signatureInfo = "སྒྲིག་འགོད་འདི་ཡིག་ཆའི་བདག་པོས་སྒྲིག་ཡོདà¼" +signaturePlaced = "ཤོག་ངོས་སྟེང་མཛུབ་འགན་བཞག་ཟིན" +signatureSettings = "མཛུབ་འགན་སྒྲིག་འགོད" +signatureText = "མཛུབ་འགན་ཡི་གེ" +signatureTextPlaceholder = "à½à¾±à½ºà½‘་ཀྱི་མིང་འཇུག..." +signatureTypeLabel = "མཛུབ་འགན་རིགས" +signingTitle = "མཛུབ་འགན" +textColor = "ཡི་གེའི་à½à¼‹à½‘ོག" +typeSignature = "à½à¾±à½ºà½‘་ཀྱི་མིང་འཇུག་ནས་མཛུབ་འགན་བཟོ" +uploadCert = "ལག་འà½à¾±à½ºà½¢à¼‹à½¦à¾²à½¼à½£à¼‹à½‚སར" +uploadCertDesc = "à½à¾±à½ºà½‘་རང་གི P12/PFX ལག་འà½à¾±à½ºà½¢à¼‹à½¦à¾¤à¾±à½¼à½‘" +uploadSignature = "à½à¾±à½ºà½‘་ཀྱི་མཛུབ་འགན་པར་ཡར་འཇུག" +usePersonalCert = "མི་དབང་ལག་འà½à¾±à½ºà½¢" +usePersonalCertDesc = "à½à¾±à½ºà½‘་ཀྱི་à½à¼‹à½–ྱང་ལ་རང་འགུལ་བཟོས" +useServerCert = "སྒྲིག་སྡེའི་ལག་འà½à¾±à½ºà½¢" +useServerCertDesc = "སྒྲིག་སྡེ་མཉམ་སྤྱོད་ལག་འà½à¾±à½ºà½¢" +workbenchTitle = "མཛུབ་འགན་རྒྱུ་འདོན" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "འབྲི་རིས་à½à¼‹à½‘ོག་གདམ" +continue = "མུ་མà½à½´à½‘" + +[certSign.collab.signRequest.certModal] +description = "à½à¾±à½ºà½‘་ཀྱིས {{count}} མཛུབ་འགན་བཞག་ཟིན༠མཇུག་མà½à½´à½“་བྱེད་པར་ལག་འà½à¾±à½ºà½¢à¼‹à½‚དམ་གནང་à¼" +sign = "ཡིག་ཆར་མཛུབ་འགན" +certValidating = "ལག་འà½à¾±à½ºà½¢à¼‹à½–དེན་བཤད་བཤེར་བཞིན..." +certValidUntil = "ལག་འà½à¾±à½ºà½¢à¼‹à½“ུས་ཡོད་པའི་ཚེས: {{date}}" +certInvalid = "ལག་འà½à¾±à½ºà½¢à¼‹à½“ུས་མེད: {{error}}" +certInvalidFallback = "ལག་འà½à¾±à½ºà½¢à¼‹à½“ུས་མེད" +certNetworkError = "ལག་འà½à¾±à½ºà½¢à¼‹à½–དེན་བཤད་བྱས་མི་à½à½´à½–" +title = "ལག་འà½à¾±à½ºà½¢à¼‹à½¦à¾’ྲིག་འགོད" + +[certSign.collab.signRequest.image] +hint = "à½à¾±à½ºà½‘་ཀྱི་མཛུབ་འགན་ PNG ཡང་ན JPG པར་ཡར་འཇུག" + +[certSign.collab.signRequest.mode] +move = "མཛུབ་འགན་སྤོ" +place = "མཛུབ་འགན་བཞག" +title = "མཛུབ་འགན་ཡང་ན་སྤོའི་རྣམ་པ" + +[certSign.collab.signRequest.modeTabs] +draw = "འབྲི" +image = "ཡར་འཇུག" +text = "ཡི་གེ" + +[certSign.collab.signRequest.placeSignature] +message = "PDF སྟེང་ལ་མནན་ནས་à½à¾±à½ºà½‘་ཀྱི་མཛུབ་འགན་བཞག" +title = "མཛུབ་འགན་བཞག" + +[certSign.collab.signRequest.preview] +imageAlt = "མཛུབ་འགན་འདེམས་ཟིན" +missing = "སྔོན་ལྟ་མེད" +textFallback = "མཛུབ་འགན" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "འབྲིས་པའི་མཛུབ་འགན" +defaultImageLabel = "ཡར་འཇུག་པར་མཛུབ་འགན" +defaultLabel = "མཛུབ་འགན" +defaultTextLabel = "ཡི་གེ་མཛུབ་འགན" +delete = "མཛུབ་འགན་བསུབ" +none = "ཉར་ཚགས་མཛུབ་འགན་མེད" + +[certSign.collab.signRequest.signatureType] +draw = "འབྲི" +type = "ཡི་གེ" +upload = "ཡར་འཇུག" + +[certSign.collab.signRequest.steps] +back = "ཕྱིར་ལོག" +cancelPlacement = "བཞག་བཤོལ" +certificate = "ལག་འà½à¾±à½ºà½¢" +clickMultipleTimes = "PDF ལ་མི་ཚུན་མང་རྟགས་མནན་ནས་མཛུབ་འགན་བཞག༠མཛུབ་འགན་གང་རུང་དེ་འདྲུད་ནས་སྤོ་བཟོ་དང་ཆེ་ཆུང་བསྒྱུར་ཆོག." +clickToPlace = "PDF ཡི་ས་གནས་ཀྱིས་à½à¾±à½ºà½‘་ཀྱི་མཛུབ་འགན་མངོན་དགོས་པ་ལ་མནནà¼" +continue = "ལག་འà½à¾±à½ºà½¢à¼‹à½ à½‘ེམས་སུ་མུ་མà½à½´à½‘" +continueToPlacement = "བཞག་སྒང་ལ་མུ་མà½à½´à½‘" +continueToReview = "བསà¾à¾±à½¢à¼‹à½žà½²à½–་ལ་མུ་མà½à½´à½‘" +createSignature = "མཛུབ་འགན་བཟོ" +invisible = "མà½à½¼à½„་མི་རུང" +location = "གནས་ཡུལ:" +multipleSignatures = "{{count}} མཛུབ་འགན PDF ལ་འཇུག" +oneSignature = "1 མཛུབ་འགན་ PDF ལ་འཇུག" +placeOnPdf = "PDF ལ་བཞག" +reason = "རྒྱུ་མཚན:" +reviewTitle = "མཛུབ་འགན་བྱེད་པའི་སྔོན་ལྟ" +signaturePlaced = "ཤོག་ངོས་ {{page}} སྟེང་མཛུབ་འགན་བཞག་ཟིན༠བསà¾à¾±à½¢à¼‹à½˜à½“ན་ནས་གནས་སར་བཅོས་བཟོ་ཡང་ན་བསà¾à¾±à½¢à¼‹à½žà½²à½–་ལ་མུ་མà½à½´à½‘à¼" +visible = "མà½à½¼à½„་རུང" +visibility = "མà½à½¼à½„་རུང་གནས:" +yourSignatures = "à½à¾±à½ºà½‘་ཀྱི་མཛུབ་འགན ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "à½à¼‹à½‘ོག" +fontLabel = "ཡིག་གཟུགས" +fontSizeLabel = "ཆེ་ཆུང" +fontSizePlaceholder = "16" +label = "མཛུབ་འགན་ཡི་གེ" +modalHint = "à½à¾±à½ºà½‘་ཀྱི་མིང་འཇུག་བྱས་à½à½º མུ་མà½à½´à½‘་མནན་ནས PDF ལ་བཞག." +placeholder = "à½à¾±à½ºà½‘་ཀྱི་མིང་འཇུག..." + +[certSign.collab.participant] +certValidating = "ལག་འà½à¾±à½ºà½¢à¼‹à½–དེན་བཤད་བཤེར་བཞིན..." +certValid = "✓ ལག་འà½à¾±à½ºà½¢à¼‹à½“ུས་ཡོད" +certValidUntil = " {{date}} བར" +certInvalid = "✗ {{error}}" +certInvalidFallback = "ལག་འà½à¾±à½ºà½¢à¼‹à½“ུས་མེད" +certNetworkError = "ལག་འà½à¾±à½ºà½¢à¼‹à½–དེན་བཤད་བྱས་མི་à½à½´à½–" + +[certSign.collab.addParticipants] +add = "ཞུགས་མà½à½“ {{count}} à½à¼‹à½¦à¾£à½¼à½“" +back = "ཕྱིར་ལོག" +configureSignatures = "མཛུབ་འགན་སྒྲིག་འགོད་སྒྲིག" +continue = "མཛུབ་འགན་སྒྲིག་འགོད་ལ་མུ་མà½à½´à½‘" +reasonHelp = "ཞུགས་མà½à½“་འདི་ཚོའི་མཛུབ་འགན་རྒྱུ་མཚན་སྔོན་འགུལ་འཇོག (གདམ་འà½à½¼à½„་ཚོས་མཛུབ་འགན་སà¾à½–ས་བརྗེ་ཆོག)" +reasonPlaceholder = "e.g. Approval, Review..." +selectUsers = "མི་སྣ་འདེམས" + +[certSign.collab.sessionCreation] +includeSummaryPage = "མཛུབ་འགན་གནས་བྱང་བརྗོད་པའི་ཤོག་ངོས་à½à¼‹à½¦à¾£à½¼à½“" +includeSummaryPageHelp = "མཇུག་ལ་མཛུབ་འགན metadata ཡོངས་ཀྱི་བརྗོད་པ་ཡོད་པའི་ཤོག་ངོས་à½à¼‹à½¦à¾£à½¼à½“་བྱེད༠ཤོག་ངོས་སོ་སོའི་ནང་གི་དགོངས་ཡིག་ལག་འà½à¾±à½ºà½¢à¼‹à½˜à½›à½´à½–་འགན་སྒྲོག་གà½à½¼à½¢à¼‹à½–ྱེད་(wet signatures ལ་ཕན་མེད)à¼" + +[certSign.collab.sessionList] +active = "འགུལ་སྤྱོད" +finalized = "མཇུག་བསྡུས" + +[certSign.collab.signatureSettings] +description = "ཞུགས་མà½à½“་ཚང་མའི་མཛུབ་འགན་མངོན་སྟོན་ཇི་ལྟར་མà½à½´à½“་འགྱུར་བྱ་དགོས་མེདà¼" +title = "མཛུབ་འགན་མངོན་སྟོན" + +[certSign.collab.userSelector] +inviteUsers = "མི་སྣ་à½à¼‹à½¦à¾£à½¼à½“" +loadError = "མི་སྣ་འཇུག་མ་à½à½´à½–" +noTeam = "ཚོགས་མིང་མེད" +noUsers = "མི་སྣ་གཞན་མ་རྙེདà¼" +placeholder = "མི་སྣ་འདེམས..." + +[certSign.mobile] +panelActions = "བྱ་བ" +panelDocument = "ཡིག་ཆ" +panelPeople = "མི་སྣ" + +[certSign.sessions] +deleted = "ལས་འà½à½´à½¢à¼‹à½–སུབ་ཟིན" +fetchFailed = "ལས་འà½à½´à½¢à¼‹à½‚ནས་ཚུལ་འཇུག་མ་à½à½´à½–" +finalized = "ལས་འà½à½´à½¢à¼‹à½˜à½‡à½´à½‚་བསྡུས" +loaded = "མཛུབ་འགན་PDF འཇུག་ཟིན" +pdfNotReady = "PDF གྲ་སྒྲིག་མེད" +pdfNotReadyDesc = "མཛུབ་འགན་PDF བཟོ་བའི་སྒང་ཡིན༠སà¾à½¢à¼‹à½˜à¼‹à½¤à½´à½£à¼‹à½£à¼‹à½¡à½„་བཤལ་ལས་ཚོད་ལྟ་རོགསà¼" + +[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 སྲོལ་གསར་ཡར་འཇུག" + [changeCreds] changePassword = "à½à¾±à½ºà½‘་ཀྱིས་སྔོན་སྒྲིག་ནང་འཛུལ་གྱི་ཡིག་ཆ་བེད་སྤྱོད་བྱེད་ཀྱི་ཡོད༠གསང་གྲངས་གསར་པ་བླུགས་རོགསà¼" changeUsername = "à½à¾±à½ºà½‘་རང་གི་སྤྱོད་མà½à½“་གྱི་མིང་གསར་བརྗེ༠à½à¾±à½ºà½‘་རང་གསར་བརྗེ་བྱས་རྗེས་ཕྱིར་à½à½¼à½“་འགྲོ་གི་རེདà¼" @@ -3242,6 +3531,46 @@ totalSelected = "བསྡོམས་འདེམས་པà¼" unsupported = "རྒྱབ་སà¾à¾±à½¼à½¢à¼‹à½˜à½ºà½‘་པà¼" unzip = "ཟིཔà¼" uploadError = "ཡིག་ཆ་འགའ་ཤས་ཡར་བསà¾à½´à½¢à¼‹à½˜à¼‹à½à½´à½–་པ་རེདà¼" +copyCreated = "པར་འདེབས་འདི་à½à½–ས་འཕྲུལ་འདིའི་à½à½¼à½‚་ཉར་ཟིནà¼" +copyFailed = "པར་འདེབས་བཟོ་མ་à½à½´à½–à¼" +leaveShare = "ངའི་à½à½¼à½ à½²à¼‹à½“ས་བསུབ" +leaveShareFailed = "མཉམ་སྤྱོད་ཡིག་ཆ་བསུབ་མ་à½à½´à½–à¼" +leaveShareSuccess = "à½à¾±à½ºà½‘་ཀྱི་མཉམ་སྤྱོད་à½à½¼à¼‹à½¡à½²à½‚་ནས་བསུབ་ཟིནà¼" +removeBoth = "གཉིས་ཀ་ནས་བསུབ" +removeFilePrompt = "ཡིག་ཆ་འདི་à½à½–ས་འཕྲུལ་འདི་དང་à½à¾±à½ºà½‘་ཀྱི་སར་བར་གཉིས་ཀ་ལ་ཉར་ཡོད༠གནས་ས་གང་ནས་བསུབ་དགོས?" +removeFileTitle = "ཡིག་ཆ་བསུབ" +removeLocalOnly = "à½à½–ས་འཕྲུལ་འདི་པོ་ཙམ" +removeServerFailed = "སར་བར་ནས་ཡིག་ཆ་བསུབ་མ་à½à½´à½–à¼" +removeServerOnly = "སར་བར་པོ་ཙམ" +removeServerOnlyPrompt = "ཡིག་ཆ་འདི་à½à¾±à½ºà½‘་ཀྱི་སར་བར་ནང་ཙམ་ཉར་ཡོད༠སར་བར་ནས་བསུབ་དགོས་སམ?" +removeServerSuccess = "སར་བར་ནས་བསུབ་ཟིནà¼" +removeSharedPrompt = "ཡིག་ཆ་འདི་à½à¾±à½ºà½‘་ལ་མཉམ་སྤྱོད་བྱས་ཟིན༠à½à½–ས་འཕྲུལ་འདིའམ མཉམ་སྤྱོད་à½à½¼à¼‹à½¡à½²à½‚་ནས་བསུབ་ཆོག." +removeSharedServerOnlyBlockedPrompt = "ཡིག་ཆ་འདི་à½à¾±à½ºà½‘་ལ་མཉམ་སྤྱོད་བྱས་པ་དང་སར་བར་ཙམ་སྟེང་ཉར་ཡོདà¼" +removeSharedServerOnlyPrompt = "ཡིག་ཆ་འདི་à½à¾±à½ºà½‘་ལ་མཉམ་སྤྱོད་བྱས་པ་དང་སར་བར་ཙམ་སྟེང་ཉར་ཡོད༠à½à¾±à½ºà½‘་ཀྱི་à½à½¼à¼‹à½¡à½²à½‚་ནས་བསུབ་དགོས་སམ?" +changesNotUploaded = "བཟོ་བཅོས་ཡར་འཇུག་མ་བྱས" +cloudFile = "Cloud ཡིག་ཆ" +filterAll = "ཡོངས" +filterLocal = "རང་ས" +filterSharedByMe = "ངས་མཉམ་སྤྱོད" +filterSharedWithMe = "ང་ལ་མཉམ་སྤྱོད" +lastSynced = "མཉམ་བསྒྲིག་à½à½ºà½„ས་མཇུག" +localOnly = "རང་ས་པོ་ཙམ" +makeCopy = "པར་འདེབས་བྱེད" +owner = "བདག་པོ" +ownerUnknown = "མ་ཤེས" +share = "མཉམ་སྤྱོད" +shareSelected = "འདེམས་པ་མཉམ་སྤྱོད" +sharedByYou = "à½à¾±à½ºà½‘་ཀྱིས་མཉམ་སྤྱོད" +sharedEditNoticeBody = "སར་བར་ཡིག་ཆ་འདིའི་རྩོམ་སྒྲིག་དབང་ཆ་མེད༠à½à¾±à½ºà½‘་ཀྱིས་བཟོ་བཅོས་བྱས་པ་དེ་རང་ས་དུ་ཉར་ཡོད་པའི་ཀླད་ཀོར་པར་འདེབས་སྦེ་ཉར་ཚགས་བྱེདà¼" +sharedEditNoticeConfirm = "གོ་སོང་" +sharedEditNoticeTitle = "སར་བར་མངོན་མà½à½¼à½„་ཙམ" +sharedWithYou = "à½à¾±à½ºà½‘་ལ་མཉམ་སྤྱོད" +sharing = "མཉམ་སྤྱོད" +storageState = "གསོག་འཇོག" +synced = "མཉམ་བསྒྲིག" +updateOnServer = "སར་བར་གསར་སྒྱུར" +uploadSelected = "འདེམས་པ་ཡར་འཇུག" +uploadToServer = "སར་བར་ཡར་འཇུག" [files] addFiles = "ཡིག་ཆ་à½à¼‹à½¦à¾£à½¼à½“à¼" @@ -3367,6 +3696,77 @@ title = "PDFs ཕ་ལེཊ་ཊེན་སི་སà¾à½¼à½¢à¼" discord = "མི་མà½à½´à½“་པà¼" issues = "གྷི་ཊི་ཧབà¼" +[groupSigning.tooltip] +header = "ཚོགས་མི་མཛུབ་འགན་སà¾à½¼à½¢" + +[groupSigning.tooltip.finalization] +bullet1 = "མཛུབ་འགན་ཚང་མ་à½à¾±à½ºà½‘་ཀྱིས་བཀོད་པའི་ཞུགས་མà½à½“་གྱི་རིམ་པ་ལྟར་འཇུག" +bullet2 = "དགོས་ཚེ་མཛུབ་འགན་ཕྱོགས་མེད་ཀྱིས་མཇུག་བསྡུ་ཆོག" +bullet3 = "མཇུག་བསྡུས་ཟིན་པའི་རྗེས་ལས་ལས་འà½à½´à½¢à¼‹à½–ཅོས་མི་à½à½´à½–" +description = "མཇུག་བསྡུས་པས་མཛུབ་འགན་ཡོངས་ཀྱིས་གཅིག་པའི་མཛུབ་འགན་PDF བཟོ་བྱེད༠འདི་བསà¾à¾±à½¢à¼‹à½˜à½ºà½‘à¼" +title = "མཇུག་བསྡུས་ལས་སà¾à¾±à½¼à½‘" + +[groupSigning.tooltip.roles] +bullet1 = "བདག་པོ (à½à¾±à½ºà½‘): ལས་འà½à½´à½¢à¼‹à½–ཟོ, མཛུབ་འགན་སྔོན་སྒྲིག, ཡིག་ཆ་མཇུག་བསྡུ" +bullet2 = "ཞུགས་མà½à½“: མཛུབ་འགན་བཟོ, ལག་འà½à¾±à½ºà½¢à¼‹à½ à½‘ེམས, PDF སྟེང་བཞག" +bullet3 = "ཞུགས་མà½à½“་ཚོས མà½à½¼à½„་རུང་/རྒྱུ་མཚན/གནས་ཡུལ སྒྲིག་འགོད་བཅོས་མི་à½à½´à½–" +description = "à½à¾±à½ºà½‘་ཀྱིས་ཞུགས་མà½à½“་ཚང་མའི་མཛུབ་འགན་མངོན་སྟོན་སྒྲིག་ཚོད་འཛིན་བྱེདà¼" +title = "ཞུགས་མà½à½“་ལས་འགན" + +[groupSigning.tooltip.sequential] +bullet1 = "ཞུགས་མà½à½“་དང་པོས་མཛུབ་འགན་བཀལ་བྱས་རྗེས་གཉིས་པས་ཡིག་ཆར་འཛུལ་ཆོག" +bullet2 = "à½à¾²à½²à½˜à½¦à¼‹à½£à¾¡à½“་གྲོས་མà½à½´à½“་གྱི་རིམ་པ་ལ་འཚམ" +bullet3 = "à½à½¼à¼‹à½¡à½²à½‚་ནས་འདྲུད་ནས་ཞུགས་མà½à½“་རིམ་པ་བརྗེ་ཆོག" +description = "ཞུགས་མà½à½“་ཚོས་à½à¾±à½ºà½‘་ཀྱིས་བཀོད་པའི་རིམ་པ་ལྟར་མཛུབ་འགན་བཀལ༠རེ་ཞུགས་མà½à½“་རེ་ལ་རིམ་པ་ཡོངས་ནས་གསར་བརྡ་འབྱོརà¼" +title = "རིམ་པ་རིམ་པའི་མཛུབ་འགན" + +[groupSigning.steps] +back = "ཕྱིར་ལོག" +completed = "མཇུག་བསྡུས" +current = "མིག་སྔའི" +stepLabel = "རིམ་པ {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "བསà¾à¾±à½¢à¼‹à½žà½²à½–་ལ་མུ་མà½à½´à½‘" +invisible = "མཛུབ་འགན་རྣམས་མà½à½¼à½„་མི་རུང (metadata ཙམ)" +locationLabel = "གནས་ཡུལ:" +preview = "སྔོན་ལྟ" +reasonLabel = "རྒྱུ་མཚན:" +title = "མཛུབ་འགན་སྒྲིག་འགོད" +visible = "མཛུབ་འགན་རྣམས་ {{page}} ཤོག་ངོས་སྟེང་མà½à½¼à½„་རུང" + +[groupSigning.steps.review] +document = "ཡིག་ཆ" +dueDate = "དུས་བཀག (གདམ་à½)" +dueDatePlaceholder = "དུས་བཀག་འདེམས..." +invisible = "མà½à½¼à½„་མི་རུང (metadata ཙམ)" +location = "གནས་ཡུལ:" +logo = "ལས་རྟགས:" +logoHidden = "ལས་རྟགས་མེད" +logoShown = "Stirling PDF ལས་རྟགས་མངོན" +participants = "ཞུགས་མà½à½“" +reason = "རྒྱུ་མཚན:" +send = "མཛུབ་འགན་ཞུ་གà½à½¼à½„" +signatureSettings = "མཛུབ་འགན་སྒྲིག་འགོད" +title = "ལས་འà½à½´à½¢à¼‹à½žà½²à½–་བཤེར" +titleShort = "ཞིབ་བཤེར་དང་གà½à½¼à½„" +visibility = "མà½à½¼à½„་རུང་གནས:" +visible = "{{page}} ཤོག་ངོས་སྟེང་མà½à½¼à½„་རུང" +participantCount = "{{count}} ཞུགས་མà½à½“་རིམ་པ་ལྟར་མཛུབ་འགན" + +[groupSigning.steps.selectDocument] +continue = "ཞུགས་མà½à½“་འདེམས་ལ་མུ་མà½à½´à½‘" +noFile = "མཛུབ་འགན་ལས་འà½à½´à½¢à¼‹à½–ཟོ་བར à½à¾±à½ºà½‘་ཀྱི་འགུལ་སྤྱོད་ཡིག་ཆ་ནས PDF ཡིག་ཆ་གཅིག་འདེམས་རོགསà¼" +selectedFile = "འདེམས་ཟིན་པའི་ཡིག་ཆ" +title = "ཡིག་ཆ་འདེམས" + +[groupSigning.steps.selectParticipants] +continue = "མཛུབ་འགན་སྒྲིག་འགོད་ལ་མུ་མà½à½´à½‘" +count = "{{count}} ཞུགས་མà½à½“་འདེམས་ཟིན" +label = "ཞུགས་མà½à½“་འདེམས" +placeholder = "མཛུབ་འགན་ལ་ཞུགས་པའི་མི་སྣ་འདེམས..." +title = "ཞུགས་མà½à½“་འདེམས" + [getPdfInfo] downloadJson = "JSON ཕབ་ལེནà¼" downloads = "ཕབ་ལེནà¼" @@ -4460,7 +4860,10 @@ zoomOut = "ཆུང་དུ་བཟོà¼" [viewer] cannotPreviewFile = "སྔོན་ལྟའི་ཡིག་འབྲུའི་ཡིག་ཚགས་བཟོ་མི་རུང་à¼" +disableColorFilter = "à½à¼‹à½‘ོག་འཚག་བཟོ་བར་བཀག" dualPageView = "ཤོག་ངོས་གཉིས་ལྡན་གྱི་མà½à½¼à½„་སྣང་à¼" +enableDarkFilter = "མུན་མà½à½¼à½„་འཚག་བཟོ་ལྕོགས་འགུལ" +enableSepiaFilter = "Sepia འཚག་བཟོ་ལྕོགས་འགུལ" firstPage = "ཤོག་ངོས་དང་པོà¼" lastPage = "ཤོག་ངོས་མà½à½ à¼‹à½˜à¼" nextPage = "ཤོག་ངོས་རྗེས་མà¼" @@ -4470,6 +4873,22 @@ singlePageView = "ཤོག་ངོས་གཅིག་པའི་མà½à½¼ unknownFile = "མ་ཤེས་པའི་ཡིག་ཆà¼" zoomIn = "Zoom in" zoomOut = "ཆུང་དུ་བཟོà¼" +resetZoom = "རྒྱས་ཚད་བསà¾à¾±à½¢à¼‹à½ à½‡à½¼à½‚" + +[viewer.nonPdf] +fileTypeBadge = "{{type}} ཡིག་ཆ" +convertToPdf = "PDF ལ་བསྒྱུར" +loading = "སྒྲུབ་བརྒྱབ..." +emptyFile = "ཡིག་ཆ་སྟོང་" +csvStats = "{{rows}} གྲལ · {{columns}} སྟར · {{size}}" +sortedBy = "གོང་འབྱེད: {{column}}" +columnDefault = "གྲལ་à½à½²à½‚ {{index}}" +htmlPreviewWarning = "HTML སྔོན་ལྟ — ཕྱི་à½à½¼à½“་à½à½´à½„ས་ཚུད་མི་སྲིད · {{size}}" +htmlPreview = "HTML སྔོན་ལྟ" +invalidJson = "JSON ནུས་མེད — ནང་དོར་à½à½¼à½‚་མངོན" +textStats = "{{lines}} ཕྲེང · {{size}}" +lineNumbers = "ཕྲེང་གྲངས" +renderMarkdown = "markdown མངོན" [viewer.attachments] title = "ཟུར་སྣོན" @@ -4531,6 +4950,7 @@ toggleAttachments = "ཟུར་སྣོན་སྟོན/སྦེལ" toggleTheme = "བསྒྱུར་བའི་བརྗོད་གཞིà¼" language = "སà¾à½‘་རིགས" toggleAnnotations = "བསྒུལ་བསྒྱུར༠མིང་ཚིག་མà½à½¼à½„་à½à½´à½–་ཚདà¼" +toggleLayers = "སྒང་རིས་བསྒྱུར" search = "འཚོལ་ཞིབ་PDF" panMode = "པན་à½à½–ས་ཀྱི་à½à½–ས་ལམà¼" applyRedactionsFirst = "དེ་སྔོན་ལ་གསང་སྦེད་འཇུག" @@ -5407,20 +5827,72 @@ title = "པར་སà¾à¾²à½´à½“à¼" 2 = "པར་འཕྲུལ་གྱི་མིང་བླུགསà¼" [quickAccess] +access = "འཛུལ་ས" +accessAddPerson = "མི་མངོན་དུ་à½à¼‹à½¦à¾£à½¼à½“" +accessBack = "ཕྱིར་ལོག" +accessCopyLink = "འབྲེལ་à½à½‚་འà½à½ºà½“" +accessEmail = "གློག་འཕྲིན་གནས་ཡུལ" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "ཡིག་ཆ" +accessGeneral = "དམིགས་མེད་འཛུལ་ས" +accessInviteTitle = "མི་སྣ་མགྲོན་འབོད" +accessOwner = "བདག་པོ" +accessPanel = "ཡིག་ཆའི་འཛུལ་ས" +accessPeople = "འཛུལ་ས་ཡོད་པའི་མི་ཚོ" +accessRemove = "བསུབ" +accessRestricted = "ཚད་འཛིན" +accessRestrictedHint = "འཛུལ་ས་ཡོད་པའི་མི་ཙམ་à½à¼‹à½•ྱེ" +accessRole = "ལས་འགན" +accessRoleCommenter = "མཆན་འགྲེལ་པ" +accessRoleEditor = "ཞུན་དག་པ" +accessRoleViewer = "ལྟ་མà½à½“" +accessSelectedFile = "འདེམས་ཟིན་པའི་ཡིག་ཆ" +accessSendInvite = "མགྲོན་འབོད་གà½à½¼à½„" +accessTitle = "ཡིག་ཆའི་འཛུལ་ས" +accessYou = "à½à¾±à½ºà½‘" account = "à½à½¼à¼‹à½–དག" +activeSessions = "འགུལ་སྤྱོད་ལས་འà½à½´à½¢" +activeTab = "འགུལ་སྤྱོད" activity = "བྱེད་སྒོ" adminSettings = "འཛིན་སà¾à¾±à½¼à½„་སྒྲིག་སྟངསà¼" +allSessions = "ལས་འà½à½´à½¢à¼‹à½¡à½¼à½„ས" allTools = "ལག་ཆ" automate = "རང་འགུལ་ཅནà¼" +back = "ཕྱིར་ལོག" +certSign = "ལག་འà½à¾±à½ºà½¢à¼‹à½˜à½›à½´à½–་འགན" +completedSessions = "མཇུག་བསྡུས་ལས་འà½à½´à½¢" +completedTab = "མཇུག་བསྡུས" config = "སྒྲིག་བཀོདà¼" +createNew = "རྒྱུ་འདོན་གསར་བ་བཟོ" +createSession = "མཛུབ་འགན་ཞུ་གà½à½¼à½„་བཟོ" +dueDate = "དུས་བཀག (གདམ་à½)" files = "ཡིག་ཆà¼" help = "རོགས་རམ" +noActiveSessions = "མཛུབ་འགན་ཞུ་གà½à½¼à½„་སྒུག་མེད ཡང་ན་འགུལ་སྤྱོད་ལས་འà½à½´à½¢à¼‹à½˜à½ºà½‘" +noCompletedSessions = "མཇུག་བསྡུས་ལས་འà½à½´à½¢à¼‹à½˜à½ºà½‘" +noFile = "ཡིག་ཆ་མ་འདེམས" read = "ཀློག་པ" reader = "ཀློག་མà½à½“à¼" +refresh = "གསར་སྒྱུར" +requestSignatures = "མཛུབ་འགན་ཞུ་གà½à½¼à½„" +selectSingleFileToRequest = "མཛུབ་འགན་ཞུ་གà½à½¼à½„་བར PDF ཡིག་ཆ་གཅིག་འདེམས" +selectedFile = "འདེམས་ཟིན་པའི་ཡིག་ཆ" +selectUsers = "མཛུབ་འགན་བཀལ་མི་སྣ་འདེམས" +selectUsersPlaceholder = "ཞུགས་མà½à½“་འདེམས..." +sendingRequest = "གà½à½¼à½„་བཞིན..." settings = "སྒྲིག་སྟངསà¼" showMeAround = "ང་ལ་སྟོན་དང་à¼" sign = "མིང་འགོད" +signatureRequests = "མཛུབ་འགན་ཞུ་གà½à½¼à½„" +signYourself = "à½à¾±à½ºà½‘་རང་མཛུབ་འགན" +newRequest = "རྒྱུ་འདོན་གསར་པ" tours = "ཡུལ་སà¾à½¼à½¢à¼" +wetSign = "མཛུབ་རྟགས་à½à¼‹à½¦à¾£à½¼à½“" +filterMine = "ངའི" +filterOverdue = "དུས་བཀག་ལས་འདས" +filterSigned = "མཛུབ་འགན་བཀལ" +filterDeclined = "à½à½¦à¼‹à½£à½ºà½“་མ་བྱས" +searchDocuments = "ཡིག་ཆ་འཚོལ…" [quickAccess.helpMenu] adminTour = "འཛིན་སà¾à¾±à½¼à½„་སà¾à½¼à½¢à¼‹à½‚ཡེང་à¼" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "à½à¾±à½ºà½‘་ཀྱི Stirling-PDF ཞབས་ཞ expired = "à½à¾±à½ºà½‘་རང་གི་ཚོགས་à½à½´à½“་དུས་ཚོད་རྫོགས་ཡོད༠ཤོག་བུ་གསར་བཟོ་བྱས་ནས་བསà¾à¾±à½¢à¼‹à½‘ུ་ཚོད་ལྟ་གནང་རོགསà¼" refreshPage = "གསར་འགྱུར་ཤོག་ངོསà¼" +[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 = "མཛུབ་འགན་རིམ་པ" + +[signatureSettings.tooltip] +header = "མཛུབ་འགན་མངོན་སྟོན་སྒྲིག" + +[signatureSettings.tooltip.location] +bullet1 = "དཔེར: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "ཤོག་ངོས་ཀྱི་གནས་ས་དང་མི་འདྲ" +bullet3 = "à½à¾²à½²à½˜à½¦à¼‹à½¦à¾²à½¼à½£à¼‹à½‚ཞན་དག་ལ་དགོས་སྲིད" +description = "མཛུབ་འགན་བཀལ་ས་གནས་ཡུལ་གྱི་གདམ་à½à¼ ལག་འà½à¾±à½ºà½¢ metadata ནང་ཉརà¼" +title = "མཛུབ་འགན་གནས་ཡུལ" + +[signatureSettings.tooltip.logo] +bullet1 = "མཛུབ་འགན་དང་ཡི་གེ་དང་མཉམ་དུ་མངོན" +bullet2 = "PNG, JPG རྣམ་གྲངས་རྒྱུན་འགྱུར" +bullet3 = "ལས་དབང་འཛུགས་ལྗོངས་ལ་བརྗོད་པ་ལེགས་སྒྱུར" +description = "མà½à½¼à½„་རུང་མཛུབ་འགན་ལ་ཚོགས་སྡེའི་ལས་རྟགས་à½à¼‹à½¦à¾£à½¼à½“་ནས བརྡ་སྟོན་དང་བློ་གྲོས་བཟོà¼" +title = "ས་à½à½„་ལས་རྟགས" + +[signatureSettings.tooltip.reason] +bullet1 = "དཔེར: \"Approval\", \"Contract Agreement\", \"Review Complete\"" +bullet2 = "PDF མཛུབ་འགན་ངོ་སྤྲོད་ནང་མངོན" +bullet3 = "བཤེར་དཔྱད་དང་ལྟ་སྤྱོད་ལ་ཕན" +description = "ཡིག་ཆ་ལ་མཛུབ་འགན་བཀལ་རྒྱུའི་རྒྱུ་མཚན༠ལག་འà½à¾±à½ºà½¢ metadata ནང་ཉརà¼" +title = "མཛུབ་འགན་རྒྱུ་མཚན" + +[signatureSettings.tooltip.visibility] +bullet1 = "མà½à½¼à½„་རུང: མཛུབ་འགན་ཡིག་ཆའི་སྟེང་མངོན རྣམ་པ་སྲོལ་གསར" +bullet2 = "མà½à½¼à½„་མི་རུང: ལག་འà½à¾±à½ºà½¢à¼‹à½¡à½²à½‚་ཆ་ནང་à½à¼‹à½˜à½¦à¼‹à½–ཅུག" +bullet3 = "མà½à½¼à½„་མི་རུང་མཛུབ་འགན་ཡང་གསང་ཨང་རྒྱུད་གཞི་à½à½´à½„ས་ཡོད" +description = "མཛུབ་འགན་ཡིག་ཆ་སྟེང་མངོན་དམ་ནང་à½à¼‹à½˜à½¦à¼‹à½–ཅུག་ན་གདམà¼" +title = "མཛུབ་འགན་མà½à½¼à½„་རུང་གནས" + [settings.configuration] advanced = "ཡན་à½à½¼à½“་ཅན" database = "གཞི་གྲངས་རྟེན་གཞིà¼" endpoints = "མཇུག་སྡོམà¼" features = "à½à¾±à½‘་ཆོས" +storageSharing = "ཡིག་ཆའི་གསོག་འཇོག་དང་མཉམ་སྤྱོད" systemSettings = "མ་ལག་སྒྲིག་བཀོདà¼" title = "བཀོད་སྒྲིགà¼" @@ -6332,10 +6868,13 @@ title = "སི་ཊར་ལིང་ལ་འཛུལ་བà¼" [setup.selfhosted] link = "ཡང་ན་རང་གིས་བདག་གཉེར་བྱེད་པའི་རྩིས་à½à¾²à¼‹à½£à¼‹à½˜à½à½´à½‘་པà¼" subtitle = "à½à¾±à½ºà½‘་རང་གི་གསབ་ལེན་ཆས་ཀྱི་ཡིག་ཆ་བླུགསà¼" +changeServerLocked = "à½à¾±à½ºà½‘་ཀྱི་ཚོགས་སྡེས་མ་ལག་འདི་སར་བར་གཅིག་ལ་བཀག་འགོག་བྱས་ཡོདà¼" switchToLocal = "རང་གནས་ལག་ཆ་སྤྱོད" title = "སར་བར་ནང་འཛུལ་བà¼" [setup.selfhosted.unreachable] +changeServer = "སར་བར་གཞན་ལ་མà½à½´à½‘" +changeServerLocked = "à½à¾±à½ºà½‘་ཀྱི་ཚོགས་སྡེས་མ་ལག་འདི་སར་བར་གཅིག་ལ་བཀག་འགོག་བྱས་ཡོདà¼" continueOffline = "རང་གནས་ལག་ཆ་སྤྱོད" message = "{{url}} ལ་མà½à½´à½‘་མི་à½à½´à½–༠ཞབས་ཞུ་འà½à½¼à½¢à¼‹à½–ཀོལ་བཞིན་དང་འà½à½¼à½–་ཚུལ་ཡོད་མིན་ཞིབ་བཤེར་བྱོསà¼" retry = "བསà¾à¾±à½¢à¼‹à½šà½¼à½‘" @@ -6529,6 +7068,15 @@ saved = "སà¾à¾±à½¼à½–་པà¼" text = "ཡིག་གཞི" title = "མཚན་རྟགས་རིགསà¼" +[signRequest] +declined = "མཛུབ་འགན་ཞུ་གà½à½¼à½„་à½à½¦à¼‹à½£à½ºà½“་མ་བྱས" +fetchFailed = "མཛུབ་འགན་ཞུ་གà½à½¼à½„་འཇུག་མ་à½à½´à½–" +signed = "ཡིག་ཆ་མཛུབ་འགན་ལེགས་གྲུབ" + +[signSession] +createFailed = "མཛུབ་འགན་ཞུ་གà½à½¼à½„་བཟོ་མ་à½à½´à½–" +created = "མཛུབ་འགན་ཞུ་གà½à½¼à½„་བà½à½„་ཟིན" + [signup] accountCreatedSuccessfully = "རྩིས་à½à¾²à¼‹à½£à½ºà½‚ས་འགྲུབ་བྱུང་༠à½à¾±à½ºà½‘་རང་ད་ལྟ་ནང་དུ་མིང་རྟགས་བཀོད་ཆོག" alreadyHaveAccount = "ད་ལྟ་རྩིས་à½à¾²à¼‹à½žà½²à½‚་ཡོད་དམ༠མཚན་རྟགས་བཀོད་པà¼" @@ -6807,6 +7355,106 @@ title = "ལེའུ་ཡིས་PDFབགོསà¼" [splitPdfByChapters] tags = "à½à¼‹à½•ྲལ་བ༠ལེའུ་ཅན༠དེབ་མཚོན་ཆ༠གོ་སྒྲིག་བྱེད་པà¼" +[storageShare] +accessed = "འཛུལ་ཟིན" +accessDenied = "à½à¾±à½ºà½‘་ལ་མཉམ་སྤྱོད་ཡིག་ཆ་འདིར་འཛུལ་མི་ཆོག བདག་པོ་ལས་མཉམ་སྤྱོད་བཀལ་དུས་ཞུà¼" +accessFailed = "ལཱ་འགུལ་འཇུག་མ་à½à½´à½–à¼" +accessDeniedBody = "à½à¾±à½ºà½‘་ལ་ཡིག་ཆ་འདི་ལ་འཛུལ་མི་ཆོག བདག་པོ་ལས་མཉམ་སྤྱོད་བཀལ་དུས་ཞུà¼" +accessDeniedTitle = "འཛུལ་ས་མེད" +accessLimitedCommenter = "མཆན་བཀོད་ལས་འགུལ་མ་འབྱུང་བསྒུག་བཞིན༠ཕབ་ལེན་དགོས་ན་བདག་པོས་ཞུན་དག་པའི་དབང་ཆ་བྱ་རོགསà¼" +accessLimitedTitle = "ཚད་འཛིན་འཛུལ་ས" +accessLimitedViewer = "འབྲེལ་à½à½‚་འདི་ལྟ་ཙམ་ཡིན༠ཕབ་ལེན་དགོས་ན་བདག་པོས་ཞུན་དག་པའི་དབང་ཆ་བྱ་རོགསà¼" +createdAt = "གསར་བཟོས" +download = "ཕབ་ལེན" +downloadFailed = "ཡིག་ཆ་འདི་ཕབ་ལེན་མི་à½à½´à½–à¼" +expiredBody = "མཉམ་སྤྱོད་འབྲེལ་à½à½‚་འདི་ནུས་མེད་ཡང་ན་དུས་ཡོལ་བྱས་ཟིནà¼" +expiredTitle = "འབྲེལ་à½à½‚་དུས་ཡོལ" +goToLogin = "à½à½¼à¼‹à½ à½‚ོད་ལ་འགྲོ" +loadFailed = "མཉམ་སྤྱོད་ཡིག་ཆ་à½à¼‹à½•ྱེ་མི་à½à½´à½–à¼" +loading = "མཉམ་སྤྱོད་འབྲེལ་à½à½‚་སྒྲུབ་བཞིན..." +loginPrompt = "མཉམ་སྤྱོད་ཡིག་ཆ་འདིར་འཛུལ་བར་à½à½¼à¼‹à½ à½‚ོད་བྱེད་རོགསà¼" +loginRequired = "à½à½¼à¼‹à½ à½‚ོད་དགོས" +openInApp = "Stirling PDF ནང་à½à¼‹à½•ྱེ" +ownerLabel = "བདག་པོ" +ownerUnknown = "མ་ཤེས" +requiresLogin = "མཉམ་སྤྱོད་ཡིག་ཆ་འདི་ལ་à½à½¼à¼‹à½ à½‚ོད་དགོསà¼" +roleCommenter = "མཆན་འགྲེལ་པ" +roleEditor = "ཞུན་དག་པ" +roleViewer = "ལྟ་མà½à½“" +shareHeading = "མཉམ་སྤྱོད་ཡིག་ཆ" +titleDefault = "མཉམ་སྤྱོད་ཡིག་ཆ" +tryAgain = "དུས་རབས་འགོར་à½à½ºà¼‹à½šà½¼à½‘་ལྟ་རོགསà¼" +addUser = "à½à¼‹à½¦à¾£à½¼à½“" +commenterHint = "མཆན་འགྲེལ་ལས་འགུལ་མ་འབྱུང་བསྒུག་བཞིནà¼" +copied = "འབྲེལ་à½à½‚་འདྲ་བཤུས་བྱས" +copy = "འདྲ་བཤུས" +copyFailed = "འདྲ་བཤུས་ཕམ་པ" +description = "ཡིག་ཆ་འདིའི་དོན་ལུགས་འབྲེལ་à½à½‚་བཟོ༠ནང་འཇུག་བྱས་པའི་མི་ཚོས་འབྲེལ་à½à½‚་བརྒྱུད་ནས་འཛུལ་ཆོག." +downloadsCount = "ཕབ་ལེན: {{count}}" +emailWarningBody = "འདི་གློག་འཕྲིན་གྲངས་འཛིན་ལྟར་མà½à½¼à½„་བ་ཡིན༠མི་འདི་ནི Stirling PDF སྤྱོད་མà½à½“་མ་རེད་ན་ཡིག་ཆ་ལ་འཛུལ་མི་à½à½´à½–à¼" +emailWarningConfirm = "མཉམ་སྤྱོད་གང་རེད་ཀྱང་" +emailWarningTitle = "གློག་འཕྲིན་གྲངས་འཛིན" +errorTitle = "མཉམ་སྤྱོད་ཕམ་པ" +failure = "མཉམ་སྤྱོད་འབྲེལ་à½à½‚་བཟོ་མ་à½à½´à½– ཚོད་ལྟ་རོགསà¼" +fileLabel = "ཡིག་ཆ" +generate = "འབྲེལ་à½à½‚་བཟོ" +generated = "མཉམ་སྤྱོད་འབྲེལ་à½à½‚་བཟོ་ཟིན" +hideActivity = "ལཱ་འགུལ་སྦ་བ" +invalidUsername = "ནུས་ལྡན་མིང་ཡང་ན་གློག་འཕྲིན་འཇུག་རོགསà¼" +lastAccessed = "མà½à½¢à¼‹à½˜à½‡à½´à½‚་འཛུལ་བ" +linkAccessTitle = "མཉམ་སྤྱོད་འབྲེལ་à½à½‚་འཛུལ་ས" +linkLabel = "མཉམ་སྤྱོད་འབྲེལ་à½à½‚" +linksDisabled = "མཉམ་སྤྱོད་འབྲེལ་à½à½‚་བཀག་ཟིནà¼" +linksDisabledBody = "མཉམ་སྤྱོད་འབྲེལ་à½à½‚་སར་བར་སྒྲིག་འགོད་གྲངས་ལས་བཀག་ཟིནà¼" +manage = "མཉམ་སྤྱོད་དོ་དམ" +manageDescription = "ཡིག་ཆ་འདིའི་མཉམ་སྤྱོད་འབྲེལ་à½à½‚་བཟོ་དང་དོ་དམ་བྱེདà¼" +manageLoadFailed = "མཉམ་སྤྱོད་འབྲེལ་à½à½‚་འཇུག་མ་à½à½´à½–à¼" +manageTitle = "མཉམ་སྤྱོད་དོ་དམ" +noActivity = "ལཱ་འགུལ་མེདà¼" +noLinks = "མཉམ་སྤྱོད་འབྲེལ་à½à½‚་མ་འདུག" +noSharedUsers = "འཛུལ་ས་ཡོད་པའི་མི་མེདà¼" +removeLink = "འབྲེལ་à½à½‚་བསུབ" +removeUser = "བསུབ" +revokeFailed = "མཉམ་སྤྱོད་འབྲེལ་à½à½‚་བསུབ་མ་à½à½´à½–" +revoked = "མཉམ་སྤྱོད་སྦྲེལ་མà½à½´à½‘་སུབ་ཟིནà¼" +roleLabel = "ལས་འགན" +sharingDisabled = "མཉམ་སྤྱོད་བཀག་ཡོདà¼" +sharingDisabledBody = "à½à¾±à½ºà½‘་ཀྱི་སར་བར་སྒྲིག་འགོད་ཀྱིས་མཉམ་སྤྱོད་བཀག་བཞག་ཟིནà¼" +sharedUsersTitle = "མཉམ་སྤྱོད་སྤྱོད་པ་ཚོ" +title = "ཡིག་ཆ་མཉམ་སྤྱོདà¼" +unknownUser = "ངོ་མ་ཤེས་པའི་སྤྱོད་པà¼" +userAddFailed = "སྤྱོད་པ་དེ་དང་མཉམ་སྤྱོད་བྱ་མི་à½à½´à½–à¼" +userAdded = "སྤྱོད་པ་དེ་མཉམ་སྤྱོད་à½à½¼à¼‹à½¡à½²à½‚་ནང་སྣོན་ཟིནà¼" +usernameLabel = "སྤྱོད་མà½à½“་མིང་ ཡང་ན་ གློག་འཕྲིནà¼" +usernamePlaceholder = "སྤྱོད་མà½à½“་མིང་ ཡང་ན་ གློག་འཕྲིན་བཙུགསà¼" +userRemoveFailed = "སྤྱོད་པ་དེ་འདོར་མི་à½à½´à½–à¼" +userRemoved = "སྤྱོད་པ་དེ་མཉམ་སྤྱོད་à½à½¼à¼‹à½¡à½²à½‚་ནས་འདོར་ཟིནà¼" +viewActivity = "འགུལ་སྒོ་ལྟ་བà¼" +viewed = "ལྟ་ཟིནà¼" +viewsCount = "ལྟ་བ: {{count}}" +downloaded = "ཕབ་ལེན་བྱས་ཟིནà¼" +bulkDescription = "བདམས་པའི་ཡིག་ཆ་ཚང་མ་à½à½¼à¼‹à½ à½‚ོད་ཟིན་པའི་སྤྱོད་པ་ཚོ་དང་མཉམ་སྤྱོད་བྱེད་པར་སྦྲེལ་མà½à½´à½‘་གཅིག་གསར་བཟོà¼" +bulkTitle = "བདམས་པའི་ཡིག་ཆ་མཉམ་སྤྱོདà¼" +copyLink = "མཉམ་སྤྱོད་སྦྲེལ་མà½à½´à½‘་འདྲ་བཤུསà¼" +fileCount = "{{count}} ཡིག་ཆ་བདམས་ཟིནà¼" +ownerOnly = "མà½à½¼à¼‹à½–དག་à½à½¼à¼‹à½¢à½„་ཙམ་གྱིས་མཉམ་སྤྱོད་དོ་དམ་བྱེད་ཆོག" +selectSingleFile = "མཉམ་སྤྱོད་དོ་དམ་བྱེད་པར་ཡིག་ཆ་གཅིག་བདམསà¼" + +[storageUpload] +description = "ད་ལྟའི་ཡིག་ཆ་འདི་à½à¾±à½ºà½‘་རང་གི་སྤྱོད་དེབ་དོན་ལ་སར་བར་གསོག་འཇོག་ནང་ཡར་བསà¾à½´à½¢à¼‹à½–ྱེདà¼" +errorTitle = "ཡར་བསà¾à½´à½¢à¼‹à½•མ་པà¼" +failure = "ཡར་བསà¾à½´à½¢à¼‹à½•མ་པ༠à½à½¼à¼‹à½ à½‚ོད་དང་གསོག་འཇོག་སྒྲིག་འགོད་ཞིབ་བཤེར་བྱོསà¼" +fileLabel = "ཡིག་ཆ" +hint = "མང་མà½à½ à½²à¼‹à½¦à¾¦à¾²à½ºà½£à¼‹à½˜à½à½´à½‘་དང་འཇུག་ཚུལ་ཚང་མ་à½à¾±à½ºà½‘་ཀྱི་སར་བར་སྒྲིག་འགོད་ཀྱིས་ཚོད་འཛིན་བྱས་ཡོདà¼" +success = "སར་བར་ཡར་བསà¾à½´à½¢à¼‹à½šà½¢à¼‹à½–à¼" +title = "སར་བར་ལ་ཡར་བསà¾à½´à½¢à¼" +updateButton = "སར་བར་ནང་གསར་སྒྱུརà¼" +uploadButton = "སར་བར་ལ་ཡར་བསà¾à½´à½¢à¼" +bulkDescription = "བདམས་པའི་ཡིག་ཆ་ཚང་མ་à½à¾±à½ºà½‘་ཀྱི་སར་བར་གསོག་འཇོག་ནང་ཡར་བསà¾à½´à½¢à¼‹à½–ྱེདà¼" +bulkTitle = "བདམས་པའི་ཡིག་ཆ་ཡར་བསà¾à½´à½¢à¼" +fileCount = "{{count}} ཡིག་ཆ་བདམས་ཟིནà¼" +more = " +{{count}} དེ་ལས་མང་" + [storage] approximateSize = "ཆེ་ཆུང་ཕལ་ཆེར་ཡོདà¼" fileTooLarge = "ཡིག་ཆ་ཆེ་དྲགས་འདུག ཡིག་ཆ་རེ་ལ་ཆེས་མà½à½¼à¼‹à½–འི་ཚད་གཞི་ནིà¼" @@ -7153,6 +7801,30 @@ title = "ལྟ་ཚུལà¼/རྩོམ་སྒྲིག་པ༠PDF" [warning] tooltipTitle = "ཉེན་བརྡ" +[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 = "མིང་འགོད་པར་ཡར་བསà¾à½´à½¢à¼" + [watermark] completed = "ཆུ་རྟགས་à½à¼‹à½¦à¾£à½¼à½“་གྱིས་à½à¼‹à½¦à¾£à½¼à½“་བྱསà¼" desc = "PDF ཡིག་ཆ་ལ་ཡིག་ཆ་ཡང་ན་པར་རིས་ཀྱི་ཆུ་རྟགས་à½à¼‹à½¦à¾£à½¼à½“་བྱེདà¼" @@ -7333,6 +8005,7 @@ activeSession = "ཤུགས་ལྡན་གྱི་ཚོགས་འདུ addMembers = "འà½à½´à½¦à¼‹à½˜à½²à¼‹à½à¼‹à½¦à¾£à½¼à½“à¼" admin = "འཛིན་སà¾à¾±à½¼à½„་à¼" confirmDelete = "à½à¾±à½ºà½‘་རང་གིས་སྤྱོད་མà½à½“་འདི་སུབ་འདོད་ཡོད་དམ༠བྱ་སྤྱོད་འདི་སེལ་མི་à½à½´à½–à¼" +confirmUnlock = "སྤྱོད་པ་འདིའི་རྩིས་à½à¾²à½¼à½‘་ཕྱེ་དགོས་པ་ལ་ངེས་པར་བཟོས་ཡོད་དམ?" deleteUser = "སྤྱོད་མà½à½“་སུབ་པà¼" deleteUserError = "སྤྱོད་མà½à½“་བསུབས་མ་à½à½´à½–་པà¼" deleteUserSuccess = "སྤྱོད་མà½à½“་གྱིས་བསུབ་པ་ལེགས་འགྲུབ་བྱུང་à¼" @@ -7341,6 +8014,8 @@ disable = "ནུས་པ་འཇོམས་པ" disabled = "ཞ་བོ" editRole = "འགན་འཛིནà¼" enable = "སྲིད་པ" +locked = "བཀག་ཡོད" +lockedBadge = "བཀག་ཡོད" loading = "མི་ལ་སà¾à¾±à½ºà½£à¼‹à½ à½‘ྲེན་བྱེད་པà¼" loginRequired = "ནང་འཇུག་à½à½–ས་ལམ་à½à½¼à½‚་མར་ལྕོགས་ཅན་བཟོ་བà¼" member = "འà½à½´à½¦à¼‹à½˜à½²à¼" @@ -7350,6 +8025,9 @@ searchMembers = "འཚོལ་ཞིབ་ཚོགས་མིà¼..." status = "གོ་གནས" team = "རུ་à½à½‚" title = "མི་དམངས" +unlockAccount = "རྩིས་à½à¾²à½¼à½‘་ཕྱེ" +unlockUserError = "སྤྱོད་པའི་རྩིས་à½à¾²à½¼à½‘་ཕྱེ་མི་à½à½´à½–à¼" +unlockUserSuccess = "སྤྱོད་པའི་རྩིས་à½à¾²à½¼à½‘་ཕྱེ་ཚར་བà¼" user = "བེད་སྤྱོདà¼" [workspace.people.actions] diff --git a/frontend/public/locales/ca-CA/translation.toml b/frontend/public/locales/ca-CA/translation.toml index eede99b385..8e3ea9f5f8 100644 --- a/frontend/public/locales/ca-CA/translation.toml +++ b/frontend/public/locales/ca-CA/translation.toml @@ -8,6 +8,7 @@ black = "Negre" blue = "Blau" bored = "Avorrit esperant?" cancel = "Cancel·la" +confirm = "Confirmar" changedCredsMessage = "Credencials canviades!" chooseFile = "Tria fitxer" close = "Tanca" @@ -146,6 +147,7 @@ insufficientCredits = "Crèdits insuficients. Necessaris: {{requiredCredits}}, D loadingCredits = "Comprovant els crèdits..." loadingProStatus = "Comprovant l’estat de la subscripció..." noticeTopUpOrPlan = "No tens prou crèdits; recarrega o passa a un pla" +accessInvite = "Convida" [account] accountSettings = "Opcions del compte" @@ -1427,6 +1429,34 @@ title = "Processament" description = "Temps màxim d'espera d'una tasca de processament abans d'informar d'un error." label = "Temps d'espera del processament (segons)" +[admin.settings.storage] +description = "Controla les opcions d'emmagatzematge del servidor i de compartició." +title = "Emmagatzematge i compartició de fitxers" + +[admin.settings.storage.enabled] +description = "Permet als usuaris desar fitxers al servidor." +label = "Habilita l'emmagatzematge de fitxers al servidor" + +[admin.settings.storage.sharing.email] +description = "Permet compartir amb adreces de correu electrònic." +label = "Habilita la compartició per correu electrònic" +mailLink = "Configura els paràmetres de correu" +mailNote = "Requereix una configuració de correu. " + +[admin.settings.storage.sharing.enabled] +description = "Permet als usuaris compartir fitxers desats." +label = "Habilita la compartició" + +[admin.settings.storage.sharing.links] +description = "Permet compartir mitjançant enllaços per a usuaris amb sessió iniciada." +frontendUrlLink = "Configura-ho als paràmetres del sistema" +frontendUrlNote = "Requereix una Frontend URL. " +label = "Habilita els enllaços de compartició" + +[admin.settings.storage.signing.enabled] +description = "Permet als usuaris crear sessions de signatura de documents amb múltiples participants. Requereix tenir habilitat l'emmagatzematge de fitxers al servidor." +label = "Habilita la signatura en grup (Alfa)" + [admin.settings.unsavedChanges] cancel = "Continua editant" discard = "Descarta els canvis" @@ -2059,7 +2089,19 @@ numbers = "Números/intervals: 5, 10-20" progressions = "Progressions: 3n, 4n+1" [certSign] +allSigned = "Tots els participants han signat. A punt per finalitzar." +awaitingSignatures = "En espera de signatures" +signatureProgress = "{{signedCount}}/{{totalCount}} signatures" chooseCertificate = "Trieu el fitxer de certificat" +declined = "Rebutjada" +fetchFailed = "No s'han pogut carregar les dades de signatura" +finalized = "Finalitzat" +notified = "Pendent" +partialNote = "Pots finalitzar abans amb les signatures actuals. Els participants sense signar s'exclouran." +pending = "Pendent" +readyToFinalize = "A punt per finalitzar" +signed = "Signat" +viewed = "Vist" chooseJksFile = "Trieu el fitxer JKS" chooseP12File = "Trieu el fitxer PKCS12" choosePfxFile = "Trieu el fitxer PFX" @@ -2082,6 +2124,7 @@ title = "Signatura amb Certificat" invisible = "Invisible" stepTitle = "Aparença de la signatura" visible = "Visible" +visibility = "Visibilitat" [certSign.appearance.options] title = "Detalls de la signatura" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Finalitza i carrega el PDF signat" +early = "Finalitza amb les signatures actuals" + +[certSign.collab.sessionDetail] +addButton = "Afegeix participants" +addParticipants = "Afegeix participants" +addParticipantsError = "No s'han pogut afegir participants" +backToList = "Torna a les sessions" +deleteConfirm = "N'estàs segur? Això no es pot desfer." +deleteError = "No s'ha pogut eliminar la sessió" +deleted = "Sessió eliminada" +deleteSession = "Elimina la sessió" +dueDate = "Data límit" +finalizeError = "No s'ha pogut finalitzar la sessió" +loadPdfError = "No s'ha pogut carregar el PDF signat" +loadSignedPdf = "Carrega el PDF signat als fitxers actius" +messageLabel = "Missatge" +noAdditionalInfo = "Sense informació addicional" +owner = "Propietari" +participantRemoved = "Participant eliminat" +participants = "Participants" +participantsAdded = "Participants afegits correctament" +removeParticipant = "Elimina" +removeParticipantError = "No s'ha pogut eliminar el participant" +selectUsers = "Selecciona usuaris..." +sessionInfo = "Informació de la sessió" +workbenchTitle = "Gestió de la sessió" + +[certSign.collab.signRequest] +addedToFiles = "Document afegit als fitxers actius" +addSignature = "Afegeix la teva signatura" +addToFiles = "Afegeix als fitxers actius" +advancedSettings = "Paràmetres avançats" +backToList = "Torna a les sol·licituds de signatura" +certificateChoice = "Selecciona un certificat per signar" +changeSignature = "Canvia la signatura" +clearSignature = "Neteja la signatura" +completeAndSign = "Completa i signa" +createNewSignature = "Crea una signatura nova" +declineButton = "Rebutja" +decline = "Rebutja la sol·licitud" +deleteSelected = "Elimina la signatura seleccionada" +drawSignature = "Dibuixa la teva signatura a continuació" +dueDate = "Data límit" +fileTooLarge = "La mida del fitxer ha de ser inferior a 5 MB" +fontFamily = "Família de lletra" +fontSize = "Mida de lletra: {{size}}px" +fontSizePlaceholder = "Mida" +from = "De" +invalidCertFile = "Selecciona un fitxer de certificat P12 o PFX" +invalidFileType = "Selecciona un fitxer d'imatge" +location = "Ubicació (opcional)" +locationPlaceholder = "Des d'on signes?" +message = "Missatge" +noCertificate = "Selecciona un fitxer de certificat" +noSignatures = "Col·loca almenys una signatura al PDF" +p12File = "Fitxer de certificat P12/PFX" +password = "Contrasenya del certificat" +passwordPlaceholder = "Introdueix la contrasenya..." +penColor = "Color del llapis" +penSize = "Gruix del traç: {{size}}px" +placementActive = "Fes clic al PDF per col·locar" +placeSignatureButton = "Col·loca la signatura al PDF" +reason = "Motiu (opcional)" +reasonPlaceholder = "Per què signes?" +removeImage = "Elimina la imatge" +removeCertFile = "Elimina el fitxer" +savedSignatures = "Signatures desades" +selectFile = "Selecciona un fitxer d'imatge" +selectSignatureTitle = "Selecciona o crea una signatura" +signButton = "Signa el document" +signatureInfo = "Aquests paràmetres els configura el propietari del document" +signaturePlaced = "Signatura col·locada a la pàgina" +signatureSettings = "Paràmetres de la signatura" +signatureText = "Text de la signatura" +signatureTextPlaceholder = "Introdueix el teu nom..." +signatureTypeLabel = "Tipus de signatura" +signingTitle = "Signatura" +textColor = "Color del text" +typeSignature = "Escriu el teu nom per crear una signatura" +uploadCert = "Certificat personalitzat" +uploadCertDesc = "Usa el teu certificat P12/PFX" +uploadSignature = "Puja la imatge de la teva signatura" +usePersonalCert = "Certificat personal" +usePersonalCertDesc = "Generat automàticament per al teu compte" +useServerCert = "Certificat de l'organització" +useServerCertDesc = "Certificat compartit de l'organització" +workbenchTitle = "Sol·licitud de signatura" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Tria el color del traç" +continue = "Continua" + +[certSign.collab.signRequest.certModal] +description = "Has col·locat {{count}} signatura(es). Tria el certificat per completar la signatura." +sign = "Signa el document" +certValidating = "S'està validant el certificat..." +certValidUntil = "Certificat vàlid fins al {{date}}" +certInvalid = "Certificat no vàlid: {{error}}" +certInvalidFallback = "Certificat no vàlid" +certNetworkError = "No s'ha pogut validar el certificat" +title = "Configura el certificat" + +[certSign.collab.signRequest.image] +hint = "Puja una imatge PNG o JPG de la teva signatura" + +[certSign.collab.signRequest.mode] +move = "Mou la signatura" +place = "Col·loca la signatura" +title = "Mode de signatura o de moviment" + +[certSign.collab.signRequest.modeTabs] +draw = "Dibuixa" +image = "Puja" +text = "Escriu" + +[certSign.collab.signRequest.placeSignature] +message = "Fes clic al PDF per col·locar la teva signatura" +title = "Col·loca la signatura" + +[certSign.collab.signRequest.preview] +imageAlt = "Signatura seleccionada" +missing = "Sense previsualització" +textFallback = "Signatura" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Signatura dibuixada" +defaultImageLabel = "Signatura pujada" +defaultLabel = "Signatura" +defaultTextLabel = "Signatura escrita" +delete = "Elimina la signatura" +none = "No hi ha signatures desades" + +[certSign.collab.signRequest.signatureType] +draw = "Dibuixa" +type = "Escriu" +upload = "Puja" + +[certSign.collab.signRequest.steps] +back = "Enrere" +cancelPlacement = "Cancel·la la col·locació" +certificate = "Certificat" +clickMultipleTimes = "Fes clic al PDF diverses vegades per col·locar signatures. Arrossega qualsevol signatura per moure-la o canviar-ne la mida." +clickToPlace = "Fes clic al PDF on vulguis que aparegui la teva signatura." +continue = "Continua a la selecció de certificat" +continueToPlacement = "Continua a la col·locació" +continueToReview = "Continua a la revisió" +createSignature = "Crea la signatura" +invisible = "Invisible" +location = "Ubicació:" +multipleSignatures = "{{count}} signatures s'aplicaran al PDF" +oneSignature = "S'aplicarà 1 signatura al PDF" +placeOnPdf = "Col·loca al PDF" +reason = "Motiu:" +reviewTitle = "Revisa abans de signar" +signaturePlaced = "Signatura col·locada a la pàgina {{page}}. Pots ajustar-ne la posició fent-hi clic de nou o continuar a la revisió." +visible = "Visible" +visibility = "Visibilitat:" +yourSignatures = "Les teves signatures ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Color" +fontLabel = "Tipus de lletra" +fontSizeLabel = "Mida" +fontSizePlaceholder = "16" +label = "Text de la signatura" +modalHint = "Introdueix el teu nom i fes clic a Continua per col·locar-lo al PDF." +placeholder = "Introdueix el teu nom..." + +[certSign.collab.participant] +certValidating = "S'està validant el certificat..." +certValid = "✓ Certificat vàlid" +certValidUntil = " fins al {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Certificat no vàlid" +certNetworkError = "No s'ha pogut validar el certificat" + +[certSign.collab.addParticipants] +add = "Afegeix {{count}} participants" +back = "Enrere" +configureSignatures = "Configura els paràmetres de la signatura" +continue = "Continua als paràmetres de la signatura" +reasonHelp = "Preconfigura un motiu de signatura per a aquests participants (opcional, el poden canviar en signar)" +reasonPlaceholder = "p. ex. Aprovació, Revisió..." +selectUsers = "Selecciona usuaris" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Inclou la pàgina de resum de signatures" +includeSummaryPageHelp = "S'afegirà una pàgina de resum al final amb totes les metadades de les signatures. Les caixes de signatura del certificat digital a les pàgines individuals se suprimiran (les signatures manuscrites no es veuran afectades)." + +[certSign.collab.sessionList] +active = "Activa" +finalized = "Finalitzada" + +[certSign.collab.signatureSettings] +description = "Configura com apareixeran les signatures per a tots els participants" +title = "Aparença de la signatura" + +[certSign.collab.userSelector] +inviteUsers = "Afegeix usuaris" +loadError = "No s'han pogut carregar els usuaris" +noTeam = "Sense equip" +noUsers = "No s'han trobat altres usuaris." +placeholder = "Selecciona usuaris..." + +[certSign.mobile] +panelActions = "Accions" +panelDocument = "Document" +panelPeople = "Persones" + +[certSign.sessions] +deleted = "Sessió eliminada" +fetchFailed = "No s'han pogut carregar els detalls de la sessió" +finalized = "Sessió finalitzada" +loaded = "PDF signat carregat" +pdfNotReady = "El PDF no està llest" +pdfNotReadyDesc = "S'està generant el PDF signat. Torna-ho a provar d'aquí a un moment." + +[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" + [changeCreds] changePassword = "Estàs utilitzant les credencials d'inici de sessió per defecte. Si us plau, introdueix una nova contrasenya" changeUsername = "Actualitza el nom d'usuari. Es tancarà la sessió després d’actualitzar." @@ -3242,6 +3531,46 @@ totalSelected = "Total seleccionat" unsupported = "No compatible" unzip = "Descomprimeix" uploadError = "No s'han pogut pujar alguns fitxers." +copyCreated = "Còpia desada en aquest dispositiu." +copyFailed = "No s'ha pogut crear una còpia." +leaveShare = "Elimina de la meva llista" +leaveShareFailed = "No s'ha pogut eliminar el fitxer compartit." +leaveShareSuccess = "S'ha eliminat de la teva llista de compartits." +removeBoth = "Elimina de tots dos llocs" +removeFilePrompt = "Aquest fitxer està desat en aquest dispositiu i al teu servidor. Des d'on vols eliminar-lo?" +removeFileTitle = "Elimina el fitxer" +removeLocalOnly = "Només aquest dispositiu" +removeServerFailed = "No s'ha pogut eliminar el fitxer del servidor." +removeServerOnly = "Només del servidor" +removeServerOnlyPrompt = "Aquest fitxer només està desat al teu servidor. Vols eliminar-lo del servidor?" +removeServerSuccess = "S'ha eliminat del servidor." +removeSharedPrompt = "Aquest fitxer està compartit amb tu. Pots eliminar-lo d'aquest dispositiu o de la teva llista de compartits." +removeSharedServerOnlyBlockedPrompt = "Aquest fitxer està compartit amb tu i només està desat al servidor." +removeSharedServerOnlyPrompt = "Aquest fitxer està compartit amb tu i només està desat al servidor. Vols eliminar-lo de la teva llista?" +changesNotUploaded = "Canvis no pujats" +cloudFile = "Fitxer al núvol" +filterAll = "Tots" +filterLocal = "Locals" +filterSharedByMe = "Compartits per mi" +filterSharedWithMe = "Compartits amb mi" +lastSynced = "Última sincronització" +localOnly = "Només local" +makeCopy = "Fes-ne una còpia" +owner = "Propietari" +ownerUnknown = "Desconegut" +share = "Comparteix" +shareSelected = "Comparteix els seleccionats" +sharedByYou = "Compartits per tu" +sharedEditNoticeBody = "No tens drets d'edició de la versió al servidor d'aquest fitxer. Qualsevol edició que facis es desarà com a còpia local." +sharedEditNoticeConfirm = "Entesos" +sharedEditNoticeTitle = "Còpia al servidor només de lectura" +sharedWithYou = "Compartits amb tu" +sharing = "Compartició" +storageState = "Emmagatzematge" +synced = "Sincronitzat" +updateOnServer = "Actualitza al servidor" +uploadSelected = "Puja els seleccionats" +uploadToServer = "Puja al servidor" [files] addFiles = "Afegeix fitxers" @@ -3367,6 +3696,77 @@ title = "Sobre l'aplanament de PDFs" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Sobre la signatura en grup" + +[groupSigning.tooltip.finalization] +bullet1 = "Totes les signatures s'apliquen en l'ordre de participants que hagis especificat" +bullet2 = "Pots finalitzar amb signatures parcials si cal" +bullet3 = "Un cop finalitzada, la sessió no es pot modificar" +description = "Quan tots els participants hagin signat (o decideixis finalitzar abans), pots generar el PDF final signat." +title = "Procés de finalització" + +[groupSigning.tooltip.roles] +bullet1 = "Propietari (tu): crea la sessió, configura els valors per defecte de la signatura, finalitza el document" +bullet2 = "Participants: creen la seva signatura, trien el certificat, la col·loquen al PDF" +bullet3 = "Els participants no poden modificar els paràmetres de visibilitat, motiu o ubicació de la signatura" +description = "Tu controles els paràmetres d'aparença de la signatura per a tots els participants." +title = "Rols dels participants" + +[groupSigning.tooltip.sequential] +bullet1 = "El primer participant ha de signar abans que el segon pugui accedir al document" +bullet2 = "Garanteix l'ordre de signatura adequat per al compliment legal" +bullet3 = "Pots reordenar els participants arrossegant-los a la llista" +description = "Els participants signen els documents en l'ordre que especifiquis. Cada signant rep una notificació quan és el seu torn." +title = "Signatura seqüencial" + +[groupSigning.steps] +back = "Enrere" +completed = "Completat" +current = "Actual" +stepLabel = "Pas {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Continua a la revisió" +invisible = "Les signatures seran invisibles (només metadades)" +locationLabel = "Ubicació:" +preview = "Previsualitza" +reasonLabel = "Motiu:" +title = "Configura els paràmetres de la signatura" +visible = "Les signatures seran visibles a la pàgina {{page}}" + +[groupSigning.steps.review] +document = "Document" +dueDate = "Data límit (opcional)" +dueDatePlaceholder = "Selecciona la data límit..." +invisible = "Invisible (només metadades)" +location = "Ubicació:" +logo = "Logotip:" +logoHidden = "Sense logotip" +logoShown = "Es mostra el logotip de Stirling PDF" +participants = "Participants" +reason = "Motiu:" +send = "Envia les sol·licituds de signatura" +signatureSettings = "Paràmetres de la signatura" +title = "Revisa els detalls de la sessió" +titleShort = "Revisa i envia" +visibility = "Visibilitat:" +visible = "Visible a la pàgina {{page}}" +participantCount = "Signaran {{count}} participants en ordre" + +[groupSigning.steps.selectDocument] +continue = "Continua a la selecció de participants" +noFile = "Selecciona un únic fitxer PDF dels teus fitxers actius per crear una sessió de signatura." +selectedFile = "Document seleccionat" +title = "Selecciona el document" + +[groupSigning.steps.selectParticipants] +continue = "Continua als paràmetres de la signatura" +count = "{{count}} participants seleccionats" +label = "Selecciona participants" +placeholder = "Tria participants per signar..." +title = "Tria participants" + [getPdfInfo] downloadJson = "Descarrega JSON" downloads = "Descàrregues" @@ -4460,7 +4860,10 @@ zoomOut = "Redueix" [viewer] cannotPreviewFile = "No es pot previsualitzar el fitxer" +disableColorFilter = "Desactiva el filtre de color" dualPageView = "Vista de dues pàgines" +enableDarkFilter = "Activa el filtre fosc" +enableSepiaFilter = "Activa el filtre sèpia" firstPage = "Primera pàgina" lastPage = "Última pàgina" nextPage = "Pàgina següent" @@ -4470,6 +4873,22 @@ singlePageView = "Vista d'una sola pàgina" unknownFile = "Fitxer desconegut" zoomIn = "Amplia" zoomOut = "Redueix" +resetZoom = "Restableix el zoom" + +[viewer.nonPdf] +fileTypeBadge = "Fitxer {{type}}" +convertToPdf = "Converteix a PDF" +loading = "Carregant..." +emptyFile = "Fitxer buit" +csvStats = "{{rows}} files · {{columns}} columnes · {{size}}" +sortedBy = "Ordenat per: {{column}}" +columnDefault = "Columna {{index}}" +htmlPreviewWarning = "Previsualització HTML — els recursos externs poden no carregar-se · {{size}}" +htmlPreview = "Previsualització HTML" +invalidJson = "JSON no vàlid — es mostra el contingut en brut" +textStats = "{{lines}} línies · {{size}}" +lineNumbers = "Números de línia" +renderMarkdown = "Renderitza Markdown" [viewer.attachments] title = "Adjunts" @@ -4531,6 +4950,7 @@ toggleAttachments = "Mostra/amaga els adjunts" toggleTheme = "Canvia el tema" language = "Idioma" toggleAnnotations = "Mostra/oculta les anotacions" +toggleLayers = "Commuta les capes" search = "Cerca al PDF" panMode = "Mode de desplaçament" applyRedactionsFirst = "Aplica primer les redaccions" @@ -5407,20 +5827,72 @@ title = "Imprimir Fitxer" 2 = "Introdueix el Nom de la Impresora" [quickAccess] +access = "Accés" +accessAddPerson = "Afegeix una altra persona" +accessBack = "Enrere" +accessCopyLink = "Copia l'enllaç" +accessEmail = "Adreça de correu electrònic" +accessEmailPlaceholder = "nom@empresa.com" +accessFileLabel = "Fitxer" +accessGeneral = "Accés general" +accessInviteTitle = "Convida persones" +accessOwner = "Propietari" +accessPanel = "Accés al document" +accessPeople = "Persones amb accés" +accessRemove = "Elimina" +accessRestricted = "Restringit" +accessRestrictedHint = "Només les persones amb accés el poden obrir" +accessRole = "Rol" +accessRoleCommenter = "Comentarista" +accessRoleEditor = "Editor" +accessRoleViewer = "Lector" +accessSelectedFile = "Fitxer seleccionat" +accessSendInvite = "Envia la invitació" +accessTitle = "Accés al document" +accessYou = "Tu" account = "Compte" +activeSessions = "Sessions actives" +activeTab = "Actives" activity = "Registre" adminSettings = "Ajustos admin" +allSessions = "Totes les sessions" allTools = "All Tools" automate = "Auto" +back = "Enrere" +certSign = "Signatura amb certificat" +completedSessions = "Sessions completades" +completedTab = "Completades" config = "Config" +createNew = "Crea una sol·licitud nova" +createSession = "Crea una sol·licitud de signatura" +dueDate = "Data límit (opcional)" files = "Fitxers" help = "Ajuda" +noActiveSessions = "No hi ha sol·licituds de signatura pendents ni sessions actives" +noCompletedSessions = "No hi ha sessions completades" +noFile = "Cap fitxer seleccionat" read = "Llegeix" reader = "Lector" +refresh = "Actualitza" +requestSignatures = "Sol·licita signatures" +selectSingleFileToRequest = "Selecciona un únic fitxer PDF per sol·licitar signatures" +selectedFile = "Fitxer seleccionat" +selectUsers = "Selecciona usuaris per signar" +selectUsersPlaceholder = "Tria participants..." +sendingRequest = "Enviant..." settings = "Ajustos" showMeAround = "Fes-me una visita guiada" sign = "Signa" +signatureRequests = "Sol·licituds de signatura" +signYourself = "Signa-ho tu" +newRequest = "Nova sol·licitud" tours = "Visites guiades" +wetSign = "Afegeix signatura" +filterMine = "Meves" +filterOverdue = "Vençudes" +filterSigned = "Signades" +filterDeclined = "Rebutjades" +searchDocuments = "Cerca documents…" [quickAccess.helpMenu] adminTour = "Visita per a administradors" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "El teu servidor de Stirling-PDF és fora de línia i expired = "La teva sessió ha expirat. Si us plau, actualitza la pàgina i torna a intentar-ho." refreshPage = "Actualitza la pàgina" +[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" + +[signatureSettings.tooltip] +header = "Paràmetres d'aparença de la signatura" + +[signatureSettings.tooltip.location] +bullet1 = "Exemples: \"Nova York, EUA\", \"Oficina de Londres\", \"En remot\"" +bullet2 = "No és el mateix que la posició a la pàgina" +bullet3 = "Pot ser necessari en algunes jurisdiccions legals" +description = "Ubicació geogràfica opcional on s'ha aplicat la signatura. S'emmagatzema a les metadades del certificat." +title = "Ubicació de la signatura" + +[signatureSettings.tooltip.logo] +bullet1 = "Es mostra junt amb la signatura i el text" +bullet2 = "Admet formats PNG, JPG" +bullet3 = "Millora l'aparença professional" +description = "Afegeix un logotip d'empresa a les signatures visibles per a marca i autenticitat." +title = "Logotip de l'empresa" + +[signatureSettings.tooltip.reason] +bullet1 = "Exemples: \"Aprovació\", \"Acord de contracte\", \"Revisió completada\"" +bullet2 = "Visible a les propietats de la signatura del PDF" +bullet3 = "Útil per a pistes d'auditoria i compliment" +description = "Text opcional que explica per què es signa el document. S'emmagatzema a les metadades del certificat." +title = "Motiu de la signatura" + +[signatureSettings.tooltip.visibility] +bullet1 = "Visible: La signatura apareix al PDF amb aparença personalitzada" +bullet2 = "Invisible: Certificat incrustat sense marca visual" +bullet3 = "Les signatures invisibles encara ofereixen validació criptogràfica" +description = "Controla si la signatura és visible al document o s'incrusta de manera invisible." +title = "Visibilitat de la signatura" + [settings.configuration] advanced = "Avançat" database = "Base de dades" endpoints = "Endpoints" features = "Funcions" +storageSharing = "Emmagatzematge i compartició de fitxers" systemSettings = "Configuració del sistema" title = "Configuració" @@ -6332,10 +6868,13 @@ title = "Inicia sessió a Stirling" [setup.selfhosted] link = "o connecteu-vos a un compte autoallotjat" subtitle = "Introdueix les credencials del servidor" +changeServerLocked = "La teva organització ha restringit aquesta aplicació a un servidor específic" switchToLocal = "Utilitza les eines locals" title = "Inicia sessió al servidor" [setup.selfhosted.unreachable] +changeServer = "Connecta't a un altre servidor" +changeServerLocked = "La teva organització ha restringit aquesta aplicació a un servidor específic" continueOffline = "Utilitza les eines locals" message = "No s’ha pogut accedir a {{url}}. Comprova que el servidor s’està executant i és accessible." retry = "Torna-ho a provar" @@ -6529,6 +7068,15 @@ saved = "Desades" text = "Text" title = "Tipus de signatura" +[signRequest] +declined = "S'ha rebutjat la sol·licitud de signatura" +fetchFailed = "No s'ha pogut carregar la sol·licitud de signatura" +signed = "Document signat correctament" + +[signSession] +createFailed = "No s'ha pogut crear la sol·licitud de signatura" +created = "S'ha enviat la sol·licitud de signatura" + [signup] accountCreatedSuccessfully = "Compte creat correctament! Ara podeu iniciar sessió." alreadyHaveAccount = "Ja teniu compte? Inicieu sessió" @@ -6807,6 +7355,106 @@ title = "Divideix PDF per Capítols" [splitPdfByChapters] tags = "dividir,capítols,marcadors,organitza" +[storageShare] +accessed = "Accedit" +accessDenied = "No tens accés a aquest fitxer compartit. Demana al propietari que el comparteixi amb tu." +accessFailed = "No s'ha pogut carregar l'activitat." +accessDeniedBody = "No tens accés a aquest fitxer. Demana al propietari que el comparteixi amb tu." +accessDeniedTitle = "Sense accés" +accessLimitedCommenter = "L'accés per comentar arribarà aviat. Demana al propietari accés d'editor si necessites descarregar." +accessLimitedTitle = "Accés limitat" +accessLimitedViewer = "Aquest enllaç és només de visualització. Demana al propietari accés d'editor si necessites descarregar." +createdAt = "Creat" +download = "Descarrega" +downloadFailed = "No es pot descarregar aquest fitxer." +expiredBody = "Aquest enllaç de compartició no és vàlid o ha caducat." +expiredTitle = "Enllaç caducat" +goToLogin = "Ves a l'inici de sessió" +loadFailed = "No es pot obrir el fitxer compartit." +loading = "Carregant l'enllaç de compartició..." +loginPrompt = "Inicia sessió per accedir a aquest fitxer compartit." +loginRequired = "Cal iniciar sessió" +openInApp = "Obre a Stirling PDF" +ownerLabel = "Propietari" +ownerUnknown = "Desconegut" +requiresLogin = "Aquest fitxer compartit requereix iniciar sessió." +roleCommenter = "Comentarista" +roleEditor = "Editor" +roleViewer = "Lector" +shareHeading = "Fitxer compartit" +titleDefault = "Fitxer compartit" +tryAgain = "Torna-ho a provar més tard." +addUser = "Afegeix" +commenterHint = "La funció de comentar arribarà aviat." +copied = "Enllaç copiat al porta-retalls" +copy = "Copia" +copyFailed = "No s'ha pogut copiar" +description = "Crea un enllaç de compartició per a aquest fitxer. Els usuaris amb sessió iniciada i l'enllaç hi podran accedir." +downloadsCount = "Descàrregues: {{count}}" +emailWarningBody = "Això sembla una adreça de correu electrònic. Si aquesta persona no és usuària de Stirling PDF, no podrà accedir al fitxer." +emailWarningConfirm = "Comparteix igualment" +emailWarningTitle = "Adreça de correu electrònic" +errorTitle = "No s'ha pogut compartir" +failure = "No s'ha pogut generar un enllaç de compartició. Torna-ho a provar." +fileLabel = "Fitxer" +generate = "Genera l'enllaç" +generated = "S'ha generat l'enllaç de compartició" +hideActivity = "Amaga l'activitat" +invalidUsername = "Introdueix un nom d'usuari o una adreça de correu electrònic vàlids." +lastAccessed = "Últim accés" +linkAccessTitle = "Accés per enllaç" +linkLabel = "Enllaç de compartició" +linksDisabled = "Els enllaços de compartició estan desactivats." +linksDisabledBody = "Els enllaços de compartició estan desactivats per la configuració del teu servidor." +manage = "Gestiona la compartició" +manageDescription = "Crea i gestiona enllaços per compartir aquest fitxer." +manageLoadFailed = "No s'han pogut carregar els enllaços de compartició." +manageTitle = "Gestiona la compartició" +noActivity = "Encara no hi ha activitat." +noLinks = "Encara no hi ha enllaços de compartició actius." +noSharedUsers = "Encara no hi ha usuaris amb accés." +removeLink = "Elimina l'enllaç" +removeUser = "Elimina" +revokeFailed = "No s'ha pogut eliminar l'enllaç de compartició." +revoked = "S'ha eliminat l'enllaç de compartició" +roleLabel = "Rol" +sharingDisabled = "La compartició està desactivada." +sharingDisabledBody = "La compartició s'ha desactivat per la configuració del servidor." +sharedUsersTitle = "Usuaris amb accés compartit" +title = "Compartir fitxer" +unknownUser = "Usuari desconegut" +userAddFailed = "No s'ha pogut compartir amb aquest usuari." +userAdded = "Usuari afegit a la llista de compartició." +usernameLabel = "Nom d'usuari o correu electrònic" +usernamePlaceholder = "Introduïu un nom d'usuari o un correu electrònic" +userRemoveFailed = "No s'ha pogut eliminar aquest usuari." +userRemoved = "Usuari eliminat de la llista de compartició." +viewActivity = "Veure l'activitat" +viewed = "Vist" +viewsCount = "Visualitzacions: {{count}}" +downloaded = "Baixat" +bulkDescription = "Creeu un enllaç per compartir tots els fitxers seleccionats amb usuaris que han iniciat la sessió." +bulkTitle = "Compartir fitxers seleccionats" +copyLink = "Copia l'enllaç per compartir" +fileCount = "{{count}} fitxers seleccionats" +ownerOnly = "Només el propietari pot gestionar la compartició." +selectSingleFile = "Seleccioneu un sol fitxer per gestionar la compartició." + +[storageUpload] +description = "Això puja el fitxer actual a l'emmagatzematge del servidor perquè hi pugueu accedir." +errorTitle = "La pujada ha fallat" +failure = "La pujada ha fallat. Comproveu l'inici de sessió i la configuració d'emmagatzematge." +fileLabel = "Fitxer" +hint = "Els enllaços públics i els modes d'accés es controlen per la configuració del servidor." +success = "S'ha pujat al servidor" +title = "Pujar al servidor" +updateButton = "Actualitza al servidor" +uploadButton = "Puja al servidor" +bulkDescription = "Això puja els fitxers seleccionats a l'emmagatzematge del servidor." +bulkTitle = "Pujar fitxers seleccionats" +fileCount = "{{count}} fitxers seleccionats" +more = " +{{count}} més" + [storage] approximateSize = "Mida aproximada" fileTooLarge = "Fitxer massa gran. La mida màxima per fitxer és" @@ -7153,6 +7801,30 @@ title = "Visualitza/edita PDF" [warning] tooltipTitle = "Avís" +[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" + [watermark] completed = "Marca d'aigua afegida" desc = "Afegeix marques d'aigua de text o d'imatge als fitxers PDF" @@ -7333,6 +8005,7 @@ activeSession = "Sessió activa" addMembers = "Afegeix membres" admin = "Administrador" confirmDelete = "Esteu segur que voleu suprimir aquest usuari? Aquesta acció no es pot desfer." +confirmUnlock = "Segur que voleu desbloquejar aquest compte d'usuari?" deleteUser = "Suprimeix l’usuari" deleteUserError = "No s’ha pogut suprimir l’usuari" deleteUserSuccess = "Usuari suprimit correctament" @@ -7341,6 +8014,8 @@ disable = "Deshabilita" disabled = "Desactivat" editRole = "Edita el rol" enable = "Habilita" +locked = "bloquejat" +lockedBadge = "Bloquejat" loading = "Carregant persones..." loginRequired = "Habilita primer el mode d'inici de sessió" member = "Membre" @@ -7350,6 +8025,9 @@ searchMembers = "Cerca membres..." status = "Estat" team = "Equip" title = "Persones" +unlockAccount = "Desbloqueja el compte" +unlockUserError = "No s'ha pogut desbloquejar el compte d'usuari" +unlockUserSuccess = "El compte d'usuari s'ha desbloquejat correctament" user = "Usuari" [workspace.people.actions] diff --git a/frontend/public/locales/cs-CZ/translation.toml b/frontend/public/locales/cs-CZ/translation.toml index 1b6046308d..5317d91191 100644 --- a/frontend/public/locales/cs-CZ/translation.toml +++ b/frontend/public/locales/cs-CZ/translation.toml @@ -8,6 +8,7 @@ black = "ÄŒerná" blue = "Modrá" bored = "Nudíte se pÅ™i Äekání?" cancel = "ZruÅ¡it" +confirm = "Potvrdit" changedCredsMessage = "PÅ™ihlaÅ¡ovací údaje byly zmÄ›nÄ›ny!" chooseFile = "Vybrat soubor" close = "Zavřít" @@ -146,6 +147,7 @@ insufficientCredits = "Nedostatek kreditů. Požadováno: {{requiredCredits}}, D loadingCredits = "Kontrola kreditů..." loadingProStatus = "Kontrola stavu pÅ™edplatného..." noticeTopUpOrPlan = "Není dostatek kreditů, dobijte je nebo pÅ™ejdÄ›te na tarif" +accessInvite = "Pozvat" [account] accountSettings = "Nastavení úÄtu" @@ -1427,6 +1429,34 @@ title = "Zpracování" description = "Maximální doba Äekání na úlohu zpracování pÅ™ed ohlášením chyby." label = "ÄŒasový limit zpracování (sekundy)" +[admin.settings.storage] +description = "Spravujte úložiÅ¡tÄ› serveru a možnosti sdílení." +title = "Ukládání souborů a sdílení" + +[admin.settings.storage.enabled] +description = "Umožnit uživatelům ukládat soubory na serveru." +label = "Povolit ukládání souborů na serveru" + +[admin.settings.storage.sharing.email] +description = "Umožnit sdílení s e-mailovými adresami." +label = "Povolit sdílení e-mailem" +mailLink = "Nastavit poÅ¡tu" +mailNote = "Vyžaduje nastavení poÅ¡ty. " + +[admin.settings.storage.sharing.enabled] +description = "Umožnit uživatelům sdílet uložené soubory." +label = "Povolit sdílení" + +[admin.settings.storage.sharing.links] +description = "Umožnit sdílení prostÅ™ednictvím odkazů pro pÅ™ihlášené uživatele." +frontendUrlLink = "Nastavit v nastavení systému" +frontendUrlNote = "Vyžaduje Frontend URL. " +label = "Povolit odkazy ke sdílení" + +[admin.settings.storage.signing.enabled] +description = "Umožnit uživatelům vytvářet relace podepisování s více úÄastníky. Vyžaduje povolené ukládání souborů na serveru." +label = "Povolit skupinové podepisování (Alpha)" + [admin.settings.unsavedChanges] cancel = "PokraÄovat v úpravách" discard = "Zahodit zmÄ›ny" @@ -2059,7 +2089,19 @@ numbers = "Čísla/rozsahy: 5, 10-20" progressions = "Progrese: 3n, 4n+1" [certSign] +allSigned = "VÅ¡ichni úÄastníci podepsali. PÅ™ipraveno k dokonÄení." +awaitingSignatures = "ÄŒeká na podpisy" +signatureProgress = "{{signedCount}}/{{totalCount}} podpisů" chooseCertificate = "Vyberte soubor s certifikátem" +declined = "Odmítnuto" +fetchFailed = "NepodaÅ™ilo se naÄíst data o podepisování" +finalized = "DokonÄeno" +notified = "Oznámeno" +partialNote = "Můžete dokonÄit dříve s aktuálními podpisy. Nepodepsaní úÄastníci budou vylouÄeni." +pending = "ÄŒeká" +readyToFinalize = "PÅ™ipraveno k dokonÄení" +signed = "Podepsáno" +viewed = "Zobrazeno" chooseJksFile = "Vyberte soubor JKS" chooseP12File = "Vyberte soubor PKCS12" choosePfxFile = "Vyberte soubor PFX" @@ -2082,6 +2124,7 @@ title = "Podepisování certifikátem" invisible = "Neviditelné" stepTitle = "Vzhled podpisu" visible = "Viditelné" +visibility = "Viditelnost" [certSign.appearance.options] title = "Podrobnosti podpisu" @@ -2188,6 +2231,252 @@ 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ů" +[certSign.collab.finalize] +button = "DokonÄit a naÄíst podepsané PDF" +early = "DokonÄit s aktuálními podpisy" + +[certSign.collab.sessionDetail] +addButton = "PÅ™idat úÄastníky" +addParticipants = "PÅ™idat úÄastníky" +addParticipantsError = "NepodaÅ™ilo se pÅ™idat úÄastníky" +backToList = "ZpÄ›t na relace" +deleteConfirm = "Opravdu? Tuto akci nelze vrátit." +deleteError = "Relaci se nepodaÅ™ilo smazat" +deleted = "Relace smazána" +deleteSession = "Smazat relaci" +dueDate = "Termín" +finalizeError = "NepodaÅ™ilo se dokonÄit relaci" +loadPdfError = "NepodaÅ™ilo se naÄíst podepsané PDF" +loadSignedPdf = "NaÄíst podepsané PDF mezi aktivní soubory" +messageLabel = "Zpráva" +noAdditionalInfo = "Žádné další informace" +owner = "Vlastník" +participantRemoved = "ÚÄastník odstranÄ›n" +participants = "ÚÄastníci" +participantsAdded = "ÚÄastníci byli úspěšnÄ› pÅ™idáni" +removeParticipant = "Odebrat" +removeParticipantError = "NepodaÅ™ilo se odebrat úÄastníka" +selectUsers = "Vyberte uživatele..." +sessionInfo = "Informace o relaci" +workbenchTitle = "Správa relací" + +[certSign.collab.signRequest] +addedToFiles = "Dokument pÅ™idán mezi aktivní soubory" +addSignature = "PÅ™idat svůj podpis" +addToFiles = "PÅ™idat mezi aktivní soubory" +advancedSettings = "PokroÄilá nastavení" +backToList = "ZpÄ›t na požadavky na podpis" +certificateChoice = "Vyberte certifikát pro podepsání" +changeSignature = "ZmÄ›nit podpis" +clearSignature = "Vymazat podpis" +completeAndSign = "DokonÄit a podepsat" +createNewSignature = "VytvoÅ™it nový podpis" +declineButton = "Odmítnout" +decline = "Odmítnout požadavek" +deleteSelected = "Smazat vybraný podpis" +drawSignature = "Nakreslete níže svůj podpis" +dueDate = "Termín" +fileTooLarge = "Velikost souboru musí být menší než 5 MB" +fontFamily = "Písmo" +fontSize = "Velikost písma: {{size}}px" +fontSizePlaceholder = "Velikost" +from = "Od" +invalidCertFile = "Vyberte soubor certifikátu P12 nebo PFX" +invalidFileType = "Vyberte soubor obrázku" +location = "Místo (volitelné)" +locationPlaceholder = "Odkud podepisujete?" +message = "Zpráva" +noCertificate = "Vyberte soubor certifikátu" +noSignatures = "UmístÄ›te na PDF alespoň jeden podpis" +p12File = "Soubor certifikátu P12/PFX" +password = "Heslo certifikátu" +passwordPlaceholder = "Zadejte heslo..." +penColor = "Barva pera" +penSize = "Tloušťka pera: {{size}}px" +placementActive = "KliknÄ›te do PDF pro umístÄ›ní" +placeSignatureButton = "Umístit podpis na PDF" +reason = "Důvod (volitelné)" +reasonPlaceholder = "ProÄ podepisujete?" +removeImage = "Odebrat obrázek" +removeCertFile = "Odebrat soubor" +savedSignatures = "Uložené podpisy" +selectFile = "Vyberte soubor obrázku" +selectSignatureTitle = "Vyberte nebo vytvoÅ™te podpis" +signButton = "Podepsat dokument" +signatureInfo = "Tato nastavení jsou urÄena vlastníkem dokumentu" +signaturePlaced = "Podpis umístÄ›n na stránku" +signatureSettings = "Nastavení podpisu" +signatureText = "Text podpisu" +signatureTextPlaceholder = "Zadejte své jméno..." +signatureTypeLabel = "Typ podpisu" +signingTitle = "Podepisování" +textColor = "Barva textu" +typeSignature = "Zadejte své jméno pro vytvoÅ™ení podpisu" +uploadCert = "Vlastní certifikát" +uploadCertDesc = "Použijte vlastní certifikát P12/PFX" +uploadSignature = "Nahrajte obrázek svého podpisu" +usePersonalCert = "Osobní certifikát" +usePersonalCertDesc = "Automaticky vytvoÅ™en pro váš úÄet" +useServerCert = "Certifikát organizace" +useServerCertDesc = "Sdílený certifikát organizace" +workbenchTitle = "Požadavek na podpis" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Vyberte barvu tahu" +continue = "PokraÄovat" + +[certSign.collab.signRequest.certModal] +description = "Umístili jste {{count}} podpis(ů). Vyberte certifikát pro dokonÄení podepisování." +sign = "Podepsat dokument" +certValidating = "Ověřování certifikátu..." +certValidUntil = "Certifikát platný do {{date}}" +certInvalid = "Certifikát je neplatný: {{error}}" +certInvalidFallback = "Neplatný certifikát" +certNetworkError = "NepodaÅ™ilo se ověřit certifikát" +title = "Nastavení certifikátu" + +[certSign.collab.signRequest.image] +hint = "Nahrajte obrázek podpisu ve formátu PNG nebo JPG" + +[certSign.collab.signRequest.mode] +move = "PÅ™esunout podpis" +place = "Umístit podpis" +title = "Režim podpisu nebo pÅ™esunu" + +[certSign.collab.signRequest.modeTabs] +draw = "Kreslit" +image = "Nahrát" +text = "Psát" + +[certSign.collab.signRequest.placeSignature] +message = "KliknÄ›te na PDF pro umístÄ›ní podpisu" +title = "Umístit podpis" + +[certSign.collab.signRequest.preview] +imageAlt = "Vybraný podpis" +missing = "Žádný náhled" +textFallback = "Podpis" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Nakreslený podpis" +defaultImageLabel = "Nahraný podpis" +defaultLabel = "Podpis" +defaultTextLabel = "Zadaný podpis" +delete = "Smazat podpis" +none = "Žádné uložené podpisy" + +[certSign.collab.signRequest.signatureType] +draw = "Kresba" +type = "Psát" +upload = "Nahrát" + +[certSign.collab.signRequest.steps] +back = "ZpÄ›t" +cancelPlacement = "ZruÅ¡it umístÄ›ní" +certificate = "Certifikát" +clickMultipleTimes = "KliknÄ›te do PDF vícekrát pro umístÄ›ní podpisů. Libovolný podpis pÅ™esuňte nebo změňte jeho velikost pÅ™etažením." +clickToPlace = "KliknÄ›te do PDF tam, kde chcete, aby se podpis zobrazil." +continue = "PokraÄovat k výbÄ›ru certifikátu" +continueToPlacement = "PokraÄovat k umístÄ›ní" +continueToReview = "PokraÄovat ke kontrole" +createSignature = "VytvoÅ™it podpis" +invisible = "Neviditelný" +location = "Místo:" +multipleSignatures = "{{count}} podpisů bude aplikováno na PDF" +oneSignature = "Na PDF bude aplikován 1 podpis" +placeOnPdf = "Umístit na PDF" +reason = "Důvod:" +reviewTitle = "Zkontrolujte pÅ™ed podpisem" +signaturePlaced = "Podpis umístÄ›n na stránku {{page}}. Polohu můžete upravit opÄ›tovným kliknutím nebo pokraÄovat ke kontrole." +visible = "Viditelný" +visibility = "Viditelnost:" +yourSignatures = "VaÅ¡e podpisy ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Barva" +fontLabel = "Písmo" +fontSizeLabel = "Velikost" +fontSizePlaceholder = "16" +label = "Text podpisu" +modalHint = "Zadejte své jméno a poté kliknÄ›te na PokraÄovat pro umístÄ›ní do PDF." +placeholder = "Zadejte své jméno..." + +[certSign.collab.participant] +certValidating = "Ověřování certifikátu..." +certValid = "✓ Certifikát je platný" +certValidUntil = " do {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Neplatný certifikát" +certNetworkError = "NepodaÅ™ilo se ověřit certifikát" + +[certSign.collab.addParticipants] +add = "PÅ™idat {{count}} úÄastníka(ů)" +back = "ZpÄ›t" +configureSignatures = "Konfigurovat nastavení podpisu" +continue = "PokraÄovat k nastavení podpisu" +reasonHelp = "PÅ™ednastavte důvod podpisu pro tyto úÄastníky (volitelné, pÅ™i podpisu jej mohou zmÄ›nit)" +reasonPlaceholder = "napÅ™. Schválení, Kontrola..." +selectUsers = "Vybrat uživatele" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Zahrnout souhrnnou stránku podpisů" +includeSummaryPageHelp = "Na konec bude pÅ™idána souhrnná stránka se vÅ¡emi metadaty podpisů. RámeÄky digitálního certifikátu na jednotlivých stránkách budou potlaÄeny (vlastnoruÄní podpisy nejsou ovlivnÄ›ny)." + +[certSign.collab.sessionList] +active = "Aktivní" +finalized = "DokonÄeno" + +[certSign.collab.signatureSettings] +description = "Nastavte, jak budou podpisy vypadat pro vÅ¡echny úÄastníky" +title = "Vzhled podpisu" + +[certSign.collab.userSelector] +inviteUsers = "PÅ™idat uživatele" +loadError = "NepodaÅ™ilo se naÄíst uživatele" +noTeam = "Žádný tým" +noUsers = "Nebyli nalezeni žádní další uživatelé." +placeholder = "Vyberte uživatele..." + +[certSign.mobile] +panelActions = "Akce" +panelDocument = "Dokument" +panelPeople = "Lidé" + +[certSign.sessions] +deleted = "Relace smazána" +fetchFailed = "NepodaÅ™ilo se naÄíst podrobnosti relace" +finalized = "Relace dokonÄena" +loaded = "Podepsané PDF naÄteno" +pdfNotReady = "PDF není pÅ™ipraveno" +pdfNotReadyDesc = "Podepsané PDF se generuje. Zkuste to prosím za okamžik." + +[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" + [changeCreds] changePassword = "Používáte výchozí pÅ™ihlaÅ¡ovací údaje. Zadejte prosím nové heslo" changeUsername = "Update your username. You will be logged out after updating." @@ -3242,6 +3531,46 @@ totalSelected = "Celkem vybráno" unsupported = "Nepodporováno" unzip = "Rozbalit" uploadError = "NepodaÅ™ilo se nahrát nÄ›které soubory." +copyCreated = "Kopie uložena do tohoto zařízení." +copyFailed = "Kopii se nepodaÅ™ilo vytvoÅ™it." +leaveShare = "Odebrat z mého seznamu" +leaveShareFailed = "NepodaÅ™ilo se odebrat sdílený soubor." +leaveShareSuccess = "Odebráno z vaÅ¡eho seznamu sdílení." +removeBoth = "Odebrat z obou" +removeFilePrompt = "Tento soubor je uložen v tomto zařízení i na vaÅ¡em serveru. Odkud jej chcete odebrat?" +removeFileTitle = "Odebrat soubor" +removeLocalOnly = "Pouze toto zařízení" +removeServerFailed = "Soubor se nepodaÅ™ilo odebrat ze serveru." +removeServerOnly = "Pouze server" +removeServerOnlyPrompt = "Tento soubor je uložen pouze na vaÅ¡em serveru. Chcete jej odebrat ze serveru?" +removeServerSuccess = "Odebrán ze serveru." +removeSharedPrompt = "Tento soubor je s vámi sdílen. Můžete jej odebrat z tohoto zařízení nebo ze svého seznamu sdílených." +removeSharedServerOnlyBlockedPrompt = "Tento soubor je s vámi sdílen a je uložen pouze na serveru." +removeSharedServerOnlyPrompt = "Tento soubor je s vámi sdílen a je uložen pouze na serveru. Odebrat jej z vaÅ¡eho seznamu?" +changesNotUploaded = "ZmÄ›ny nebyly nahrány" +cloudFile = "Cloudový soubor" +filterAll = "VÅ¡e" +filterLocal = "Místní" +filterSharedByMe = "Sdíleno mnou" +filterSharedWithMe = "Sdíleno se mnou" +lastSynced = "Naposledy synchronizováno" +localOnly = "Pouze místní" +makeCopy = "VytvoÅ™it kopii" +owner = "Vlastník" +ownerUnknown = "Neznámý" +share = "Sdílet" +shareSelected = "Sdílet vybrané" +sharedByYou = "Sdíleno vámi" +sharedEditNoticeBody = "Nemáte práva upravovat serverovou verzi tohoto souboru. Jakékoli úpravy se uloží jako místní kopie." +sharedEditNoticeConfirm = "Rozumím" +sharedEditNoticeTitle = "Kopie na serveru je jen pro Ätení" +sharedWithYou = "Sdíleno s vámi" +sharing = "Sdílení" +storageState = "ÚložiÅ¡tÄ›" +synced = "Synchronizováno" +updateOnServer = "Aktualizovat na serveru" +uploadSelected = "Nahrát vybrané" +uploadToServer = "Nahrát na server" [files] addFiles = "PÅ™idat soubory" @@ -3367,6 +3696,77 @@ title = "O zplošťování PDF" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "O skupinovém podepisování" + +[groupSigning.tooltip.finalization] +bullet1 = "VÅ¡echny podpisy se aplikují v poÅ™adí úÄastníků, které jste urÄili" +bullet2 = "V případÄ› potÅ™eby můžete dokonÄit i s neúplnými podpisy" +bullet3 = "Po dokonÄení již relaci nelze upravovat" +description = "Jakmile vÅ¡ichni úÄastníci podepíší (nebo se rozhodnete dokonÄit dříve), můžete vygenerovat finální podepsané PDF." +title = "Proces dokonÄení" + +[groupSigning.tooltip.roles] +bullet1 = "Vlastník (vy): Vytváří relaci, nastavuje výchozí podpisové parametry, dokonÄuje dokument" +bullet2 = "ÚÄastníci: Vytvářejí svůj podpis, volí certifikát, umisÅ¥ují na PDF" +bullet3 = "ÚÄastníci nemohou mÄ›nit nastavení viditelnosti, důvodu ani místa podpisu" +description = "Nastavení vzhledu podpisů ovládáte pro vÅ¡echny úÄastníky." +title = "Role úÄastníků" + +[groupSigning.tooltip.sequential] +bullet1 = "První úÄastník musí podepsat, než se druhý dostane k dokumentu" +bullet2 = "Zajišťuje správné poÅ™adí podpisů pro právní soulad" +bullet3 = "ÚÄastníky můžete pÅ™eÅ™adit pÅ™etažením v seznamu" +description = "ÚÄastníci podepisují dokumenty v poÅ™adí, které urÄíte. Každý podepisující obdrží oznámení, když je na Å™adÄ›." +title = "SekvenÄní podepisování" + +[groupSigning.steps] +back = "ZpÄ›t" +completed = "DokonÄeno" +current = "Aktuální" +stepLabel = "Krok {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "PokraÄovat ke kontrole" +invisible = "Podpisy budou neviditelné (pouze metadata)" +locationLabel = "Místo:" +preview = "Náhled" +reasonLabel = "Důvod:" +title = "Nastavit parametry podpisu" +visible = "Podpisy budou viditelné na stránce {{page}}" + +[groupSigning.steps.review] +document = "Dokument" +dueDate = "Termín (volitelné)" +dueDatePlaceholder = "Vyberte termín..." +invisible = "Neviditelné (pouze metadata)" +location = "Místo:" +logo = "Logo:" +logoHidden = "Bez loga" +logoShown = "Zobrazeno logo Stirling PDF" +participants = "ÚÄastníci" +reason = "Důvod:" +send = "Odeslat požadavky na podpis" +signatureSettings = "Nastavení podpisu" +title = "Zkontrolovat údaje relace" +titleShort = "Zkontrolovat a odeslat" +visibility = "Viditelnost:" +visible = "Viditelné na stránce {{page}}" +participantCount = "{{count}} úÄastník(ů) bude podepisovat v daném poÅ™adí" + +[groupSigning.steps.selectDocument] +continue = "PokraÄovat k výbÄ›ru úÄastníků" +noFile = "Vyberte jeden soubor PDF z vaÅ¡ich aktivních souborů pro vytvoÅ™ení relace podepisování." +selectedFile = "Vybraný dokument" +title = "Vyberte dokument" + +[groupSigning.steps.selectParticipants] +continue = "PokraÄovat k nastavení podpisu" +count = "Vybráno: {{count}} úÄastník(ů)" +label = "Vyberte úÄastníky" +placeholder = "Zvolte úÄastníky k podpisu..." +title = "Zvolte úÄastníky" + [getPdfInfo] downloadJson = "Stáhnout JSON" downloads = "Stažení" @@ -4460,7 +4860,10 @@ zoomOut = "Oddálit" [viewer] cannotPreviewFile = "Nelze zobrazit náhled souboru" +disableColorFilter = "Vypnout barevný filtr" dualPageView = "Zobrazení dvou stránek" +enableDarkFilter = "Zapnout tmavý filtr" +enableSepiaFilter = "Zapnout sépiový filtr" firstPage = "První stránka" lastPage = "Poslední stránka" nextPage = "Další stránka" @@ -4470,6 +4873,22 @@ singlePageView = "Zobrazení jedné stránky" unknownFile = "Neznámý soubor" zoomIn = "PÅ™iblížit" zoomOut = "Oddálit" +resetZoom = "Obnovit pÅ™iblížení" + +[viewer.nonPdf] +fileTypeBadge = "Soubor {{type}}" +convertToPdf = "PÅ™evést do PDF" +loading = "NaÄítání..." +emptyFile = "Prázdný soubor" +csvStats = "{{rows}} řádků · {{columns}} sloupců · {{size}}" +sortedBy = "SeÅ™azeno podle: {{column}}" +columnDefault = "Sloupec {{index}}" +htmlPreviewWarning = "Náhled HTML — externí zdroje se nemusí naÄíst · {{size}}" +htmlPreview = "Náhled HTML" +invalidJson = "Neplatný JSON — zobrazen nezpracovaný obsah" +textStats = "{{lines}} řádků · {{size}}" +lineNumbers = "Čísla řádků" +renderMarkdown = "Vykreslit Markdown" [viewer.attachments] title = "Přílohy" @@ -4531,6 +4950,7 @@ toggleAttachments = "PÅ™epnout zobrazení příloh" toggleTheme = "PÅ™epnout motiv" language = "Jazyk" toggleAnnotations = "PÅ™epnout viditelnost anotací" +toggleLayers = "PÅ™epnout vrstvy" search = "Hledat v PDF" panMode = "Režim posunu" applyRedactionsFirst = "Nejprve použijte zaÄernÄ›ní" @@ -5407,20 +5827,72 @@ title = "Tisk souboru" 2 = "Zadejte název tiskárny" [quickAccess] +access = "Přístup" +accessAddPerson = "PÅ™idat další osobu" +accessBack = "ZpÄ›t" +accessCopyLink = "Kopírovat odkaz" +accessEmail = "E-mailová adresa" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Soubor" +accessGeneral = "Obecný přístup" +accessInviteTitle = "Pozvat osoby" +accessOwner = "Vlastník" +accessPanel = "Přístup k dokumentu" +accessPeople = "Osoby s přístupem" +accessRemove = "Odebrat" +accessRestricted = "Omezeno" +accessRestrictedHint = "Otevřít mohou pouze osoby s přístupem" +accessRole = "Role" +accessRoleCommenter = "Komentující" +accessRoleEditor = "Editor" +accessRoleViewer = "ProhlížeÄ" +accessSelectedFile = "Vybraný soubor" +accessSendInvite = "Odeslat pozvánku" +accessTitle = "Přístup k dokumentu" +accessYou = "Vy" account = "ÚÄet" +activeSessions = "Aktivní relace" +activeTab = "Aktivní" activity = "Aktivita" adminSettings = "Admin nastav." +allSessions = "VÅ¡echny relace" allTools = "All Tools" automate = "Automat." +back = "ZpÄ›t" +certSign = "Podepsat certifikátem" +completedSessions = "DokonÄené relace" +completedTab = "DokonÄené" config = "Konfig." +createNew = "VytvoÅ™it nový požadavek" +createSession = "VytvoÅ™it požadavek na podpis" +dueDate = "Termín (volitelné)" files = "Soubory" help = "NápovÄ›da" +noActiveSessions = "Žádné Äekající požadavky na podpis ani aktivní relace" +noCompletedSessions = "Žádné dokonÄené relace" +noFile = "Není vybrán žádný soubor" read = "Číst" reader = "ÄŒteÄka" +refresh = "Obnovit" +requestSignatures = "Požádat o podpisy" +selectSingleFileToRequest = "Vyberte jeden soubor PDF pro vyžádání podpisů" +selectedFile = "Vybraný soubor" +selectUsers = "Vyberte uživatele k podpisu" +selectUsersPlaceholder = "Zvolte úÄastníky..." +sendingRequest = "Odesílání..." settings = "Nastav." showMeAround = "Show me around" sign = "Podepsat" +signatureRequests = "Požadavky na podpis" +signYourself = "Podepsat se" +newRequest = "Nový požadavek" tours = "Tours" +wetSign = "PÅ™idat podpis" +filterMine = "Moje" +filterOverdue = "Po termínu" +filterSigned = "Podepsáno" +filterDeclined = "Odmítnuto" +searchDocuments = "Hledat dokumenty…" [quickAccess.helpMenu] adminTour = "Prohlídka administrace" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Váš server Stirling-PDF je offline a \"{{endpoint}} expired = "VaÅ¡e relace vyprÅ¡ela. Obnovte prosím stránku a zkuste to znovu." refreshPage = "Obnovit stránku" +[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ů" + +[signatureSettings.tooltip] +header = "Nastavení vzhledu podpisu" + +[signatureSettings.tooltip.location] +bullet1 = "Příklady: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Není totéž co pozice na stránce" +bullet3 = "Může být vyžadováno v nÄ›kterých právních jurisdikcích" +description = "Volitelné geografické místo, kde byl podpis aplikován. Ukládá se do metadat certifikátu." +title = "Místo podpisu" + +[signatureSettings.tooltip.logo] +bullet1 = "Zobrazeno vedle podpisu a textu" +bullet2 = "Podporuje formáty PNG, JPG" +bullet3 = "ZvyÅ¡uje profesionální dojem" +description = "PÅ™idejte k viditelným podpisům logo spoleÄnosti pro branding a důvÄ›ryhodnost." +title = "Logo spoleÄnosti" + +[signatureSettings.tooltip.reason] +bullet1 = "Příklady: \"Schválení\", \"UzavÅ™ení smlouvy\", \"Kontrola dokonÄena\"" +bullet2 = "Viditelné ve vlastnostech podpisu PDF" +bullet3 = "UžiteÄné pro auditní záznamy a dodržování pÅ™edpisů" +description = "Volitelný text vysvÄ›tlující, proÄ je dokument podepisován. Ukládá se do metadat certifikátu." +title = "Důvod podpisu" + +[signatureSettings.tooltip.visibility] +bullet1 = "Viditelný: Podpis se zobrazí v PDF s vlastním vzhledem" +bullet2 = "Neviditelný: Certifikát je vložen bez vizuální znaÄky" +bullet3 = "Neviditelné podpisy stále poskytují kryptografické ověření" +description = "UrÄuje, zda je podpis na dokumentu viditelný, nebo je vložen neviditelnÄ›." +title = "Viditelnost podpisu" + [settings.configuration] advanced = "PokroÄilé" database = "Databáze" endpoints = "Endpoints" features = "Funkce" +storageSharing = "Ukládání souborů a sdílení" systemSettings = "Systémová nastavení" title = "Konfigurace" @@ -6332,10 +6868,13 @@ title = "PÅ™ihlásit se do Stirling" [setup.selfhosted] link = "nebo se pÅ™ipojte k úÄtu s vlastním hostováním" subtitle = "Zadejte pÅ™ihlaÅ¡ovací údaje k vaÅ¡emu serveru" +changeServerLocked = "VaÅ¡e organizace omezila tuto aplikaci na konkrétní server" switchToLocal = "Místo toho použít místní nástroje" title = "PÅ™ihlásit se k serveru" [setup.selfhosted.unreachable] +changeServer = "PÅ™ipojit se k jinému serveru" +changeServerLocked = "VaÅ¡e organizace omezila tuto aplikaci na konkrétní server" continueOffline = "Místo toho použít místní nástroje" message = "Nelze se pÅ™ipojit k {{url}}. Zkontrolujte, že server běží a je dostupný." retry = "Zkusit znovu" @@ -6529,6 +7068,15 @@ saved = "Uložené" text = "Text" title = "Typ podpisu" +[signRequest] +declined = "Požadavek na podpis byl odmítnut" +fetchFailed = "NepodaÅ™ilo se naÄíst požadavek na podpis" +signed = "Dokument byl úspěšnÄ› podepsán" + +[signSession] +createFailed = "NepodaÅ™ilo se vytvoÅ™it požadavek na podpis" +created = "Požadavek na podpis odeslán" + [signup] accountCreatedSuccessfully = "ÚÄet byl úspěšnÄ› vytvoÅ™en! Nyní se můžete pÅ™ihlásit." alreadyHaveAccount = "Už máte úÄet? PÅ™ihlaste se" @@ -6807,6 +7355,106 @@ title = "RozdÄ›lit PDF podle kapitol" [splitPdfByChapters] tags = "rozdÄ›lit,kapitoly,záložky,uspořádat" +[storageShare] +accessed = "Přístup" +accessDenied = "K tomuto sdílenému souboru nemáte přístup. Požádejte vlastníka o sdílení." +accessFailed = "Nelze naÄíst aktivitu." +accessDeniedBody = "K tomuto souboru nemáte přístup. Požádejte vlastníka o sdílení." +accessDeniedTitle = "Bez přístupu" +accessLimitedCommenter = "Přístup pro komentující bude brzy k dispozici. Pokud potÅ™ebujete stahovat, požádejte vlastníka o přístup editora." +accessLimitedTitle = "Omezený přístup" +accessLimitedViewer = "Tento odkaz je pouze pro zobrazení. Pokud potÅ™ebujete stahovat, požádejte vlastníka o přístup editora." +createdAt = "VytvoÅ™eno" +download = "Stáhnout" +downloadFailed = "Tento soubor nelze stáhnout." +expiredBody = "Tento odkaz ke sdílení je neplatný nebo vyprÅ¡el." +expiredTitle = "Odkaz vyprÅ¡el" +goToLogin = "PÅ™ejít na pÅ™ihlášení" +loadFailed = "Nelze otevřít sdílený soubor." +loading = "NaÄítání odkazu ke sdílení..." +loginPrompt = "PÅ™ihlaste se pro přístup k tomuto sdílenému souboru." +loginRequired = "Vyžadováno pÅ™ihlášení" +openInApp = "Otevřít ve Stirling PDF" +ownerLabel = "Vlastník" +ownerUnknown = "Neznámý" +requiresLogin = "Tento sdílený soubor vyžaduje pÅ™ihlášení." +roleCommenter = "Komentující" +roleEditor = "Editor" +roleViewer = "ProhlížeÄ" +shareHeading = "Sdílený soubor" +titleDefault = "Sdílený soubor" +tryAgain = "Zkuste to prosím pozdÄ›ji." +addUser = "PÅ™idat" +commenterHint = "Komentování bude brzy k dispozici." +copied = "Odkaz zkopírován do schránky" +copy = "Kopírovat" +copyFailed = "Kopírování se nezdaÅ™ilo" +description = "VytvoÅ™te odkaz ke sdílení tohoto souboru. PÅ™ihlášení uživatelé s odkazem k nÄ›mu budou mít přístup." +downloadsCount = "Stažení: {{count}}" +emailWarningBody = "Vypadá to jako e-mailová adresa. Pokud tato osoba jeÅ¡tÄ› není uživatelem Stirling PDF, k souboru se nedostane." +emailWarningConfirm = "Sdílet i tak" +emailWarningTitle = "E-mailová adresa" +errorTitle = "Sdílení se nezdaÅ™ilo" +failure = "Nelze vygenerovat odkaz ke sdílení. Zkuste to prosím znovu." +fileLabel = "Soubor" +generate = "Vygenerovat odkaz" +generated = "Odkaz ke sdílení vygenerován" +hideActivity = "Skrýt aktivitu" +invalidUsername = "Zadejte platné uživatelské jméno nebo e-mailovou adresu." +lastAccessed = "Poslední přístup" +linkAccessTitle = "Přístup pÅ™es odkaz" +linkLabel = "Odkaz ke sdílení" +linksDisabled = "Odkazy ke sdílení jsou zakázány." +linksDisabledBody = "Odkazy ke sdílení jsou zakázány v nastavení vaÅ¡eho serveru." +manage = "Spravovat sdílení" +manageDescription = "Vytvářejte a spravujte odkazy pro sdílení tohoto souboru." +manageLoadFailed = "Nelze naÄíst odkazy ke sdílení." +manageTitle = "Správa sdílení" +noActivity = "Zatím žádná aktivita." +noLinks = "Zatím nejsou žádné aktivní odkazy ke sdílení." +noSharedUsers = "Zatím nemá přístup žádný uživatel." +removeLink = "Odebrat odkaz" +removeUser = "Odebrat" +revokeFailed = "Odkaz ke sdílení se nepodaÅ™ilo odstranit." +revoked = "Odkaz pro sdílení odstranÄ›n" +roleLabel = "Role" +sharingDisabled = "Sdílení je zakázáno." +sharingDisabledBody = "Sdílení bylo zakázáno nastavením vaÅ¡eho serveru." +sharedUsersTitle = "Sdíleno s uživateli" +title = "Sdílet soubor" +unknownUser = "Neznámý uživatel" +userAddFailed = "Nelze sdílet s tímto uživatelem." +userAdded = "Uživatel pÅ™idán do seznamu sdílení." +usernameLabel = "Uživatelské jméno nebo e-mail" +usernamePlaceholder = "Zadejte uživatelské jméno nebo e-mail" +userRemoveFailed = "Tohoto uživatele nelze odebrat." +userRemoved = "Uživatel odebrán ze seznamu sdílení." +viewActivity = "Zobrazit aktivitu" +viewed = "Zobrazeno" +viewsCount = "Zobrazení: {{count}}" +downloaded = "Staženo" +bulkDescription = "VytvoÅ™te jeden odkaz pro sdílení vÅ¡ech vybraných souborů s pÅ™ihlášenými uživateli." +bulkTitle = "Sdílet vybrané soubory" +copyLink = "Kopírovat odkaz pro sdílení" +fileCount = "Vybráno {{count}} souborů" +ownerOnly = "Sdílení může spravovat pouze vlastník." +selectSingleFile = "Pro správu sdílení vyberte jeden soubor." + +[storageUpload] +description = "Tímto nahrajete aktuální soubor do úložiÅ¡tÄ› serveru pro svůj přístup." +errorTitle = "Nahrávání selhalo" +failure = "Nahrávání selhalo. Zkontrolujte prosím své pÅ™ihlášení a nastavení úložiÅ¡tÄ›." +fileLabel = "Soubor" +hint = "VeÅ™ejné odkazy a režimy přístupu jsou řízeny nastavením vaÅ¡eho serveru." +success = "Nahráno na server" +title = "Nahrát na server" +updateButton = "Aktualizovat na serveru" +uploadButton = "Nahrát na server" +bulkDescription = "Tímto nahrajete vybrané soubory do úložiÅ¡tÄ› vaÅ¡eho serveru." +bulkTitle = "Nahrát vybrané soubory" +fileCount = "Vybráno {{count}} souborů" +more = " +{{count}} navíc" + [storage] approximateSize = "PÅ™ibližná velikost" fileTooLarge = "Soubor je příliÅ¡ velký. Maximální velikost na soubor je" @@ -7153,6 +7801,30 @@ title = "Zobrazit/Upravit PDF" [warning] tooltipTitle = "UpozornÄ›ní" +[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" + [watermark] completed = "Vodoznak pÅ™idán" desc = "PÅ™idat textové nebo obrazové vodoznaky do souborů PDF" @@ -7333,6 +8005,7 @@ activeSession = "Aktivní relace" addMembers = "PÅ™idat Äleny" admin = "Administrátor" confirmDelete = "Opravdu chcete tohoto uživatele smazat? Tuto akci nelze vrátit." +confirmUnlock = "Opravdu chcete odemknout tento uživatelský úÄet?" deleteUser = "Smazat uživatele" deleteUserError = "Uživatele se nepodaÅ™ilo smazat" deleteUserSuccess = "Uživatel úspěšnÄ› smazán" @@ -7341,6 +8014,8 @@ disable = "Zakázat" disabled = "Deaktivován" editRole = "Upravit roli" enable = "Povolit" +locked = "uzamÄeno" +lockedBadge = "UzamÄeno" loading = "NaÄítání osob..." loginRequired = "Nejprve povolte režim pÅ™ihlášení" member = "ÄŒlen" @@ -7350,6 +8025,9 @@ searchMembers = "Hledat Äleny..." status = "Stav" team = "Tým" title = "Lidé" +unlockAccount = "Odemknout úÄet" +unlockUserError = "NepodaÅ™ilo se odemknout uživatelský úÄet" +unlockUserSuccess = "Uživatelský úÄet byl úspěšnÄ› odemknut" user = "Uživatel" [workspace.people.actions] diff --git a/frontend/public/locales/da-DK/translation.toml b/frontend/public/locales/da-DK/translation.toml index 5ae58fe318..a7ecb538f3 100644 --- a/frontend/public/locales/da-DK/translation.toml +++ b/frontend/public/locales/da-DK/translation.toml @@ -8,6 +8,7 @@ black = "Sort" blue = "BlÃ¥" bored = "Træt af at vente?" cancel = "Annuller" +confirm = "Bekræft" changedCredsMessage = "Legitimationsoplysninger ændret!" chooseFile = "Vælg fil" close = "Luk" @@ -146,6 +147,7 @@ insufficientCredits = "Utilstrækkelige kreditter. Krævet: {{requiredCredits}}, loadingCredits = "Kontrollerer kreditter..." loadingProStatus = "Kontrollerer abonnementsstatus..." noticeTopUpOrPlan = "Ikke nok kreditter. Fyld op eller opgrader til et abonnement" +accessInvite = "Inviter" [account] accountSettings = "Kontoindstillinger" @@ -1427,6 +1429,34 @@ title = "Behandling" description = "Maksimal ventetid pÃ¥ en behandlingsopgave før fejl meldes." label = "Behandlingstimeout (sekunder)" +[admin.settings.storage] +description = "Styr serverlagring og delingsmuligheder." +title = "Fillagring og deling" + +[admin.settings.storage.enabled] +description = "Tillad brugere at gemme filer pÃ¥ serveren." +label = "Aktiver server-fillagring" + +[admin.settings.storage.sharing.email] +description = "Tillad deling med e-mailadresser." +label = "Aktiver e-mail-deling" +mailLink = "Konfigurer mailindstillinger" +mailNote = "Kræver mailkonfiguration. " + +[admin.settings.storage.sharing.enabled] +description = "Tillad brugere at dele gemte filer." +label = "Aktiver deling" + +[admin.settings.storage.sharing.links] +description = "Tillad deling via links for indloggede brugere." +frontendUrlLink = "Konfigurer i systemindstillinger" +frontendUrlNote = "Kræver en Frontend URL. " +label = "Aktiver delingslinks" + +[admin.settings.storage.signing.enabled] +description = "Tillad brugere at oprette underskriftssessioner med flere deltagere. Kræver at server-fillagring er aktiveret." +label = "Aktiver gruppeunderskrift (Alpha)" + [admin.settings.unsavedChanges] cancel = "Fortsæt redigering" discard = "Kassér ændringer" @@ -2059,7 +2089,19 @@ numbers = "Tal/intervaller: 5, 10-20" progressions = "Progressioner: 3n, 4n+1" [certSign] +allSigned = "Alle deltagere har underskrevet. Klar til at færdiggøre." +awaitingSignatures = "Afventer underskrifter" +signatureProgress = "{{signedCount}}/{{totalCount}} underskrifter" chooseCertificate = "Vælg certifikatfil" +declined = "Afvist" +fetchFailed = "Kunne ikke indlæse underskriftsdata" +finalized = "Færdiggjort" +notified = "Afventer" +partialNote = "Du kan færdiggøre tidligt med de nuværende underskrifter. Ikke-underskrevne deltagere vil blive udeladt." +pending = "Afventer" +readyToFinalize = "Klar til at færdiggøre" +signed = "Underskrevet" +viewed = "Set" chooseJksFile = "Vælg JKS-fil" chooseP12File = "Vælg PKCS12-fil" choosePfxFile = "Vælg PFX-fil" @@ -2082,6 +2124,7 @@ title = "Certifikat Underskrivning" invisible = "Usynlig" stepTitle = "Signaturudseende" visible = "Synlig" +visibility = "Synlighed" [certSign.appearance.options] title = "Signaturdetaljer" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Færdiggør og indlæs underskrevet PDF" +early = "Færdiggør med nuværende underskrifter" + +[certSign.collab.sessionDetail] +addButton = "Tilføj deltagere" +addParticipants = "Tilføj deltagere" +addParticipantsError = "Kunne ikke tilføje deltagere" +backToList = "Tilbage til sessioner" +deleteConfirm = "Er du sikker? Dette kan ikke fortrydes." +deleteError = "Kunne ikke slette session" +deleted = "Session slettet" +deleteSession = "Slet session" +dueDate = "Forfaldsdato" +finalizeError = "Kunne ikke færdiggøre session" +loadPdfError = "Kunne ikke indlæse underskrevet PDF" +loadSignedPdf = "Indlæs underskrevet PDF i aktive filer" +messageLabel = "Besked" +noAdditionalInfo = "Ingen yderligere oplysninger" +owner = "Ejer" +participantRemoved = "Deltager fjernet" +participants = "Deltagere" +participantsAdded = "Deltagere tilføjet" +removeParticipant = "Fjern" +removeParticipantError = "Kunne ikke fjerne deltager" +selectUsers = "Vælg brugere..." +sessionInfo = "Sessionsinfo" +workbenchTitle = "Sessionsstyring" + +[certSign.collab.signRequest] +addedToFiles = "Dokument tilføjet til aktive filer" +addSignature = "Tilføj din underskrift" +addToFiles = "Tilføj til aktive filer" +advancedSettings = "Avancerede indstillinger" +backToList = "Tilbage til underskriftsanmodninger" +certificateChoice = "Vælg et certifikat at underskrive med" +changeSignature = "Skift underskrift" +clearSignature = "Ryd underskrift" +completeAndSign = "Fuldfør og underskriv" +createNewSignature = "Opret ny underskrift" +declineButton = "Afvis" +decline = "Afvis anmodning" +deleteSelected = "Slet valgt underskrift" +drawSignature = "Tegn din underskrift nedenfor" +dueDate = "Forfaldsdato" +fileTooLarge = "Filstørrelse skal være mindre end 5 MB" +fontFamily = "Skrifttype" +fontSize = "Skriftstørrelse: {{size}}px" +fontSizePlaceholder = "Størrelse" +from = "Fra" +invalidCertFile = "Vælg en P12- eller PFX-certifikatfil" +invalidFileType = "Vælg en billedfil" +location = "Placering (valgfrit)" +locationPlaceholder = "Hvor underskriver du fra?" +message = "Besked" +noCertificate = "Vælg en certifikatfil" +noSignatures = "Placer mindst én underskrift pÃ¥ PDF'en" +p12File = "P12/PFX-certifikatfil" +password = "Certifikatadgangskode" +passwordPlaceholder = "Indtast adgangskode..." +penColor = "Penfarve" +penSize = "Penstørrelse: {{size}}px" +placementActive = "Klik pÃ¥ PDF for at placere" +placeSignatureButton = "Placer underskrift pÃ¥ PDF" +reason = "Ã…rsag (valgfrit)" +reasonPlaceholder = "Hvorfor underskriver du?" +removeImage = "Fjern billede" +removeCertFile = "Fjern fil" +savedSignatures = "Gemte underskrifter" +selectFile = "Vælg billedfil" +selectSignatureTitle = "Vælg eller opret underskrift" +signButton = "Underskriv dokument" +signatureInfo = "Disse indstillinger konfigureres af dokumentets ejer" +signaturePlaced = "Underskrift placeret pÃ¥ side" +signatureSettings = "Underskriftsindstillinger" +signatureText = "Underskriftstekst" +signatureTextPlaceholder = "Indtast dit navn..." +signatureTypeLabel = "Underskriftstype" +signingTitle = "Underskrivning" +textColor = "Tekstfarve" +typeSignature = "Skriv dit navn for at oprette en underskrift" +uploadCert = "Brugerdefineret certifikat" +uploadCertDesc = "Brug dit eget P12/PFX-certifikat" +uploadSignature = "Upload dit underskriftsbillede" +usePersonalCert = "Personligt certifikat" +usePersonalCertDesc = "Automatisk genereret til din konto" +useServerCert = "Organisationscertifikat" +useServerCertDesc = "Delt organisationscertifikat" +workbenchTitle = "Underskriftsanmodning" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Vælg stregfarve" +continue = "Fortsæt" + +[certSign.collab.signRequest.certModal] +description = "Du har placeret {{count}} underskrift(er). Vælg dit certifikat for at fuldføre underskrivningen." +sign = "Underskriv dokument" +certValidating = "Validerer certifikat..." +certValidUntil = "Certifikat gyldigt indtil {{date}}" +certInvalid = "Ugyldigt certifikat: {{error}}" +certInvalidFallback = "Ugyldigt certifikat" +certNetworkError = "Kunne ikke validere certifikat" +title = "Konfigurer certifikat" + +[certSign.collab.signRequest.image] +hint = "Upload et PNG- eller JPG-billede af din underskrift" + +[certSign.collab.signRequest.mode] +move = "Flyt underskrift" +place = "Placer underskrift" +title = "Underskriv eller flyt-tilstand" + +[certSign.collab.signRequest.modeTabs] +draw = "Tegn" +image = "Upload" +text = "Skriv" + +[certSign.collab.signRequest.placeSignature] +message = "Klik pÃ¥ PDF'en for at placere din underskrift" +title = "Placer underskrift" + +[certSign.collab.signRequest.preview] +imageAlt = "Valgt underskrift" +missing = "Ingen forhÃ¥ndsvisning" +textFallback = "Underskrift" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Tegnet underskrift" +defaultImageLabel = "Uploadet underskrift" +defaultLabel = "Underskrift" +defaultTextLabel = "Indtastet underskrift" +delete = "Slet underskrift" +none = "Ingen gemte underskrifter" + +[certSign.collab.signRequest.signatureType] +draw = "Tegn" +type = "Skriv" +upload = "Upload" + +[certSign.collab.signRequest.steps] +back = "Tilbage" +cancelPlacement = "Annuller placering" +certificate = "Certifikat" +clickMultipleTimes = "Klik flere gange pÃ¥ PDF'en for at placere underskrifter. Træk en hvilken som helst underskrift for at flytte eller ændre størrelse." +clickToPlace = "Klik pÃ¥ PDF'en, hvor du vil have din underskrift til at vises." +continue = "Fortsæt til valg af certifikat" +continueToPlacement = "Fortsæt til placering" +continueToReview = "Fortsæt til gennemgang" +createSignature = "Opret underskrift" +invisible = "Usynlig" +location = "Placering:" +multipleSignatures = "{{count}} underskrifter vil blive anvendt pÃ¥ PDF'en" +oneSignature = "1 underskrift vil blive anvendt pÃ¥ PDF'en" +placeOnPdf = "Placer pÃ¥ PDF" +reason = "Ã…rsag:" +reviewTitle = "Gennemse før underskrivning" +signaturePlaced = "Underskrift placeret pÃ¥ side {{page}}. Du kan justere placeringen ved at klikke igen eller fortsætte til gennemgang." +visible = "Synlig" +visibility = "Synlighed:" +yourSignatures = "Dine underskrifter ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Farve" +fontLabel = "Skrifttype" +fontSizeLabel = "Størrelse" +fontSizePlaceholder = "16" +label = "Underskriftstekst" +modalHint = "Indtast dit navn, og klik derefter pÃ¥ Fortsæt for at placere det pÃ¥ PDF'en." +placeholder = "Indtast dit navn..." + +[certSign.collab.participant] +certValidating = "Validerer certifikat..." +certValid = "✓ Certifikat gyldigt" +certValidUntil = " indtil {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Ugyldigt certifikat" +certNetworkError = "Kunne ikke validere certifikat" + +[certSign.collab.addParticipants] +add = "Tilføj {{count}} deltager(e)" +back = "Tilbage" +configureSignatures = "Konfigurer underskriftsindstillinger" +continue = "Fortsæt til underskriftsindstillinger" +reasonHelp = "Forudindstil en underskriftsÃ¥rsag for disse deltagere (valgfrit, de kan tilsidesætte den ved underskrivning)" +reasonPlaceholder = "f.eks. Godkendelse, Gennemgang..." +selectUsers = "Vælg brugere" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Medtag side med underskriftssammendrag" +includeSummaryPageHelp = "En sammendragside vil blive tilføjet til sidst med al underskriftsmetadata. De digitale certifikat-underskriftsfelter pÃ¥ de enkelte sider undertrykkes (vÃ¥de underskrifter pÃ¥virkes ikke)." + +[certSign.collab.sessionList] +active = "Aktiv" +finalized = "Færdiggjort" + +[certSign.collab.signatureSettings] +description = "Konfigurer, hvordan underskrifter vises for alle deltagere" +title = "Underskriftsudseende" + +[certSign.collab.userSelector] +inviteUsers = "Tilføj brugere" +loadError = "Kunne ikke indlæse brugere" +noTeam = "Intet team" +noUsers = "Ingen andre brugere fundet." +placeholder = "Vælg brugere..." + +[certSign.mobile] +panelActions = "Handlinger" +panelDocument = "Dokument" +panelPeople = "Personer" + +[certSign.sessions] +deleted = "Session slettet" +fetchFailed = "Kunne ikke indlæse sessionsdetaljer" +finalized = "Session færdiggjort" +loaded = "Underskrevet PDF indlæst" +pdfNotReady = "PDF ikke klar" +pdfNotReadyDesc = "Den underskrevne PDF genereres. Prøv igen om et øjeblik." + +[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" + [changeCreds] changePassword = "Du bruger standard loginoplysninger. Indtast venligst en ny adgangskode" changeUsername = "Opdater dit brugernavn. Du bliver logget ud efter opdatering." @@ -3242,6 +3531,46 @@ totalSelected = "Valgt i alt" unsupported = "Ikke understøttet" unzip = "Udpak" uploadError = "Kunne ikke uploade nogle filer." +copyCreated = "Kopi gemt pÃ¥ denne enhed." +copyFailed = "Kunne ikke oprette en kopi." +leaveShare = "Fjern fra min liste" +leaveShareFailed = "Kunne ikke fjerne den delte fil." +leaveShareSuccess = "Fjernet fra din delte liste." +removeBoth = "Fjern begge steder" +removeFilePrompt = "Denne fil er gemt pÃ¥ denne enhed og pÃ¥ din server. Hvor vil du fjerne den fra?" +removeFileTitle = "Fjern fil" +removeLocalOnly = "Kun denne enhed" +removeServerFailed = "Kunne ikke fjerne filen fra serveren." +removeServerOnly = "Kun server" +removeServerOnlyPrompt = "Denne fil er kun lagret pÃ¥ din server. Vil du fjerne den fra serveren?" +removeServerSuccess = "Fjernet fra server." +removeSharedPrompt = "Denne fil er delt med dig. Du kan fjerne den fra denne enhed eller din delte liste." +removeSharedServerOnlyBlockedPrompt = "Denne fil er delt med dig og kun lagret pÃ¥ serveren." +removeSharedServerOnlyPrompt = "Denne fil er delt med dig og kun lagret pÃ¥ serveren. Vil du fjerne den fra din liste?" +changesNotUploaded = "Ændringer ikke uploadet" +cloudFile = "Cloud-fil" +filterAll = "Alle" +filterLocal = "Lokal" +filterSharedByMe = "Delt af mig" +filterSharedWithMe = "Delt med mig" +lastSynced = "Sidst synkroniseret" +localOnly = "Kun lokal" +makeCopy = "Opret en kopi" +owner = "Ejer" +ownerUnknown = "Ukendt" +share = "Del" +shareSelected = "Del valgte" +sharedByYou = "Delt af dig" +sharedEditNoticeBody = "Du har ikke redigeringsrettigheder til serverversionen af denne fil. Eventuelle ændringer, du laver, gemmes som en lokal kopi." +sharedEditNoticeConfirm = "ForstÃ¥et" +sharedEditNoticeTitle = "Skrivebeskyttet serverkopi" +sharedWithYou = "Delt med dig" +sharing = "Deling" +storageState = "Lagring" +synced = "Synkroniseret" +updateOnServer = "Opdater pÃ¥ server" +uploadSelected = "Upload valgte" +uploadToServer = "Upload til server" [files] addFiles = "Tilføj filer" @@ -3367,6 +3696,77 @@ title = "Om udfladning af PDF'er" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Om gruppeunderskrift" + +[groupSigning.tooltip.finalization] +bullet1 = "Alle underskrifter anvendes i den rækkefølge, du har angivet for deltagerne" +bullet2 = "Du kan færdiggøre med delvise underskrifter om nødvendigt" +bullet3 = "NÃ¥r den er færdiggjort, kan sessionen ikke ændres" +description = "NÃ¥r alle deltagere har underskrevet (eller du vælger at færdiggøre tidligt), kan du generere den endelige underskrevne PDF." +title = "Færdiggørelsesproces" + +[groupSigning.tooltip.roles] +bullet1 = "Ejer (dig): Opretter session, konfigurerer standarder for underskrift, færdiggør dokument" +bullet2 = "Deltagere: Opretter deres underskrift, vælger certifikat, placerer pÃ¥ PDF" +bullet3 = "Deltagere kan ikke ændre indstillinger for underskriftens synlighed, Ã¥rsag eller placering" +description = "Du styrer indstillingerne for underskriftsudseende for alle deltagere." +title = "Deltagerroller" + +[groupSigning.tooltip.sequential] +bullet1 = "Første deltager skal underskrive, før den anden kan fÃ¥ adgang til dokumentet" +bullet2 = "Sikrer korrekt underskriftsrækkefølge for juridisk overholdelse" +bullet3 = "Du kan omarrangere deltagere ved at trække dem i listen" +description = "Deltagere underskriver dokumenter i den rækkefølge, du angiver. Hver underskriver fÃ¥r en meddelelse, nÃ¥r det er deres tur." +title = "Sekventiel underskrift" + +[groupSigning.steps] +back = "Tilbage" +completed = "Fuldført" +current = "Aktuel" +stepLabel = "Trin {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Fortsæt til gennemgang" +invisible = "Underskrifter vil være usynlige (kun metadata)" +locationLabel = "Placering:" +preview = "ForhÃ¥ndsvisning" +reasonLabel = "Ã…rsag:" +title = "Konfigurer underskriftsindstillinger" +visible = "Underskrifter vil være synlige pÃ¥ side {{page}}" + +[groupSigning.steps.review] +document = "Dokument" +dueDate = "Forfaldsdato (valgfrit)" +dueDatePlaceholder = "Vælg forfaldsdato..." +invisible = "Usynlig (kun metadata)" +location = "Placering:" +logo = "Logo:" +logoHidden = "Intet logo" +logoShown = "Stirling PDF-logo vist" +participants = "Deltagere" +reason = "Ã…rsag:" +send = "Send underskriftsanmodninger" +signatureSettings = "Underskriftsindstillinger" +title = "Gennemse sessionsdetaljer" +titleShort = "Gennemse og send" +visibility = "Synlighed:" +visible = "Synlig pÃ¥ side {{page}}" +participantCount = "{{count}} deltager(e) vil underskrive i rækkefølge" + +[groupSigning.steps.selectDocument] +continue = "Fortsæt til valg af deltagere" +noFile = "Vælg én PDF-fil fra dine aktive filer for at oprette en underskriftssession." +selectedFile = "Valgt dokument" +title = "Vælg dokument" + +[groupSigning.steps.selectParticipants] +continue = "Fortsæt til underskriftsindstillinger" +count = "{{count}} deltager(e) valgt" +label = "Vælg deltagere" +placeholder = "Vælg deltagere til at underskrive..." +title = "Vælg deltagere" + [getPdfInfo] downloadJson = "Download JSON" downloads = "Downloads" @@ -4460,7 +4860,10 @@ zoomOut = "Zoom ud" [viewer] cannotPreviewFile = "Kan ikke forhÃ¥ndsvise fil" +disableColorFilter = "Deaktiver farvefilter" dualPageView = "To-siders visning" +enableDarkFilter = "Aktiver mørkt filter" +enableSepiaFilter = "Aktiver sepiafilter" firstPage = "Første side" lastPage = "Sidste side" nextPage = "Næste side" @@ -4470,6 +4873,22 @@ singlePageView = "Enkelt-sides visning" unknownFile = "Ukendt fil" zoomIn = "Zoom ind" zoomOut = "Zoom ud" +resetZoom = "Nulstil zoom" + +[viewer.nonPdf] +fileTypeBadge = "{{type}}-fil" +convertToPdf = "Konverter til PDF" +loading = "Indlæser..." +emptyFile = "Tom fil" +csvStats = "{{rows}} rækker · {{columns}} kolonner · {{size}}" +sortedBy = "Sorterede efter: {{column}}" +columnDefault = "Kolonne {{index}}" +htmlPreviewWarning = "HTML-forhÃ¥ndsvisning — eksterne ressourcer indlæses muligvis ikke · {{size}}" +htmlPreview = "HTML-forhÃ¥ndsvisning" +invalidJson = "Ugyldig JSON — viser rÃ¥t indhold" +textStats = "{{lines}} linjer · {{size}}" +lineNumbers = "Linjenumre" +renderMarkdown = "Gengiv Markdown" [viewer.attachments] title = "Vedhæftninger" @@ -4531,6 +4950,7 @@ toggleAttachments = "Vis/skjul vedhæftninger" toggleTheme = "Skift tema" language = "Sprog" toggleAnnotations = "Skift visning af annoteringer" +toggleLayers = "Skift lag" search = "Søg i PDF" panMode = "Pan-tilstand" applyRedactionsFirst = "Anvend maskeringer først" @@ -5407,20 +5827,72 @@ title = "Udskriv Fil" 2 = "Indtast printernavn" [quickAccess] +access = "Adgang" +accessAddPerson = "Tilføj en person mere" +accessBack = "Tilbage" +accessCopyLink = "Kopiér link" +accessEmail = "E-mailadresse" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Fil" +accessGeneral = "Generel adgang" +accessInviteTitle = "Inviter personer" +accessOwner = "Ejer" +accessPanel = "Dokumentadgang" +accessPeople = "Personer med adgang" +accessRemove = "Fjern" +accessRestricted = "Begrænset" +accessRestrictedHint = "Kun personer med adgang kan Ã¥bne" +accessRole = "Rolle" +accessRoleCommenter = "Kommentator" +accessRoleEditor = "Redaktør" +accessRoleViewer = "Læser" +accessSelectedFile = "Valgt fil" +accessSendInvite = "Send invitation" +accessTitle = "Dokumentadgang" +accessYou = "Dig" account = "Konto" +activeSessions = "Aktive sessioner" +activeTab = "Aktiv" activity = "Historik" adminSettings = "Admin Indstil." +allSessions = "Alle sessioner" allTools = "All Tools" automate = "Automat." +back = "Tilbage" +certSign = "Certifikatunderskrift" +completedSessions = "Fuldførte sessioner" +completedTab = "Fuldført" config = "Konfig." +createNew = "Opret ny anmodning" +createSession = "Opret underskriftsanmodning" +dueDate = "Forfaldsdato (valgfrit)" files = "Filer" help = "Hjælp" +noActiveSessions = "Ingen afventende underskriftsanmodninger eller aktive sessioner" +noCompletedSessions = "Ingen fuldførte sessioner" +noFile = "Ingen fil valgt" read = "Læs" reader = "Læser" +refresh = "Opdater" +requestSignatures = "Anmod om underskrifter" +selectSingleFileToRequest = "Vælg én PDF-fil for at anmode om underskrifter" +selectedFile = "Valgt fil" +selectUsers = "Vælg brugere til at underskrive" +selectUsersPlaceholder = "Vælg deltagere..." +sendingRequest = "Sender..." settings = "Indstil." showMeAround = "Vis mig rundt" sign = "Signer" +signatureRequests = "Underskriftsanmodninger" +signYourself = "Underskriv selv" +newRequest = "Ny anmodning" tours = "Rundvisninger" +wetSign = "Tilføj underskrift" +filterMine = "Mine" +filterOverdue = "Forfaldne" +filterSigned = "Underskrevet" +filterDeclined = "Afvist" +searchDocuments = "Søg i dokumenter…" [quickAccess.helpMenu] adminTour = "Admin-rundtur" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Din Stirling-PDF-server er offline, og \"{{endpoint}} expired = "Din sesions tid har udløbet. Genlad siden og prøv igen." refreshPage = "Opdater side" +[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" + +[signatureSettings.tooltip] +header = "Indstillinger for underskriftsudseende" + +[signatureSettings.tooltip.location] +bullet1 = "Eksempler: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Ikke det samme som sideplacering" +bullet3 = "Kan være pÃ¥krævet i visse retsomrÃ¥der" +description = "Valgfri geografisk placering, hvor underskriften blev anvendt. Gemmes i certifikatmetadata." +title = "Underskriftsplacering" + +[signatureSettings.tooltip.logo] +bullet1 = "Vises sammen med underskrift og tekst" +bullet2 = "Understøtter PNG-, JPG-formater" +bullet3 = "Forbedrer det professionelle udseende" +description = "Tilføj et firmalogo til synlige underskrifter for branding og autenticitet." +title = "Firmalogo" + +[signatureSettings.tooltip.reason] +bullet1 = "Eksempler: \"Approval\", \"Contract Agreement\", \"Review Complete\"" +bullet2 = "Synlig i PDF'ens underskriftsegenskaber" +bullet3 = "Nyttigt til revisionsspor og compliance" +description = "Valgfri tekst, der forklarer, hvorfor dokumentet underskrives. Gemmes i certifikatmetadata." +title = "UnderskriftsÃ¥rsag" + +[signatureSettings.tooltip.visibility] +bullet1 = "Synlig: Underskrift vises pÃ¥ PDF med tilpasset udseende" +bullet2 = "Usynlig: Certifikat indlejret uden visuelt mærke" +bullet3 = "Usynlige underskrifter giver stadig kryptografisk validering" +description = "Styrer om underskriften er synlig pÃ¥ dokumentet eller indlejres usynligt." +title = "Underskriftssynlighed" + [settings.configuration] advanced = "Avanceret" database = "Database" endpoints = "Endpoints" features = "Funktioner" +storageSharing = "Fillagring og deling" systemSettings = "Systemindstillinger" title = "Konfiguration" @@ -6332,10 +6868,13 @@ title = "Log ind pÃ¥ Stirling" [setup.selfhosted] link = "eller opret forbindelse til en selvhostet konto" subtitle = "Indtast dine server-loginoplysninger" +changeServerLocked = "Din organisation har begrænset denne app til en specifik server" switchToLocal = "Brug lokale værktøjer i stedet" title = "Log ind pÃ¥ server" [setup.selfhosted.unreachable] +changeServer = "Opret forbindelse til en anden server" +changeServerLocked = "Din organisation har begrænset denne app til en specifik server" continueOffline = "Brug lokale værktøjer i stedet" message = "Kunne ikke nÃ¥ {{url}}. Kontroller, at serveren kører og er tilgængelig." retry = "Prøv igen" @@ -6529,6 +7068,15 @@ saved = "Gemt" text = "Tekst" title = "Underskriftstype" +[signRequest] +declined = "Underskriftsanmodning afvist" +fetchFailed = "Kunne ikke indlæse underskriftsanmodning" +signed = "Dokument underskrevet" + +[signSession] +createFailed = "Kunne ikke oprette underskriftsanmodning" +created = "Underskriftsanmodning sendt" + [signup] accountCreatedSuccessfully = "Konto oprettet! Du kan nu logge ind." alreadyHaveAccount = "Har du allerede en konto? Log ind" @@ -6807,6 +7355,106 @@ title = "Del PDF ved Kapitler" [splitPdfByChapters] tags = "partitionering,kapitler,merker,organisering" +[storageShare] +accessed = "TilgÃ¥et" +accessDenied = "Du har ikke adgang til denne delte fil. Bed ejeren om at dele den med dig." +accessFailed = "Kan ikke indlæse aktivitet." +accessDeniedBody = "Du har ikke adgang til denne fil. Bed ejeren om at dele den med dig." +accessDeniedTitle = "Ingen adgang" +accessLimitedCommenter = "Kommentaradgang kommer snart. Bed ejeren om redaktøradgang, hvis du har brug for at downloade." +accessLimitedTitle = "Begrænset adgang" +accessLimitedViewer = "Dette link er kun til visning. Bed ejeren om redaktøradgang, hvis du har brug for at downloade." +createdAt = "Oprettet" +download = "Download" +downloadFailed = "Kan ikke downloade denne fil." +expiredBody = "Dette delingslink er ugyldigt eller udløbet." +expiredTitle = "Link udløbet" +goToLogin = "GÃ¥ til login" +loadFailed = "Kan ikke Ã¥bne delt fil." +loading = "Indlæser delingslink..." +loginPrompt = "Log ind for at fÃ¥ adgang til denne delte fil." +loginRequired = "Login pÃ¥krævet" +openInApp = "Ã…bn i Stirling PDF" +ownerLabel = "Ejer" +ownerUnknown = "Ukendt" +requiresLogin = "Denne delte fil kræver login." +roleCommenter = "Kommentator" +roleEditor = "Redaktør" +roleViewer = "Læser" +shareHeading = "Delt fil" +titleDefault = "Delt fil" +tryAgain = "Prøv igen senere." +addUser = "Tilføj" +commenterHint = "Kommentarer kommer snart." +copied = "Link kopieret til udklipsholder" +copy = "Kopiér" +copyFailed = "Kopiering mislykkedes" +description = "Opret et delingslink til denne fil. Indloggede brugere med linket kan fÃ¥ adgang til den." +downloadsCount = "Downloads: {{count}}" +emailWarningBody = "Dette ligner en e-mailadresse. Hvis denne person ikke allerede er Stirling PDF-bruger, vil vedkommende ikke kunne fÃ¥ adgang til filen." +emailWarningConfirm = "Del alligevel" +emailWarningTitle = "E-mailadresse" +errorTitle = "Deling mislykkedes" +failure = "Kan ikke generere et delingslink. Prøv igen." +fileLabel = "Fil" +generate = "Generer link" +generated = "Delingslink genereret" +hideActivity = "Skjul aktivitet" +invalidUsername = "Indtast et gyldigt brugernavn eller en gyldig e-mailadresse." +lastAccessed = "Sidst tilgÃ¥et" +linkAccessTitle = "Adgang via delingslink" +linkLabel = "Delingslink" +linksDisabled = "Delingslinks er deaktiveret." +linksDisabledBody = "Delingslinks er deaktiveret af dine serverindstillinger." +manage = "Administrer deling" +manageDescription = "Opret og administrer links for at dele denne fil." +manageLoadFailed = "Kan ikke indlæse delingslinks." +manageTitle = "Administrer deling" +noActivity = "Ingen aktivitet endnu." +noLinks = "Ingen aktive delingslinks endnu." +noSharedUsers = "Ingen brugere har adgang endnu." +removeLink = "Fjern link" +removeUser = "Fjern" +revokeFailed = "Kan ikke fjerne delingslinket." +revoked = "Delingslink fjernet" +roleLabel = "Rolle" +sharingDisabled = "Deling er deaktiveret." +sharingDisabledBody = "Deling er deaktiveret af dine serverindstillinger." +sharedUsersTitle = "Delte brugere" +title = "Del fil" +unknownUser = "Ukendt bruger" +userAddFailed = "Kan ikke dele med den bruger." +userAdded = "Bruger tilføjet til delingslisten." +usernameLabel = "Brugernavn eller e-mail" +usernamePlaceholder = "Indtast et brugernavn eller en e-mail" +userRemoveFailed = "Kan ikke fjerne den bruger." +userRemoved = "Bruger fjernet fra delingslisten." +viewActivity = "Vis aktivitet" +viewed = "Vist" +viewsCount = "Visninger: {{count}}" +downloaded = "Hentet" +bulkDescription = "Opret ét link for at dele alle valgte filer med indloggede brugere." +bulkTitle = "Del valgte filer" +copyLink = "Kopiér delingslink" +fileCount = "{{count}} filer valgt" +ownerOnly = "Kun ejeren kan administrere deling." +selectSingleFile = "Vælg en enkelt fil for at administrere deling." + +[storageUpload] +description = "Dette uploader den aktuelle fil til serverlager, sÃ¥ du selv kan fÃ¥ adgang." +errorTitle = "Upload mislykkedes" +failure = "Upload mislykkedes. Kontroller dine login- og lagerindstillinger." +fileLabel = "Fil" +hint = "Offentlige links og adgangstilstande styres af dine serverindstillinger." +success = "Uploadet til server" +title = "Upload til server" +updateButton = "Opdater pÃ¥ server" +uploadButton = "Upload til server" +bulkDescription = "Dette uploader de valgte filer til dit serverlager." +bulkTitle = "Upload valgte filer" +fileCount = "{{count}} filer valgt" +more = " +{{count}} flere" + [storage] approximateSize = "Omtrent størrelse" fileTooLarge = "Filen er for stor. Maksimal størrelse pr. fil er" @@ -7153,6 +7801,30 @@ title = "Vis/Rediger PDF" [warning] tooltipTitle = "Advarsel" +[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" + [watermark] completed = "Vandmærke tilføjet" desc = "Tilføj tekst- eller billedvandmærker til PDF-filer" @@ -7333,6 +8005,7 @@ activeSession = "Aktiv session" addMembers = "Tilføj medlemmer" admin = "Administrator" confirmDelete = "Er du sikker pÃ¥, at du vil slette denne bruger? Denne handling kan ikke fortrydes." +confirmUnlock = "Er du sikker pÃ¥, at du vil lÃ¥se denne brugerkonto op?" deleteUser = "Slet bruger" deleteUserError = "Kunne ikke slette bruger" deleteUserSuccess = "Bruger slettet" @@ -7341,6 +8014,8 @@ disable = "Deaktivér" disabled = "Deaktiveret" editRole = "Redigér rolle" enable = "Aktivér" +locked = "lÃ¥st" +lockedBadge = "LÃ¥st" loading = "Indlæser personer..." loginRequired = "Aktivér først login-tilstand" member = "Medlem" @@ -7350,6 +8025,9 @@ searchMembers = "Søg efter medlemmer..." status = "Status" team = "Team" title = "Personer" +unlockAccount = "LÃ¥s konto op" +unlockUserError = "Kunne ikke lÃ¥se brugerkonto op" +unlockUserSuccess = "Brugerkonto lÃ¥st op" user = "Bruger" [workspace.people.actions] diff --git a/frontend/public/locales/de-DE/translation.toml b/frontend/public/locales/de-DE/translation.toml index 31a1dec13b..6b37ffb95c 100644 --- a/frontend/public/locales/de-DE/translation.toml +++ b/frontend/public/locales/de-DE/translation.toml @@ -8,6 +8,7 @@ black = "Schwarz" blue = "Blau" bored = "Langeweile beim Warten?" cancel = "Abbrechen" +confirm = "Bestätigen" changedCredsMessage = "Anmeldedaten geändert!" chooseFile = "Datei wählen" close = "Schließen" @@ -146,6 +147,7 @@ insufficientCredits = "Unzureichende Credits. Erforderlich: {{requiredCredits}}, loadingCredits = "Credits werden geprüft..." loadingProStatus = "Abonnementstatus wird geprüft..." noticeTopUpOrPlan = "Nicht genug Credits, bitte aufladen oder auf einen Tarif upgraden" +accessInvite = "Einladen" [account] accountSettings = "Kontoeinstellungen" @@ -1427,6 +1429,34 @@ title = "Processing" description = "Maximale Wartezeit für einen Verarbeitungsauftrag, bevor ein Fehler gemeldet wird." label = "Processing Timeout (seconds)" +[admin.settings.storage] +description = "Server-Speicher und Freigabeoptionen verwalten." +title = "Dateispeicher & Freigabe" + +[admin.settings.storage.enabled] +description = "Ermöglicht Benutzern, Dateien auf dem Server zu speichern." +label = "Server-Dateispeicher aktivieren" + +[admin.settings.storage.sharing.email] +description = "Erlaubt das Teilen mit E-Mail-Adressen." +label = "E-Mail-Freigabe aktivieren" +mailLink = "E-Mail-Einstellungen konfigurieren" +mailNote = "Erfordert eine Mail-Konfiguration. " + +[admin.settings.storage.sharing.enabled] +description = "Ermöglicht Benutzern, gespeicherte Dateien freizugeben." +label = "Freigabe aktivieren" + +[admin.settings.storage.sharing.links] +description = "Erlaubt Freigabe über Links für angemeldete Benutzer." +frontendUrlLink = "In den Systemeinstellungen konfigurieren" +frontendUrlNote = "Erfordert eine Frontend-URL. " +label = "Freigabelinks aktivieren" + +[admin.settings.storage.signing.enabled] +description = "Ermöglicht Benutzern, Signatursitzungen mit mehreren Teilnehmern zu erstellen. Erfordert aktivierten Server-Dateispeicher." +label = "Gruppensignieren aktivieren (Alpha)" + [admin.settings.unsavedChanges] cancel = "Weiter bearbeiten" discard = "Änderungen verwerfen" @@ -2059,7 +2089,19 @@ numbers = "Zahlen/Bereiche: 5, 10-20" progressions = "Progressionen: 3n, 4n+1" [certSign] +allSigned = "Alle Teilnehmer haben signiert. Bereit zum Finalisieren." +awaitingSignatures = "Warten auf Signaturen" +signatureProgress = "{{signedCount}}/{{totalCount}} Signaturen" chooseCertificate = "Zertifikatdatei auswählen" +declined = "Abgelehnt" +fetchFailed = "Signierdaten konnten nicht geladen werden" +finalized = "Finalisiert" +notified = "Ausstehend" +partialNote = "Sie können vorzeitig mit den aktuellen Signaturen finalisieren. Nicht signierte Teilnehmer werden ausgeschlossen." +pending = "Ausstehend" +readyToFinalize = "Bereit zum Finalisieren" +signed = "Signiert" +viewed = "Angesehen" chooseJksFile = "JKS-Datei auswählen" chooseP12File = "PKCS12-Datei auswählen" choosePfxFile = "PFX-Datei auswählen" @@ -2082,6 +2124,7 @@ title = "Zertifikatsignierung" invisible = "Unsichtbar" stepTitle = "Signatur-Erscheinungsbild" visible = "Sichtbar" +visibility = "Sichtbarkeit" [certSign.appearance.options] title = "Signaturdetails" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Finalisieren und signiertes PDF laden" +early = "Mit aktuellen Signaturen finalisieren" + +[certSign.collab.sessionDetail] +addButton = "Teilnehmer hinzufügen" +addParticipants = "Teilnehmer hinzufügen" +addParticipantsError = "Teilnehmer konnten nicht hinzugefügt werden" +backToList = "Zurück zu Sitzungen" +deleteConfirm = "Sind Sie sicher? Dies kann nicht rückgängig gemacht werden." +deleteError = "Sitzung konnte nicht gelöscht werden" +deleted = "Sitzung gelöscht" +deleteSession = "Sitzung löschen" +dueDate = "Fälligkeitsdatum" +finalizeError = "Sitzung konnte nicht finalisiert werden" +loadPdfError = "Signiertes PDF konnte nicht geladen werden" +loadSignedPdf = "Signiertes PDF in aktive Dateien laden" +messageLabel = "Nachricht" +noAdditionalInfo = "Keine weiteren Informationen" +owner = "Eigentümer" +participantRemoved = "Teilnehmer entfernt" +participants = "Teilnehmer" +participantsAdded = "Teilnehmer erfolgreich hinzugefügt" +removeParticipant = "Entfernen" +removeParticipantError = "Teilnehmer konnte nicht entfernt werden" +selectUsers = "Benutzer auswählen..." +sessionInfo = "Sitzungsinfo" +workbenchTitle = "Sitzungsverwaltung" + +[certSign.collab.signRequest] +addedToFiles = "Dokument zu aktiven Dateien hinzugefügt" +addSignature = "Eigene Signatur hinzufügen" +addToFiles = "Zu aktiven Dateien hinzufügen" +advancedSettings = "Erweiterte Einstellungen" +backToList = "Zurück zu Signaturanfragen" +certificateChoice = "Wählen Sie ein Zertifikat zum Signieren aus" +changeSignature = "Signatur ändern" +clearSignature = "Signatur löschen" +completeAndSign = "Abschließen & Signieren" +createNewSignature = "Neue Signatur erstellen" +declineButton = "Ablehnen" +decline = "Anfrage ablehnen" +deleteSelected = "Ausgewählte Signatur löschen" +drawSignature = "Zeichnen Sie unten Ihre Signatur" +dueDate = "Fälligkeitsdatum" +fileTooLarge = "Dateigröße muss unter 5 MB liegen" +fontFamily = "Schriftfamilie" +fontSize = "Schriftgröße: {{size}}px" +fontSizePlaceholder = "Größe" +from = "Von" +invalidCertFile = "Bitte wählen Sie eine P12- oder PFX-Zertifikatsdatei aus" +invalidFileType = "Bitte wählen Sie eine Bilddatei aus" +location = "Ort (optional)" +locationPlaceholder = "Von wo aus signieren Sie?" +message = "Nachricht" +noCertificate = "Bitte wählen Sie eine Zertifikatsdatei aus" +noSignatures = "Bitte platzieren Sie mindestens eine Signatur auf dem PDF" +p12File = "P12/PFX-Zertifikatsdatei" +password = "Zertifikatspasswort" +passwordPlaceholder = "Passwort eingeben..." +penColor = "Stiftfarbe" +penSize = "Stiftstärke: {{size}}px" +placementActive = "Klicken Sie ins PDF zum Platzieren" +placeSignatureButton = "Signatur auf PDF platzieren" +reason = "Grund (optional)" +reasonPlaceholder = "Warum signieren Sie?" +removeImage = "Bild entfernen" +removeCertFile = "Datei entfernen" +savedSignatures = "Gespeicherte Signaturen" +selectFile = "Bilddatei auswählen" +selectSignatureTitle = "Signatur auswählen oder erstellen" +signButton = "Dokument signieren" +signatureInfo = "Diese Einstellungen werden vom Dokumenteigentümer konfiguriert" +signaturePlaced = "Signatur auf Seite platziert" +signatureSettings = "Signatur-Einstellungen" +signatureText = "Signaturtext" +signatureTextPlaceholder = "Geben Sie Ihren Namen ein..." +signatureTypeLabel = "Signaturtyp" +signingTitle = "Signieren" +textColor = "Textfarbe" +typeSignature = "Geben Sie Ihren Namen ein, um eine Signatur zu erstellen" +uploadCert = "Eigenes Zertifikat" +uploadCertDesc = "Verwenden Sie Ihr eigenes P12/PFX-Zertifikat" +uploadSignature = "Laden Sie Ihr Signaturbild hoch" +usePersonalCert = "Persönliches Zertifikat" +usePersonalCertDesc = "Automatisch für Ihr Konto erzeugt" +useServerCert = "Organisationszertifikat" +useServerCertDesc = "Geteiltes Organisationszertifikat" +workbenchTitle = "Signaturanfrage" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Strichfarbe auswählen" +continue = "Weiter" + +[certSign.collab.signRequest.certModal] +description = "Sie haben {{count}} Signatur(en) platziert. Wählen Sie Ihr Zertifikat, um das Signieren abzuschließen." +sign = "Dokument signieren" +certValidating = "Zertifikat wird validiert..." +certValidUntil = "Zertifikat gültig bis {{date}}" +certInvalid = "Zertifikat ungültig: {{error}}" +certInvalidFallback = "Ungültiges Zertifikat" +certNetworkError = "Zertifikat konnte nicht validiert werden" +title = "Zertifikat konfigurieren" + +[certSign.collab.signRequest.image] +hint = "Laden Sie ein PNG- oder JPG-Bild Ihrer Signatur hoch" + +[certSign.collab.signRequest.mode] +move = "Signatur verschieben" +place = "Signatur platzieren" +title = "Signier- oder Verschiebemodus" + +[certSign.collab.signRequest.modeTabs] +draw = "Zeichnen" +image = "Hochladen" +text = "Eingeben" + +[certSign.collab.signRequest.placeSignature] +message = "Klicken Sie auf das PDF, um Ihre Signatur zu platzieren" +title = "Signatur platzieren" + +[certSign.collab.signRequest.preview] +imageAlt = "Ausgewählte Signatur" +missing = "Keine Vorschau" +textFallback = "Signatur" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Gezeichnete Signatur" +defaultImageLabel = "Hochgeladene Signatur" +defaultLabel = "Signatur" +defaultTextLabel = "Eingegebene Signatur" +delete = "Signatur löschen" +none = "Keine gespeicherten Signaturen" + +[certSign.collab.signRequest.signatureType] +draw = "Zeichnen" +type = "Eingeben" +upload = "Hochladen" + +[certSign.collab.signRequest.steps] +back = "Zurück" +cancelPlacement = "Platzierung abbrechen" +certificate = "Zertifikat" +clickMultipleTimes = "Klicken Sie mehrfach auf das PDF, um Signaturen zu platzieren. Ziehen Sie eine Signatur, um sie zu verschieben oder zu skalieren." +clickToPlace = "Klicken Sie auf das PDF, wo Ihre Signatur erscheinen soll." +continue = "Weiter zur Zertifikatsauswahl" +continueToPlacement = "Weiter zur Platzierung" +continueToReview = "Weiter zur Prüfung" +createSignature = "Signatur erstellen" +invisible = "Unsichtbar" +location = "Ort:" +multipleSignatures = "{{count}} Signaturen werden auf das PDF angewendet" +oneSignature = "1 Signatur wird auf das PDF angewendet" +placeOnPdf = "Auf PDF platzieren" +reason = "Grund:" +reviewTitle = "Vor dem Signieren prüfen" +signaturePlaced = "Signatur auf Seite {{page}} platziert. Sie können die Position durch erneutes Klicken anpassen oder mit der Prüfung fortfahren." +visible = "Sichtbar" +visibility = "Sichtbarkeit:" +yourSignatures = "Ihre Signaturen ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Farbe" +fontLabel = "Schriftart" +fontSizeLabel = "Größe" +fontSizePlaceholder = "16" +label = "Signaturtext" +modalHint = "Geben Sie Ihren Namen ein und klicken Sie dann auf Weiter, um ihn auf dem PDF zu platzieren." +placeholder = "Geben Sie Ihren Namen ein..." + +[certSign.collab.participant] +certValidating = "Zertifikat wird validiert..." +certValid = "✓ Zertifikat gültig" +certValidUntil = " bis {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Ungültiges Zertifikat" +certNetworkError = "Zertifikat konnte nicht validiert werden" + +[certSign.collab.addParticipants] +add = "{{count}} Teilnehmer hinzufügen" +back = "Zurück" +configureSignatures = "Signatur-Einstellungen konfigurieren" +continue = "Weiter zu den Signatur-Einstellungen" +reasonHelp = "Voreinstellung eines Signiergrundes für diese Teilnehmer (optional, kann beim Signieren überschrieben werden)" +reasonPlaceholder = "z. B. Genehmigung, Prüfung..." +selectUsers = "Benutzer auswählen" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Seite mit Signaturübersicht einschließen" +includeSummaryPageHelp = "Am Ende wird eine Übersichtsseite mit allen Signatur-Metadaten hinzugefügt. Die Signaturfelder des digitalen Zertifikats auf einzelnen Seiten werden unterdrückt (Handsignaturen sind nicht betroffen)." + +[certSign.collab.sessionList] +active = "Aktiv" +finalized = "Finalisiert" + +[certSign.collab.signatureSettings] +description = "Legen Sie fest, wie Signaturen für alle Teilnehmer erscheinen" +title = "Signaturdarstellung" + +[certSign.collab.userSelector] +inviteUsers = "Benutzer hinzufügen" +loadError = "Benutzer konnten nicht geladen werden" +noTeam = "Kein Team" +noUsers = "Keine weiteren Benutzer gefunden." +placeholder = "Benutzer auswählen..." + +[certSign.mobile] +panelActions = "Aktionen" +panelDocument = "Dokument" +panelPeople = "Personen" + +[certSign.sessions] +deleted = "Sitzung gelöscht" +fetchFailed = "Sitzungsdetails konnten nicht geladen werden" +finalized = "Sitzung finalisiert" +loaded = "Signiertes PDF geladen" +pdfNotReady = "PDF noch nicht bereit" +pdfNotReadyDesc = "Das signierte PDF wird erzeugt. Bitte versuchen Sie es in Kürze erneut." + +[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" + [changeCreds] changePassword = "Sie verwenden die Standard-Zugangsdaten. Bitte geben Sie ein neues Passwort ein." changeUsername = "Aktualisieren Sie Ihren Benutzernamen. Nach dem Update werden Sie abgemeldet." @@ -3242,6 +3531,46 @@ totalSelected = "Gesamt ausgewählt" unsupported = "Nicht unterstützt" unzip = "Entpacken" uploadError = "Einige Dateien konnten nicht hochgeladen werden." +copyCreated = "Kopie auf diesem Gerät gespeichert." +copyFailed = "Kopie konnte nicht erstellt werden." +leaveShare = "Aus meiner Liste entfernen" +leaveShareFailed = "Die freigegebene Datei konnte nicht entfernt werden." +leaveShareSuccess = "Aus Ihrer Freigabeliste entfernt." +removeBoth = "Aus beiden entfernen" +removeFilePrompt = "Diese Datei ist auf diesem Gerät und auf Ihrem Server gespeichert. Wo möchten Sie sie entfernen?" +removeFileTitle = "Datei entfernen" +removeLocalOnly = "Nur dieses Gerät" +removeServerFailed = "Die Datei konnte nicht vom Server entfernt werden." +removeServerOnly = "Nur Server" +removeServerOnlyPrompt = "Diese Datei ist nur auf Ihrem Server gespeichert. Möchten Sie sie vom Server entfernen?" +removeServerSuccess = "Vom Server entfernt." +removeSharedPrompt = "Diese Datei wurde für Sie freigegeben. Sie können sie von diesem Gerät oder aus Ihrer Freigabeliste entfernen." +removeSharedServerOnlyBlockedPrompt = "Diese Datei wurde für Sie freigegeben und ist nur auf dem Server gespeichert." +removeSharedServerOnlyPrompt = "Diese Datei wurde für Sie freigegeben und ist nur auf dem Server gespeichert. Aus Ihrer Liste entfernen?" +changesNotUploaded = "Änderungen nicht hochgeladen" +cloudFile = "Cloud-Datei" +filterAll = "Alle" +filterLocal = "Lokal" +filterSharedByMe = "Von mir freigegeben" +filterSharedWithMe = "Für mich freigegeben" +lastSynced = "Zuletzt synchronisiert" +localOnly = "Nur lokal" +makeCopy = "Kopie erstellen" +owner = "Eigentümer" +ownerUnknown = "Unbekannt" +share = "Freigeben" +shareSelected = "Auswahl freigeben" +sharedByYou = "Von Ihnen freigegeben" +sharedEditNoticeBody = "Sie haben keine Bearbeitungsrechte für die Serverversion dieser Datei. Alle Änderungen werden als lokale Kopie gespeichert." +sharedEditNoticeConfirm = "Verstanden" +sharedEditNoticeTitle = "Schreibgeschützte Serverkopie" +sharedWithYou = "Für Sie freigegeben" +sharing = "Freigabe" +storageState = "Speicher" +synced = "Synchronisiert" +updateOnServer = "Auf dem Server aktualisieren" +uploadSelected = "Auswahl hochladen" +uploadToServer = "Auf den Server hochladen" [files] addFiles = "Dateien hinzufügen" @@ -3367,6 +3696,77 @@ title = "Über das Abflachen von PDFs" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Über das Gruppensignieren" + +[groupSigning.tooltip.finalization] +bullet1 = "Alle Signaturen werden in der von Ihnen festgelegten Teilnehmerreihenfolge angewendet" +bullet2 = "Sie können bei Bedarf mit teilweisen Signaturen finalisieren" +bullet3 = "Nach dem Finalisieren kann die Sitzung nicht mehr geändert werden" +description = "Sobald alle Teilnehmer signiert haben (oder Sie sich für eine vorzeitige Finalisierung entscheiden), können Sie das endgültige signierte PDF erzeugen." +title = "Finalisierungsprozess" + +[groupSigning.tooltip.roles] +bullet1 = "Eigentümer (Sie): erstellt die Sitzung, konfiguriert Signaturstandards, finalisiert das Dokument" +bullet2 = "Teilnehmer: Erstellen ihre Signatur, wählen ein Zertifikat und platzieren sie im PDF" +bullet3 = "Teilnehmer können Sichtbarkeit, Grund oder Ort der Signatur nicht ändern" +description = "Sie steuern die Einstellungen zur Signaturdarstellung für alle Teilnehmer." +title = "Teilnehmerrollen" + +[groupSigning.tooltip.sequential] +bullet1 = "Der erste Teilnehmer muss signieren, bevor der zweite auf das Dokument zugreifen kann" +bullet2 = "Stellt die korrekte Signierreihenfolge für rechtliche Konformität sicher" +bullet3 = "Sie können die Teilnehmer durch Ziehen in der Liste neu anordnen" +description = "Teilnehmer signieren Dokumente in der von Ihnen festgelegten Reihenfolge. Jeder Unterzeichner erhält eine Benachrichtigung, wenn er an der Reihe ist." +title = "Sequenzielles Signieren" + +[groupSigning.steps] +back = "Zurück" +completed = "Abgeschlossen" +current = "Aktuell" +stepLabel = "Schritt {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Weiter zur Prüfung" +invisible = "Signaturen sind unsichtbar (nur Metadaten)" +locationLabel = "Ort:" +preview = "Vorschau" +reasonLabel = "Grund:" +title = "Signatur-Einstellungen konfigurieren" +visible = "Signaturen sind auf Seite {{page}} sichtbar" + +[groupSigning.steps.review] +document = "Dokument" +dueDate = "Fälligkeitsdatum (optional)" +dueDatePlaceholder = "Fälligkeitsdatum auswählen..." +invisible = "Unsichtbar (nur Metadaten)" +location = "Ort:" +logo = "Logo:" +logoHidden = "Kein Logo" +logoShown = "Stirling PDF-Logo wird angezeigt" +participants = "Teilnehmer" +reason = "Grund:" +send = "Signaturanfragen senden" +signatureSettings = "Signatur-Einstellungen" +title = "Sitzungsdetails prüfen" +titleShort = "Prüfen & Senden" +visibility = "Sichtbarkeit:" +visible = "Auf Seite {{page}} sichtbar" +participantCount = "{{count}} Teilnehmer signieren der Reihe nach" + +[groupSigning.steps.selectDocument] +continue = "Weiter zur Teilnehmerauswahl" +noFile = "Bitte wählen Sie eine einzelne PDF-Datei aus Ihren aktiven Dateien, um eine Signiersitzung zu erstellen." +selectedFile = "Ausgewähltes Dokument" +title = "Dokument auswählen" + +[groupSigning.steps.selectParticipants] +continue = "Weiter zu den Signatur-Einstellungen" +count = "{{count}} Teilnehmer ausgewählt" +label = "Teilnehmer auswählen" +placeholder = "Teilnehmer zum Signieren auswählen..." +title = "Teilnehmer auswählen" + [getPdfInfo] downloadJson = "Als JSON herunterladen" downloads = "Downloads" @@ -4460,7 +4860,10 @@ zoomOut = "Verkleinern" [viewer] cannotPreviewFile = "Datei kann nicht in der Vorschau angezeigt werden" +disableColorFilter = "Farbfilter deaktivieren" dualPageView = "Doppelseitenansicht" +enableDarkFilter = "Dunkelfilter aktivieren" +enableSepiaFilter = "Sepia-Filter aktivieren" firstPage = "Erste Seite" lastPage = "Letzte Seite" nextPage = "Nächste Seite" @@ -4470,6 +4873,22 @@ singlePageView = "Einzelseitenansicht" unknownFile = "Unbekannte Datei" zoomIn = "Vergrößern" zoomOut = "Verkleinern" +resetZoom = "Zoom zurücksetzen" + +[viewer.nonPdf] +fileTypeBadge = "{{type}}-Datei" +convertToPdf = "In PDF konvertieren" +loading = "Laden..." +emptyFile = "Leere Datei" +csvStats = "{{rows}} Zeilen · {{columns}} Spalten · {{size}}" +sortedBy = "Sortiert nach: {{column}}" +columnDefault = "Spalte {{index}}" +htmlPreviewWarning = "HTML-Vorschau — externe Ressourcen werden möglicherweise nicht geladen · {{size}}" +htmlPreview = "HTML-Vorschau" +invalidJson = "Ungültiges JSON — Rohinhalt wird angezeigt" +textStats = "{{lines}} Zeilen · {{size}}" +lineNumbers = "Zeilennummern" +renderMarkdown = "Markdown rendern" [viewer.attachments] title = "Attachments" @@ -4531,6 +4950,7 @@ toggleAttachments = "Toggle Attachments" toggleTheme = "Design wechseln" language = "Sprache" toggleAnnotations = "Anmerkungen ein-/ausblenden" +toggleLayers = "Ebenen umschalten" search = "PDF durchsuchen" panMode = "Verschiebemodus" applyRedactionsFirst = "Apply redactions first" @@ -5407,20 +5827,72 @@ title = "Datei drucken" 2 = "Druckernamen eingeben" [quickAccess] +access = "Zugriff" +accessAddPerson = "Weitere Person hinzufügen" +accessBack = "Zurück" +accessCopyLink = "Link kopieren" +accessEmail = "E-Mail-Adresse" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Datei" +accessGeneral = "Allgemeiner Zugriff" +accessInviteTitle = "Personen einladen" +accessOwner = "Eigentümer" +accessPanel = "Dokumentzugriff" +accessPeople = "Personen mit Zugriff" +accessRemove = "Entfernen" +accessRestricted = "Eingeschränkt" +accessRestrictedHint = "Nur Personen mit Zugriff können öffnen" +accessRole = "Rolle" +accessRoleCommenter = "Kommentator" +accessRoleEditor = "Bearbeiter" +accessRoleViewer = "Betrachter" +accessSelectedFile = "Ausgewählte Datei" +accessSendInvite = "Einladung senden" +accessTitle = "Dokumentzugriff" +accessYou = "Sie" account = "Konto" +activeSessions = "Aktive Sitzungen" +activeTab = "Aktiv" activity = "Verlauf" adminSettings = "Admin Optionen" +allSessions = "Alle Sitzungen" allTools = "Werkzeuge" automate = "Autom." +back = "Zurück" +certSign = "Mit Zertifikat signieren" +completedSessions = "Abgeschlossene Sitzungen" +completedTab = "Abgeschlossen" config = "Konfig" +createNew = "Neue Anfrage erstellen" +createSession = "Signaturanfrage erstellen" +dueDate = "Fälligkeitsdatum (optional)" files = "Dateien" help = "Hilfe" +noActiveSessions = "Keine ausstehenden Signaturanfragen oder aktiven Sitzungen" +noCompletedSessions = "Keine abgeschlossenen Sitzungen" +noFile = "Keine Datei ausgewählt" read = "Lesen" reader = "Reader" +refresh = "Aktualisieren" +requestSignatures = "Signaturen anfordern" +selectSingleFileToRequest = "Wählen Sie eine einzelne PDF-Datei, um Signaturen anzufordern" +selectedFile = "Ausgewählte Datei" +selectUsers = "Benutzer zum Signieren auswählen" +selectUsersPlaceholder = "Teilnehmer auswählen..." +sendingRequest = "Wird gesendet..." settings = "Optionen" showMeAround = "Rundgang starten" sign = "Signatur" +signatureRequests = "Signaturanfragen" +signYourself = "Selbst signieren" +newRequest = "Neue Anfrage" tours = "Touren" +wetSign = "Signatur hinzufügen" +filterMine = "Meine" +filterOverdue = "Überfällig" +filterSigned = "Signiert" +filterDeclined = "Abgelehnt" +searchDocuments = "Dokumente suchen…" [quickAccess.helpMenu] adminTour = "Admin-Tour" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Ihr Stirling-PDF-Server ist offline und „{{endpoint expired = "Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut." refreshPage = "Seite aktualisieren" +[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" + +[signatureSettings.tooltip] +header = "Einstellungen für Signaturdarstellung" + +[signatureSettings.tooltip.location] +bullet1 = "Beispiele: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Nicht dasselbe wie die Seitenposition" +bullet3 = "Kann in bestimmten Rechtsräumen erforderlich sein" +description = "Optionaler geografischer Ort, an dem die Signatur angewendet wurde. Wird in den Zertifikat-Metadaten gespeichert." +title = "Signaturort" + +[signatureSettings.tooltip.logo] +bullet1 = "Wird neben Signatur und Text angezeigt" +bullet2 = "Unterstützt PNG- und JPG-Formate" +bullet3 = "Verbessert das professionelle Erscheinungsbild" +description = "Fügen Sie sichtbaren Signaturen ein Firmenlogo für Branding und Authentizität hinzu." +title = "Firmenlogo" + +[signatureSettings.tooltip.reason] +bullet1 = "Beispiele: \"Genehmigung\", \"Vertragsvereinbarung\", \"Prüfung abgeschlossen\"" +bullet2 = "Sichtbar in den PDF-Signatureigenschaften" +bullet3 = "Nützlich für Audit-Trails und Compliance" +description = "Optionaler Text, der erklärt, warum das Dokument signiert wird. Wird in den Zertifikat-Metadaten gespeichert." +title = "Signiergrund" + +[signatureSettings.tooltip.visibility] +bullet1 = "Sichtbar: Signatur erscheint im PDF mit benutzerdefinierter Darstellung" +bullet2 = "Unsichtbar: Zertifikat wird ohne sichtbare Markierung eingebettet" +bullet3 = "Unsichtbare Signaturen bieten dennoch kryptografische Validierung" +description = "Steuert, ob die Signatur im Dokument sichtbar ist oder unsichtbar eingebettet wird." +title = "Signatursichtbarkeit" + [settings.configuration] advanced = "Erweitert" database = "Datenbank" endpoints = "Endpunkte" features = "Funktionen" +storageSharing = "Dateispeicher & Freigabe" systemSettings = "Systemeinstellungen" title = "Konfiguration" @@ -6332,10 +6868,13 @@ title = "Bei Stirling anmelden" [setup.selfhosted] link = "oder mit einem selbstgehosteten Konto verbinden" subtitle = "Geben Sie Ihre Server-Anmeldedaten ein" +changeServerLocked = "Ihre Organisation hat diese App auf einen bestimmten Server beschränkt" switchToLocal = "Stattdessen lokale Tools verwenden" title = "Am Server anmelden" [setup.selfhosted.unreachable] +changeServer = "Mit einem anderen Server verbinden" +changeServerLocked = "Ihre Organisation hat diese App auf einen bestimmten Server beschränkt" continueOffline = "Stattdessen lokale Tools verwenden" message = "Konnte {{url}} nicht erreichen. Prüfen Sie, ob der Server läuft und erreichbar ist." retry = "Erneut versuchen" @@ -6529,6 +7068,15 @@ saved = "Gespeichert" text = "Text" title = "Signaturtyp" +[signRequest] +declined = "Signaturanfrage abgelehnt" +fetchFailed = "Signaturanfrage konnte nicht geladen werden" +signed = "Dokument erfolgreich signiert" + +[signSession] +createFailed = "Signaturanfrage konnte nicht erstellt werden" +created = "Signaturanfrage gesendet" + [signup] accountCreatedSuccessfully = "Konto erfolgreich erstellt! Sie können sich jetzt anmelden." alreadyHaveAccount = "Sie haben bereits ein Konto? Anmelden" @@ -6807,6 +7355,106 @@ title = "PDF nach Kapiteln aufteilen" [splitPdfByChapters] tags = "aufteilen,kapitel,lesezeichen,organisieren" +[storageShare] +accessed = "Zugegriffen" +accessDenied = "Sie haben keinen Zugriff auf diese freigegebene Datei. Bitten Sie den Eigentümer, sie für Sie freizugeben." +accessFailed = "Aktivität kann nicht geladen werden." +accessDeniedBody = "Sie haben keinen Zugriff auf diese Datei. Bitten Sie den Eigentümer, sie für Sie freizugeben." +accessDeniedTitle = "Kein Zugriff" +accessLimitedCommenter = "Kommentarzugriff ist bald verfügbar. Bitten Sie den Eigentümer um Bearbeiterzugriff, wenn Sie herunterladen müssen." +accessLimitedTitle = "Eingeschränkter Zugriff" +accessLimitedViewer = "Dieser Link ist nur zum Ansehen. Bitten Sie den Eigentümer um Bearbeiterzugriff, wenn Sie herunterladen müssen." +createdAt = "Erstellt" +download = "Herunterladen" +downloadFailed = "Diese Datei kann nicht heruntergeladen werden." +expiredBody = "Dieser Freigabelink ist ungültig oder abgelaufen." +expiredTitle = "Link abgelaufen" +goToLogin = "Zum Login" +loadFailed = "Freigegebene Datei kann nicht geöffnet werden." +loading = "Freigabelink wird geladen..." +loginPrompt = "Melden Sie sich an, um auf diese freigegebene Datei zuzugreifen." +loginRequired = "Anmeldung erforderlich" +openInApp = "In Stirling PDF öffnen" +ownerLabel = "Eigentümer" +ownerUnknown = "Unbekannt" +requiresLogin = "Für diese freigegebene Datei ist eine Anmeldung erforderlich." +roleCommenter = "Kommentator" +roleEditor = "Bearbeiter" +roleViewer = "Betrachter" +shareHeading = "Freigegebene Datei" +titleDefault = "Freigegebene Datei" +tryAgain = "Bitte versuchen Sie es später erneut." +addUser = "Hinzufügen" +commenterHint = "Kommentieren ist bald verfügbar." +copied = "Link in die Zwischenablage kopiert" +copy = "Kopieren" +copyFailed = "Kopieren fehlgeschlagen" +description = "Erstellen Sie einen Freigabelink für diese Datei. Angemeldete Benutzer mit dem Link können darauf zugreifen." +downloadsCount = "Downloads: {{count}}" +emailWarningBody = "Das sieht nach einer E-Mail-Adresse aus. Wenn diese Person noch kein Stirling PDF-Benutzer ist, kann sie nicht auf die Datei zugreifen." +emailWarningConfirm = "Trotzdem freigeben" +emailWarningTitle = "E-Mail-Adresse" +errorTitle = "Freigabe fehlgeschlagen" +failure = "Freigabelink kann nicht erstellt werden. Bitte versuchen Sie es erneut." +fileLabel = "Datei" +generate = "Link erzeugen" +generated = "Freigabelink erstellt" +hideActivity = "Aktivität ausblenden" +invalidUsername = "Geben Sie einen gültigen Benutzernamen oder eine E-Mail-Adresse ein." +lastAccessed = "Zuletzt zugegriffen" +linkAccessTitle = "Zugriff per Freigabelink" +linkLabel = "Freigabelink" +linksDisabled = "Freigabelinks sind deaktiviert." +linksDisabledBody = "Freigabelinks sind durch Ihre Servereinstellungen deaktiviert." +manage = "Freigabe verwalten" +manageDescription = "Erstellen und verwalten Sie Links, um diese Datei freizugeben." +manageLoadFailed = "Freigabelinks können nicht geladen werden." +manageTitle = "Freigabe verwalten" +noActivity = "Noch keine Aktivität." +noLinks = "Noch keine aktiven Freigabelinks." +noSharedUsers = "Noch keine Benutzer mit Zugriff." +removeLink = "Link entfernen" +removeUser = "Entfernen" +revokeFailed = "Freigabelink kann nicht entfernt werden." +revoked = "Freigabelink entfernt" +roleLabel = "Rolle" +sharingDisabled = "Freigabe ist deaktiviert." +sharingDisabledBody = "Die Freigabe wurde durch Ihre Servereinstellungen deaktiviert." +sharedUsersTitle = "Freigegebene Benutzer" +title = "Datei freigeben" +unknownUser = "Unbekannter Benutzer" +userAddFailed = "Freigabe für diesen Benutzer nicht möglich." +userAdded = "Benutzer zur Freigabeliste hinzugefügt." +usernameLabel = "Benutzername oder E-Mail" +usernamePlaceholder = "Geben Sie einen Benutzernamen oder eine E-Mail ein" +userRemoveFailed = "Benutzer konnte nicht entfernt werden." +userRemoved = "Benutzer aus Freigabeliste entfernt." +viewActivity = "Aktivität anzeigen" +viewed = "Angesehen" +viewsCount = "Aufrufe: {{count}}" +downloaded = "Heruntergeladen" +bulkDescription = "Einen Link erstellen, um alle ausgewählten Dateien mit angemeldeten Benutzern freizugeben." +bulkTitle = "Ausgewählte Dateien freigeben" +copyLink = "Freigabelink kopieren" +fileCount = "{{count}} Dateien ausgewählt" +ownerOnly = "Nur der Besitzer kann die Freigabe verwalten." +selectSingleFile = "Wählen Sie eine einzelne Datei aus, um die Freigabe zu verwalten." + +[storageUpload] +description = "Dadurch wird die aktuelle Datei in den Serverspeicher hochgeladen, damit Sie selbst darauf zugreifen können." +errorTitle = "Hochladen fehlgeschlagen" +failure = "Hochladen fehlgeschlagen. Bitte überprüfen Sie Ihre Anmelde- und Speichereinstellungen." +fileLabel = "Datei" +hint = "Öffentliche Links und Zugriffsmodi werden durch Ihre Servereinstellungen gesteuert." +success = "Auf Server hochgeladen" +title = "Auf Server hochladen" +updateButton = "Auf Server aktualisieren" +uploadButton = "Auf Server hochladen" +bulkDescription = "Dies lädt die ausgewählten Dateien in Ihren Serverspeicher hoch." +bulkTitle = "Ausgewählte Dateien hochladen" +fileCount = "{{count}} Dateien ausgewählt" +more = " +{{count}} weitere" + [storage] approximateSize = "Ungefähre Größe" fileTooLarge = "Datei zu groß. Maximale Größe pro Datei ist" @@ -7153,6 +7801,30 @@ title = "PDF anzeigen/bearbeiten" [warning] tooltipTitle = "Warnung" +[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" + [watermark] completed = "Wasserzeichen hinzugefügt" desc = "Text- oder Bildwasserzeichen zu PDF-Dateien hinzufügen" @@ -7333,6 +8005,7 @@ activeSession = "Aktive Sitzung" addMembers = "Mitglieder hinzufügen" admin = "Admin" confirmDelete = "Sind Sie sicher, dass Sie diesen Benutzer löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden." +confirmUnlock = "Möchten Sie dieses Benutzerkonto wirklich entsperren?" deleteUser = "Benutzer löschen" deleteUserError = "Benutzer konnte nicht gelöscht werden" deleteUserSuccess = "Benutzer erfolgreich gelöscht" @@ -7341,6 +8014,8 @@ disable = "Deaktivieren" disabled = "Deaktiviert" editRole = "Rolle bearbeiten" enable = "Aktivieren" +locked = "gesperrt" +lockedBadge = "Gesperrt" loading = "Personen werden geladen..." loginRequired = "Zuerst Login-Modus aktivieren" member = "Mitglied" @@ -7350,6 +8025,9 @@ searchMembers = "Mitglieder suchen..." status = "Status" team = "Team" title = "Personen" +unlockAccount = "Konto entsperren" +unlockUserError = "Benutzerkonto konnte nicht entsperrt werden" +unlockUserSuccess = "Benutzerkonto erfolgreich entsperrt" user = "Benutzer" [workspace.people.actions] diff --git a/frontend/public/locales/el-GR/translation.toml b/frontend/public/locales/el-GR/translation.toml index 040e813f83..eabfeab528 100644 --- a/frontend/public/locales/el-GR/translation.toml +++ b/frontend/public/locales/el-GR/translation.toml @@ -8,6 +8,7 @@ black = "ΜαÏÏο" blue = "Μπλε" bored = "ΒαÏιέστε την αναμονή;" cancel = "ΆκυÏο" +confirm = "Επιβεβαίωση" changedCredsMessage = "Τα διαπιστευτήÏια άλλαξαν!" chooseFile = "Επιλέξτε αÏχείο" close = "Κλείσιμο" @@ -146,6 +147,7 @@ insufficientCredits = "ΑνεπαÏκείς μονάδες. ΑπαιτοÏντα loadingCredits = "Έλεγχος μονάδων..." loadingProStatus = "Έλεγχος κατάστασης συνδÏομής..." noticeTopUpOrPlan = "Μη επαÏκείς μονάδες, παÏακαλοÏμε ανανεώστε το υπόλοιπο ή αναβαθμίστε σε πλάνο" +accessInvite = "ΠÏόσκληση" [account] accountSettings = "Ρυθμίσεις λογαÏιασμοÏ" @@ -1427,6 +1429,34 @@ title = "ΕπεξεÏγασία" description = "Μέγιστος χÏόνος αναμονής επεξεÏγασίας Ï€Ïιν αναφεÏθεί σφάλμα." label = "ΧÏονικό ÏŒÏιο επεξεÏγασίας (δευτ.)" +[admin.settings.storage] +description = "Έλεγχος αποθήκευσης διακομιστή και επιλογών κοινής χÏήσης." +title = "Αποθήκευση ΑÏχείων & Κοινή ΧÏήση" + +[admin.settings.storage.enabled] +description = "Îα επιτÏέπεται στους χÏήστες να αποθηκεÏουν αÏχεία στον διακομιστή." +label = "ΕνεÏγοποίηση αποθήκευσης αÏχείων στον διακομιστή" + +[admin.settings.storage.sharing.email] +description = "Îα επιτÏέπεται η κοινή χÏήση με διευθÏνσεις email." +label = "ΕνεÏγοποίηση κοινής χÏήσης μέσω email" +mailLink = "ΔιαμόÏφωση Ïυθμίσεων αλληλογÏαφίας" +mailNote = "Απαιτεί ÏÏθμιση αλληλογÏαφίας. " + +[admin.settings.storage.sharing.enabled] +description = "Îα επιτÏέπεται στους χÏήστες να μοιÏάζονται αποθηκευμένα αÏχεία." +label = "ΕνεÏγοποίηση κοινής χÏήσης" + +[admin.settings.storage.sharing.links] +description = "Îα επιτÏέπεται η κοινή χÏήση μέσω συνδέσμων με σÏνδεση." +frontendUrlLink = "ΡÏθμιση στις Ρυθμίσεις Συστήματος" +frontendUrlNote = "Απαιτεί Frontend URL. " +label = "ΕνεÏγοποίηση συνδέσμων κοινής χÏήσης" + +[admin.settings.storage.signing.enabled] +description = "Îα επιτÏέπεται στους χÏήστες να δημιουÏγοÏν συνεδÏίες υπογÏαφής εγγÏάφων με πολλοÏÏ‚ συμμετέχοντες. Απαιτεί ενεÏγοποιημένη την αποθήκευση αÏχείων στον διακομιστή." +label = "ΕνεÏγοποίηση ομαδικής υπογÏαφής (Alpha)" + [admin.settings.unsavedChanges] cancel = "Συνέχεια επεξεÏγασίας" discard = "ΑπόÏÏιψη αλλαγών" @@ -2059,7 +2089,19 @@ numbers = "ΑÏιθμοί/εÏÏη: 5, 10-20" progressions = "ΠÏόοδοι: 3n, 4n+1" [certSign] +allSigned = "Όλοι οι συμμετέχοντες έχουν υπογÏάψει. Έτοιμο για οÏιστικοποίηση." +awaitingSignatures = "Αναμονή υπογÏαφών" +signatureProgress = "{{signedCount}}/{{totalCount}} υπογÏαφές" chooseCertificate = "Επιλέξτε αÏχείο πιστοποιητικοÏ" +declined = "ΑποÏÏίφθηκε" +fetchFailed = "Αποτυχία φόÏτωσης δεδομένων υπογÏαφής" +finalized = "ΟÏιστικοποιήθηκε" +notified = "Σε εκκÏεμότητα" +partialNote = "ΜποÏείτε να οÏιστικοποιήσετε νωÏίτεÏα με τις Ï„Ïέχουσες υπογÏαφές. Οι μη υπογεγÏαμμένοι συμμετέχοντες θα εξαιÏεθοÏν." +pending = "Σε εκκÏεμότητα" +readyToFinalize = "Έτοιμο για οÏιστικοποίηση" +signed = "ΥπογεγÏαμμένο" +viewed = "ΠÏοβλήθηκε" chooseJksFile = "Επιλέξτε αÏχείο JKS" chooseP12File = "Επιλέξτε αÏχείο PKCS12" choosePfxFile = "Επιλέξτε αÏχείο PFX" @@ -2082,6 +2124,7 @@ title = "ΥπογÏαφή με πιστοποιητικό" invisible = "ΑόÏατη" stepTitle = "Εμφάνιση υπογÏαφής" visible = "ΟÏατή" +visibility = "ΟÏατότητα" [certSign.appearance.options] title = "ΛεπτομέÏειες υπογÏαφής" @@ -2188,6 +2231,252 @@ bullet4 = "ΜποÏεί να χÏησιμοποιήσει Ï€ÏοσαÏμοσμέ text = "Όταν ελέγχετε υπογÏαφές, το εÏγαλείο σας λέει αν είναι έγκυÏες, ποιος υπέγÏαψε το έγγÏαφο, πότε υπογÏάφηκε και αν το έγγÏαφο έχει αλλάξει από τότε που υπογÏάφηκε." title = "Έλεγχος υπογÏαφών" +[certSign.collab.finalize] +button = "ΟÏιστικοποίηση και φόÏτωση υπογεγÏαμμένου PDF" +early = "ΟÏιστικοποίηση με τις Ï„Ïέχουσες υπογÏαφές" + +[certSign.collab.sessionDetail] +addButton = "ΠÏοσθήκη συμμετεχόντων" +addParticipants = "ΠÏοσθήκη συμμετεχόντων" +addParticipantsError = "Αποτυχία Ï€Ïοσθήκης συμμετεχόντων" +backToList = "ΕπιστÏοφή στις συνεδÏίες" +deleteConfirm = "Είστε βέβαιοι; Αυτό δεν μποÏεί να αναιÏεθεί." +deleteError = "Αποτυχία διαγÏαφής συνεδÏίας" +deleted = "Η συνεδÏία διαγÏάφηκε" +deleteSession = "ΔιαγÏαφή συνεδÏίας" +dueDate = "ΗμεÏομηνία λήξης" +finalizeError = "Αποτυχία οÏιστικοποίησης συνεδÏίας" +loadPdfError = "Αποτυχία φόÏτωσης υπογεγÏαμμένου PDF" +loadSignedPdf = "ΦόÏτωση υπογεγÏαμμένου PDF στα ενεÏγά αÏχεία" +messageLabel = "Μήνυμα" +noAdditionalInfo = "Δεν υπάÏχουν Ï€Ïόσθετες πληÏοφοÏίες" +owner = "Κάτοχος" +participantRemoved = "Ο συμμετέχων αφαιÏέθηκε" +participants = "Συμμετέχοντες" +participantsAdded = "Οι συμμετέχοντες Ï€Ïοστέθηκαν με επιτυχία" +removeParticipant = "ΑφαίÏεση" +removeParticipantError = "Αποτυχία αφαίÏεσης συμμετέχοντα" +selectUsers = "Επιλέξτε χÏήστες..." +sessionInfo = "ΠληÏοφοÏίες συνεδÏίας" +workbenchTitle = "ΔιαχείÏιση συνεδÏίας" + +[certSign.collab.signRequest] +addedToFiles = "Το έγγÏαφο Ï€Ïοστέθηκε στα ενεÏγά αÏχεία" +addSignature = "ΠÏοσθέστε την υπογÏαφή σας" +addToFiles = "ΠÏοσθήκη στα ενεÏγά αÏχεία" +advancedSettings = "ΣÏνθετες Ïυθμίσεις" +backToList = "ΕπιστÏοφή στα αιτήματα υπογÏαφής" +certificateChoice = "Επιλέξτε πιστοποιητικό για υπογÏαφή" +changeSignature = "Αλλαγή υπογÏαφής" +clearSignature = "ΚαθαÏισμός υπογÏαφής" +completeAndSign = "ΟλοκλήÏωση & ΥπογÏαφή" +createNewSignature = "ΔημιουÏγία νέας υπογÏαφής" +declineButton = "ΑπόÏÏιψη" +decline = "ΑπόÏÏιψη αιτήματος" +deleteSelected = "ΔιαγÏαφή επιλεγμένης υπογÏαφής" +drawSignature = "Σχεδιάστε την υπογÏαφή σας παÏακάτω" +dueDate = "ΗμεÏομηνία λήξης" +fileTooLarge = "Το μέγεθος αÏχείου Ï€Ïέπει να είναι μικÏότεÏο από 5MB" +fontFamily = "Οικογένεια γÏαμματοσειÏάς" +fontSize = "Μέγεθος γÏαμματοσειÏάς: {{size}}px" +fontSizePlaceholder = "Μέγεθος" +from = "Από" +invalidCertFile = "Επιλέξτε αÏχείο Ï€Î¹ÏƒÏ„Î¿Ï€Î¿Î¹Î·Ï„Î¹ÎºÎ¿Ï P12 ή PFX" +invalidFileType = "Επιλέξτε ένα αÏχείο εικόνας" +location = "Τοποθεσία (ΠÏοαιÏετικό)" +locationPlaceholder = "Από Ï€Î¿Ï Ï…Ï€Î¿Î³Ïάφετε;" +message = "Μήνυμα" +noCertificate = "Επιλέξτε αÏχείο πιστοποιητικοÏ" +noSignatures = "Τοποθετήστε τουλάχιστον μία υπογÏαφή στο PDF" +p12File = "ΑÏχείο Ï€Î¹ÏƒÏ„Î¿Ï€Î¿Î¹Î·Ï„Î¹ÎºÎ¿Ï P12/PFX" +password = "Κωδικός Ï€Ïόσβασης πιστοποιητικοÏ" +passwordPlaceholder = "Εισαγάγετε κωδικό..." +penColor = "ΧÏώμα πένας" +penSize = "Μέγεθος πένας: {{size}}px" +placementActive = "Κάντε κλικ στο PDF για τοποθέτηση" +placeSignatureButton = "Τοποθέτηση υπογÏαφής στο PDF" +reason = "Λόγος (ΠÏοαιÏετικό)" +reasonPlaceholder = "Γιατί υπογÏάφετε;" +removeImage = "ΚατάÏγηση εικόνας" +removeCertFile = "ΚατάÏγηση αÏχείου" +savedSignatures = "Αποθηκευμένες υπογÏαφές" +selectFile = "Επιλογή αÏχείου εικόνας" +selectSignatureTitle = "Επιλέξτε ή δημιουÏγήστε υπογÏαφή" +signButton = "ΥπογÏαφή εγγÏάφου" +signatureInfo = "Αυτές οι Ïυθμίσεις έχουν διαμοÏφωθεί από τον κάτοχο του εγγÏάφου" +signaturePlaced = "Η υπογÏαφή τοποθετήθηκε στη σελίδα" +signatureSettings = "Ρυθμίσεις υπογÏαφής" +signatureText = "Κείμενο υπογÏαφής" +signatureTextPlaceholder = "Εισαγάγετε το όνομά σας..." +signatureTypeLabel = "ΤÏπος υπογÏαφής" +signingTitle = "ΥπογÏαφή" +textColor = "ΧÏώμα κειμένου" +typeSignature = "ΠληκτÏολογήστε το όνομά σας για να δημιουÏγήσετε υπογÏαφή" +uploadCert = "ΠÏοσαÏμοσμένο πιστοποιητικό" +uploadCertDesc = "ΧÏησιμοποιήστε το δικό σας πιστοποιητικό P12/PFX" +uploadSignature = "ΜεταφόÏτωση εικόνας υπογÏαφής" +usePersonalCert = "ΠÏοσωπικό πιστοποιητικό" +usePersonalCertDesc = "ΔημιουÏγείται αυτόματα για τον λογαÏιασμό σας" +useServerCert = "Πιστοποιητικό οÏγανισμοÏ" +useServerCertDesc = "ΚοινόχÏηστο πιστοποιητικό οÏγανισμοÏ" +workbenchTitle = "Αίτημα υπογÏαφής" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Επιλέξτε χÏώμα γÏαμμής" +continue = "Συνέχεια" + +[certSign.collab.signRequest.certModal] +description = "Έχετε τοποθετήσει {{count}} υπογÏαφή(ές). Επιλέξτε πιστοποιητικό για να ολοκληÏώσετε την υπογÏαφή." +sign = "ΥπογÏαφή εγγÏάφου" +certValidating = "Γίνεται επικÏÏωση πιστοποιητικοÏ..." +certValidUntil = "Το πιστοποιητικό ισχÏει έως {{date}}" +certInvalid = "Μη έγκυÏο πιστοποιητικό: {{error}}" +certInvalidFallback = "Μη έγκυÏο πιστοποιητικό" +certNetworkError = "Δεν ήταν δυνατή η επικÏÏωση του πιστοποιητικοÏ" +title = "ΡÏθμιση πιστοποιητικοÏ" + +[certSign.collab.signRequest.image] +hint = "ΜεταφοÏτώστε μια εικόνα PNG ή JPG της υπογÏαφής σας" + +[certSign.collab.signRequest.mode] +move = "Μετακίνηση υπογÏαφής" +place = "Τοποθέτηση υπογÏαφής" +title = "ΛειτουÏγία υπογÏαφής ή μετακίνησης" + +[certSign.collab.signRequest.modeTabs] +draw = "Σχεδίαση" +image = "ΜεταφόÏτωση" +text = "ΠληκτÏολόγηση" + +[certSign.collab.signRequest.placeSignature] +message = "Κάντε κλικ στο PDF για να τοποθετήσετε την υπογÏαφή σας" +title = "Τοποθέτηση υπογÏαφής" + +[certSign.collab.signRequest.preview] +imageAlt = "Επιλεγμένη υπογÏαφή" +missing = "ΧωÏίς Ï€Ïοεπισκόπηση" +textFallback = "ΥπογÏαφή" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Σχεδιασμένη υπογÏαφή" +defaultImageLabel = "ΜεταφοÏτωμένη υπογÏαφή" +defaultLabel = "ΥπογÏαφή" +defaultTextLabel = "ΠληκτÏολογημένη υπογÏαφή" +delete = "ΔιαγÏαφή υπογÏαφής" +none = "Δεν υπάÏχουν αποθηκευμένες υπογÏαφές" + +[certSign.collab.signRequest.signatureType] +draw = "Σχεδίαση" +type = "ΠληκτÏολόγηση" +upload = "ΜεταφόÏτωση" + +[certSign.collab.signRequest.steps] +back = "Πίσω" +cancelPlacement = "ΑκÏÏωση τοποθέτησης" +certificate = "Πιστοποιητικό" +clickMultipleTimes = "Κάντε κλικ στο PDF πολλές φοÏές για να τοποθετήσετε υπογÏαφές. ΣÏÏετε οποιαδήποτε υπογÏαφή για μετακίνηση ή αλλαγή μεγέθους." +clickToPlace = "Κάντε κλικ στο PDF στο σημείο που θέλετε να εμφανίζεται η υπογÏαφή σας." +continue = "Συνέχεια στην επιλογή πιστοποιητικοÏ" +continueToPlacement = "Συνέχεια στην τοποθέτηση" +continueToReview = "Συνέχεια στην επισκόπηση" +createSignature = "ΔημιουÏγία υπογÏαφής" +invisible = "ΑόÏατη" +location = "Τοποθεσία:" +multipleSignatures = "{{count}} υπογÏαφές θα εφαÏμοστοÏν στο PDF" +oneSignature = "1 υπογÏαφή θα εφαÏμοστεί στο PDF" +placeOnPdf = "Τοποθέτηση στο PDF" +reason = "Λόγος:" +reviewTitle = "Επισκόπηση Ï€Ïιν από την υπογÏαφή" +signaturePlaced = "Η υπογÏαφή τοποθετήθηκε στη σελίδα {{page}}. ΜποÏείτε να Ï€ÏοσαÏμόσετε τη θέση κάνοντας ξανά κλικ ή να συνεχίσετε στην επισκόπηση." +visible = "ΟÏατή" +visibility = "ΟÏατότητα:" +yourSignatures = "Οι υπογÏαφές σας ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "ΧÏώμα" +fontLabel = "ΓÏαμματοσειÏά" +fontSizeLabel = "Μέγεθος" +fontSizePlaceholder = "16" +label = "Κείμενο υπογÏαφής" +modalHint = "Εισαγάγετε το όνομά σας και έπειτα κάντε κλικ στη Συνέχεια για να το τοποθετήσετε στο PDF." +placeholder = "Εισαγάγετε το όνομά σας..." + +[certSign.collab.participant] +certValidating = "Γίνεται επικÏÏωση πιστοποιητικοÏ..." +certValid = "✓ ΈγκυÏο πιστοποιητικό" +certValidUntil = " έως {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Μη έγκυÏο πιστοποιητικό" +certNetworkError = "Δεν ήταν δυνατή η επικÏÏωση του πιστοποιητικοÏ" + +[certSign.collab.addParticipants] +add = "ΠÏοσθήκη {{count}} συμμετεχόντων" +back = "Πίσω" +configureSignatures = "ΔιαμόÏφωση Ïυθμίσεων υπογÏαφής" +continue = "Συνέχεια στις Ïυθμίσεις υπογÏαφής" +reasonHelp = "ΠÏοκαθοÏίστε έναν λόγο υπογÏαφής για αυτοÏÏ‚ τους συμμετέχοντες (Ï€ÏοαιÏετικό, μποÏοÏν να τον Ï„Ïοποποιήσουν κατά την υπογÏαφή)" +reasonPlaceholder = "Ï€.χ. ΈγκÏιση, Ανασκόπηση..." +selectUsers = "Επιλογή χÏηστών" + +[certSign.collab.sessionCreation] +includeSummaryPage = "ΣυμπεÏίληψη σελίδας σÏνοψης υπογÏαφών" +includeSummaryPageHelp = "Θα Ï€Ïοστεθεί στο τέλος μια σελίδα σÏνοψης με όλα τα μεταδεδομένα υπογÏαφής. Τα πλαίσια ψηφιακής υπογÏαφής στις μεμονωμένες σελίδες θα κατασταλοÏν (οι χειÏόγÏαφες υπογÏαφές δεν επηÏεάζονται)." + +[certSign.collab.sessionList] +active = "ΕνεÏγές" +finalized = "ΟÏιστικοποιημένες" + +[certSign.collab.signatureSettings] +description = "ΔιαμοÏφώστε πώς θα εμφανίζονται οι υπογÏαφές για όλους τους συμμετέχοντες" +title = "Εμφάνιση υπογÏαφής" + +[certSign.collab.userSelector] +inviteUsers = "ΠÏοσθήκη χÏηστών" +loadError = "Αποτυχία φόÏτωσης χÏηστών" +noTeam = "ΧωÏίς ομάδα" +noUsers = "Δεν βÏέθηκαν άλλοι χÏήστες." +placeholder = "Επιλέξτε χÏήστες..." + +[certSign.mobile] +panelActions = "ΕνέÏγειες" +panelDocument = "ΈγγÏαφο" +panelPeople = "Άτομα" + +[certSign.sessions] +deleted = "Η συνεδÏία διαγÏάφηκε" +fetchFailed = "Αποτυχία φόÏτωσης λεπτομεÏειών συνεδÏίας" +finalized = "Η συνεδÏία οÏιστικοποιήθηκε" +loaded = "Το υπογεγÏαμμένο PDF φοÏτώθηκε" +pdfNotReady = "Το PDF δεν είναι έτοιμο" +pdfNotReadyDesc = "Το υπογεγÏαμμένο PDF δημιουÏγείται. Δοκιμάστε ξανά σε λίγο." + +[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" + [changeCreds] changePassword = "ΧÏησιμοποιείτε Ï€Ïοεπιλεγμένα διαπιστευτήÏια σÏνδεσης. ΠαÏακαλώ εισάγετε νέο κωδικό" changeUsername = "ΕνημεÏώστε το όνομα χÏήστη σας. Θα αποσυνδεθείτε μετά την ενημέÏωση." @@ -3242,6 +3531,46 @@ totalSelected = "ΣÏνολο επιλεγμένων" unsupported = "Μη υποστηÏιζόμενο" unzip = "Αποσυμπίεση" uploadError = "Αποτυχία μεταφόÏτωσης οÏισμένων αÏχείων." +copyCreated = "Το αντίγÏαφο αποθηκεÏτηκε σε αυτήν τη συσκευή." +copyFailed = "Δεν ήταν δυνατή η δημιουÏγία αντιγÏάφου." +leaveShare = "ΑφαίÏεση από τη λίστα μου" +leaveShareFailed = "Δεν ήταν δυνατή η αφαίÏεση του κοινόχÏηστου αÏχείου." +leaveShareSuccess = "ΑφαιÏέθηκε από τη λίστα κοινόχÏηστων." +removeBoth = "ΑφαίÏεση και από τα δÏο" +removeFilePrompt = "Αυτό το αÏχείο είναι αποθηκευμένο σε αυτήν τη συσκευή και στον διακομιστή σας. Από Ï€Î¿Ï Î¸Î­Î»ÎµÏ„Îµ να το αφαιÏέσετε;" +removeFileTitle = "ΑφαίÏεση αÏχείου" +removeLocalOnly = "Μόνο από αυτήν τη συσκευή" +removeServerFailed = "Δεν ήταν δυνατή η αφαίÏεση του αÏχείου από τον διακομιστή." +removeServerOnly = "Μόνο από τον διακομιστή" +removeServerOnlyPrompt = "Αυτό το αÏχείο είναι αποθηκευμένο μόνο στον διακομιστή σας. Θέλετε να το αφαιÏέσετε από τον διακομιστή;" +removeServerSuccess = "ΑφαιÏέθηκε από τον διακομιστή." +removeSharedPrompt = "Αυτό το αÏχείο έχει κοινοποιηθεί σε εσάς. ΜποÏείτε να το αφαιÏέσετε από αυτήν τη συσκευή ή από τη λίστα κοινόχÏηστων." +removeSharedServerOnlyBlockedPrompt = "Αυτό το αÏχείο έχει κοινοποιηθεί σε εσάς και είναι αποθηκευμένο μόνο στον διακομιστή." +removeSharedServerOnlyPrompt = "Αυτό το αÏχείο έχει κοινοποιηθεί σε εσάς και είναι αποθηκευμένο μόνο στον διακομιστή. Îα αφαιÏεθεί από τη λίστα σας;" +changesNotUploaded = "Οι αλλαγές δεν μεταφοÏτώθηκαν" +cloudFile = "ΑÏχείο στο cloud" +filterAll = "Όλα" +filterLocal = "Τοπικά" +filterSharedByMe = "Κοινοποιημένα από εμένα" +filterSharedWithMe = "Κοινοποιημένα σε εμένα" +lastSynced = "Τελευταίος συγχÏονισμός" +localOnly = "Μόνο τοπικά" +makeCopy = "ΔημιουÏγία αντιγÏάφου" +owner = "Κάτοχος" +ownerUnknown = "Άγνωστος" +share = "Κοινή χÏήση" +shareSelected = "Κοινή χÏήση επιλεγμένων" +sharedByYou = "Κοινοποιήθηκε από εσάς" +sharedEditNoticeBody = "Δεν έχετε δικαιώματα επεξεÏγασίας για την έκδοση διακομιστή Î±Ï…Ï„Î¿Ï Ï„Î¿Ï… αÏχείου. Τυχόν επεξεÏγασίες θα αποθηκευτοÏν ως τοπικό αντίγÏαφο." +sharedEditNoticeConfirm = "Το κατάλαβα" +sharedEditNoticeTitle = "ΑντίγÏαφο διακομιστή μόνο για ανάγνωση" +sharedWithYou = "Κοινοποιήθηκε σε εσάς" +sharing = "Κοινή χÏήση" +storageState = "Αποθήκευση" +synced = "ΣυγχÏονισμένο" +updateOnServer = "ΕνημέÏωση στον διακομιστή" +uploadSelected = "ΜεταφόÏτωση επιλεγμένων" +uploadToServer = "ΜεταφόÏτωση στον διακομιστή" [files] addFiles = "ΠÏοσθήκη αÏχείων" @@ -3367,6 +3696,77 @@ title = "Σχετικά με την επιπέδωση PDF" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Σχετικά με την ομαδική υπογÏαφή" + +[groupSigning.tooltip.finalization] +bullet1 = "Όλες οι υπογÏαφές εφαÏμόζονται με τη σειÏά συμμετεχόντων που οÏίσατε" +bullet2 = "ΜποÏείτε να οÏιστικοποιήσετε με μεÏικές υπογÏαφές αν χÏειάζεται" +bullet3 = "Μόλις οÏιστικοποιηθεί, η συνεδÏία δεν μποÏεί να Ï„Ïοποποιηθεί" +description = "Μόλις όλοι οι συμμετέχοντες υπογÏάψουν (ή επιλέξετε να οÏιστικοποιήσετε νωÏίτεÏα), μποÏείτε να δημιουÏγήσετε το τελικό υπογεγÏαμμένο PDF." +title = "Διαδικασία οÏιστικοποίησης" + +[groupSigning.tooltip.roles] +bullet1 = "Κάτοχος (εσείς): ΔημιουÏγεί συνεδÏία, διαμοÏφώνει Ï€Ïοεπιλογές υπογÏαφής, οÏιστικοποιεί το έγγÏαφο" +bullet2 = "Συμμετέχοντες: ΔημιουÏγοÏν την υπογÏαφή τους, επιλέγουν πιστοποιητικό, τοποθετοÏν στο PDF" +bullet3 = "Οι συμμετέχοντες δεν μποÏοÏν να Ï„Ïοποποιήσουν τις Ïυθμίσεις οÏατότητας, λόγου ή τοποθεσίας υπογÏαφής" +description = "Ελέγχετε τις Ïυθμίσεις εμφάνισης υπογÏαφής για όλους τους συμμετέχοντες." +title = "Ρόλοι συμμετεχόντων" + +[groupSigning.tooltip.sequential] +bullet1 = "Ο Ï€Ïώτος συμμετέχων Ï€Ïέπει να υπογÏάψει Ï€Ïιν αποκτήσει Ï€Ïόσβαση ο δεÏτεÏος στο έγγÏαφο" +bullet2 = "Εξασφαλίζει τη σωστή σειÏά υπογÏαφών για νομική συμμόÏφωση" +bullet3 = "ΜποÏείτε να αλλάξετε τη σειÏά συμμετεχόντων σÏÏοντάς τους στη λίστα" +description = "Οι συμμετέχοντες υπογÏάφουν τα έγγÏαφα με τη σειÏά που οÏίζετε. Κάθε υπογÏάφων λαμβάνει ειδοποίηση όταν έÏθει η σειÏά του." +title = "Διαδοχική υπογÏαφή" + +[groupSigning.steps] +back = "Πίσω" +completed = "ΟλοκληÏώθηκε" +current = "ΤÏέχον" +stepLabel = "Βήμα {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Συνέχεια στην επισκόπηση" +invisible = "Οι υπογÏαφές θα είναι αόÏατες (μόνο μεταδεδομένα)" +locationLabel = "Τοποθεσία:" +preview = "ΠÏοεπισκόπηση" +reasonLabel = "Λόγος:" +title = "ΔιαμόÏφωση Ïυθμίσεων υπογÏαφής" +visible = "Οι υπογÏαφές θα είναι οÏατές στη σελίδα {{page}}" + +[groupSigning.steps.review] +document = "ΈγγÏαφο" +dueDate = "ΗμεÏομηνία λήξης (ΠÏοαιÏετικό)" +dueDatePlaceholder = "Επιλέξτε ημεÏομηνία λήξης..." +invisible = "ΑόÏατη (μόνο μεταδεδομένα)" +location = "Τοποθεσία:" +logo = "Λογότυπο:" +logoHidden = "ΧωÏίς λογότυπο" +logoShown = "Εμφανίζεται το λογότυπο του Stirling PDF" +participants = "Συμμετέχοντες" +reason = "Λόγος:" +send = "Αποστολή αιτημάτων υπογÏαφής" +signatureSettings = "Ρυθμίσεις υπογÏαφής" +title = "Επισκόπηση λεπτομεÏειών συνεδÏίας" +titleShort = "Επισκόπηση & Αποστολή" +visibility = "ΟÏατότητα:" +visible = "ΟÏατή στη σελίδα {{page}}" +participantCount = "{{count}} συμμετέχοντες θα υπογÏάψουν με τη σειÏά" + +[groupSigning.steps.selectDocument] +continue = "Συνέχεια στην επιλογή συμμετεχόντων" +noFile = "Επιλέξτε ένα μόνο αÏχείο PDF από τα ενεÏγά αÏχεία σας για να δημιουÏγήσετε συνεδÏία υπογÏαφής." +selectedFile = "Επιλεγμένο έγγÏαφο" +title = "Επιλογή εγγÏάφου" + +[groupSigning.steps.selectParticipants] +continue = "Συνέχεια στις Ïυθμίσεις υπογÏαφής" +count = "Επιλέχθηκαν {{count}} συμμετέχοντες" +label = "Επιλέξτε συμμετέχοντες" +placeholder = "Επιλέξτε συμμετέχοντες για υπογÏαφή..." +title = "Επιλογή συμμετεχόντων" + [getPdfInfo] downloadJson = "Λήψη JSON" downloads = "Λήψεις" @@ -4460,7 +4860,10 @@ zoomOut = "ΣμίκÏυνση" [viewer] cannotPreviewFile = "Δεν είναι δυνατή η Ï€Ïοεπισκόπηση του αÏχείου" +disableColorFilter = "ΑπενεÏγοποίηση φίλτÏου χÏώματος" dualPageView = "ΠÏοβολή διπλής σελίδας" +enableDarkFilter = "ΕνεÏγοποίηση ÏƒÎºÎ¿Ï„ÎµÎ¹Î½Î¿Ï Ï†Î¯Î»Ï„Ïου" +enableSepiaFilter = "ΕνεÏγοποίηση φίλτÏου σέπια" firstPage = "ΠÏώτη σελίδα" lastPage = "Τελευταία σελίδα" nextPage = "Επόμενη σελίδα" @@ -4470,6 +4873,22 @@ singlePageView = "ΠÏοβολή μίας σελίδας" unknownFile = "Άγνωστο αÏχείο" zoomIn = "Μεγέθυνση" zoomOut = "ΣμίκÏυνση" +resetZoom = "ΕπαναφοÏά ζουμ" + +[viewer.nonPdf] +fileTypeBadge = "ΑÏχείο {{type}}" +convertToPdf = "ΜετατÏοπή σε PDF" +loading = "ΦόÏτωση..." +emptyFile = "Κενό αÏχείο" +csvStats = "{{rows}} γÏαμμές · {{columns}} στήλες · {{size}}" +sortedBy = "Ταξινόμηση κατά: {{column}}" +columnDefault = "Στήλη {{index}}" +htmlPreviewWarning = "ΠÏοεπισκόπηση HTML — ενδέχεται να μη φοÏτωθοÏν εξωτεÏικοί πόÏοι · {{size}}" +htmlPreview = "ΠÏοεπισκόπηση HTML" +invalidJson = "Μη έγκυÏο JSON — εμφάνιση ακατέÏγαστου πεÏιεχομένου" +textStats = "{{lines}} γÏαμμές · {{size}}" +lineNumbers = "ΑÏιθμοί γÏαμμών" +renderMarkdown = "Απόδοση Markdown" [viewer.attachments] title = "Συνημμένα" @@ -4531,6 +4950,7 @@ toggleAttachments = "Εναλλαγή συνημμένων" toggleTheme = "Εναλλαγή θέματος" language = "Γλώσσα" toggleAnnotations = "Εναλλαγή οÏατότητας σχολιασμών" +toggleLayers = "Εναλλαγή επιπέδων" search = "Αναζήτηση PDF" panMode = "ΛειτουÏγία μετακίνησης" applyRedactionsFirst = "ΕφαÏμόστε Ï€Ïώτα τις αποκÏÏψεις" @@ -5407,20 +5827,72 @@ title = "ΕκτÏπωση αÏχείου" 2 = "Εισάγετε όνομα εκτυπωτή" [quickAccess] +access = "ΠÏόσβαση" +accessAddPerson = "ΠÏοσθήκη ατόμου" +accessBack = "Πίσω" +accessCopyLink = "ΑντιγÏαφή συνδέσμου" +accessEmail = "ΔιεÏθυνση email" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "ΑÏχείο" +accessGeneral = "Γενική Ï€Ïόσβαση" +accessInviteTitle = "ΠÏόσκληση ατόμων" +accessOwner = "Κάτοχος" +accessPanel = "ΠÏόσβαση στο έγγÏαφο" +accessPeople = "Άτομα με Ï€Ïόσβαση" +accessRemove = "ΑφαίÏεση" +accessRestricted = "ΠεÏιοÏισμένη" +accessRestrictedHint = "Μόνο τα άτομα με Ï€Ïόσβαση μποÏοÏν να ανοίξουν" +accessRole = "Ρόλος" +accessRoleCommenter = "Σχολιαστής" +accessRoleEditor = "ΕπεξεÏγαστής" +accessRoleViewer = "Θεατής" +accessSelectedFile = "Επιλεγμένο αÏχείο" +accessSendInvite = "Αποστολή Ï€Ïόσκλησης" +accessTitle = "ΠÏόσβαση εγγÏάφου" +accessYou = "Εσείς" account = "ΠÏοφίλ" +activeSessions = "ΕνεÏγές συνεδÏίες" +activeTab = "ΕνεÏγές" activity = "ΙστοÏικό" adminSettings = "Ρυθμ. διαχ." +allSessions = "Όλες οι συνεδÏίες" allTools = "All Tools" automate = "Αυτόματα" +back = "Πίσω" +certSign = "ΥπογÏαφή με πιστοποιητικό" +completedSessions = "ΟλοκληÏωμένες συνεδÏίες" +completedTab = "ΟλοκληÏωμένες" config = "ΡÏθμιση" +createNew = "ΔημιουÏγία νέου αιτήματος" +createSession = "ΔημιουÏγία αιτήματος υπογÏαφής" +dueDate = "ΗμεÏομηνία λήξης (Ï€ÏοαιÏετικό)" files = "ΑÏχεία" help = "Βοήθεια" +noActiveSessions = "Δεν υπάÏχουν εκκÏεμή αιτήματα υπογÏαφής ή ενεÏγές συνεδÏίες" +noCompletedSessions = "Δεν υπάÏχουν ολοκληÏωμένες συνεδÏίες" +noFile = "Δεν έχει επιλεγεί αÏχείο" read = "Ανάγνωση" reader = "Ανάγνωση" +refresh = "Ανανέωση" +requestSignatures = "Αίτηση υπογÏαφών" +selectSingleFileToRequest = "Επιλέξτε ένα μόνο αÏχείο PDF για να ζητήσετε υπογÏαφές" +selectedFile = "Επιλεγμένο αÏχείο" +selectUsers = "Επιλέξτε χÏήστες για υπογÏαφή" +selectUsersPlaceholder = "Επιλέξτε συμμετέχοντες..." +sendingRequest = "Αποστολή..." settings = "Ρυθμ." showMeAround = "Ξενάγηση" sign = "ΥπογÏαφή" +signatureRequests = "Αιτήματα υπογÏαφής" +signYourself = "ΥπογÏάψτε εσείς" +newRequest = "Îέο αίτημα" tours = "Ξεναγήσεις" +wetSign = "ΠÏοσθήκη υπογÏαφής" +filterMine = "Δικά μου" +filterOverdue = "ΕκπÏόθεσμα" +filterSigned = "ΥπογεγÏαμμένα" +filterDeclined = "ΑποÏÏιφθέντα" +searchDocuments = "Αναζήτηση εγγÏάφων…" [quickAccess.helpMenu] adminTour = "Ξενάγηση διαχειÏιστή" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Ο διακομιστής σας Stirling-PDF είν expired = "Η συνεδÏία σας έληξε. ΠαÏακαλώ ανανεώστε τη σελίδα και Ï€Ïοσπαθήστε ξανά." refreshPage = "Ανανέωση σελίδας" +[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 = "ΣειÏά υπογÏαφών" + +[signatureSettings.tooltip] +header = "Ρυθμίσεις εμφάνισης υπογÏαφής" + +[signatureSettings.tooltip.location] +bullet1 = "ΠαÏαδείγματα: \"New York, USA\", \"ΓÏαφείο Λονδίνου\", \"ΑπομακÏυσμένα\"" +bullet2 = "Δεν είναι το ίδιο με τη θέση στη σελίδα" +bullet3 = "Ενδέχεται να απαιτείται σε οÏισμένες νομικές δικαιοδοσίες" +description = "ΠÏοαιÏετική γεωγÏαφική τοποθεσία όπου εφαÏμόστηκε η υπογÏαφή. ΑποθηκεÏεται στα μεταδεδομένα του πιστοποιητικοÏ." +title = "Τοποθεσία υπογÏαφής" + +[signatureSettings.tooltip.logo] +bullet1 = "Εμφανίζεται δίπλα στην υπογÏαφή και στο κείμενο" +bullet2 = "ΥποστηÏίζει μοÏφές PNG, JPG" +bullet3 = "ΕνισχÏει την επαγγελματική εμφάνιση" +description = "ΠÏοσθέστε εταιÏικό λογότυπο σε οÏατές υπογÏαφές για επωνυμία και αυθεντικότητα." +title = "Λογότυπο εταιÏείας" + +[signatureSettings.tooltip.reason] +bullet1 = "ΠαÏαδείγματα: \"ΈγκÏιση\", \"Συμφωνία σÏμβασης\", \"ΟλοκλήÏωση ανασκόπησης\"" +bullet2 = "Εμφανίζεται στις ιδιότητες υπογÏαφής του PDF" +bullet3 = "ΧÏήσιμο για ίχνη ελέγχου και συμμόÏφωση" +description = "ΠÏοαιÏετικό κείμενο που εξηγεί γιατί υπογÏάφεται το έγγÏαφο. ΑποθηκεÏεται στα μεταδεδομένα του πιστοποιητικοÏ." +title = "Λόγος υπογÏαφής" + +[signatureSettings.tooltip.visibility] +bullet1 = "ΟÏατή: Η υπογÏαφή εμφανίζεται στο PDF με Ï€ÏοσαÏμοσμένη εμφάνιση" +bullet2 = "ΑόÏατη: Το πιστοποιητικό ενσωματώνεται χωÏίς οπτικό σημάδι" +bullet3 = "Οι αόÏατες υπογÏαφές εξακολουθοÏν να παÏέχουν κÏυπτογÏαφική επαλήθευση" +description = "Ελέγχει εάν η υπογÏαφή είναι οÏατή στο έγγÏαφο ή ενσωματωμένη αόÏατα." +title = "ΟÏατότητα υπογÏαφής" + [settings.configuration] advanced = "ΠÏοχωÏημένα" database = "Βάση δεδομένων" endpoints = "Endpoints" features = "Δυνατότητες" +storageSharing = "Αποθήκευση ΑÏχείων & Κοινή ΧÏήση" systemSettings = "Ρυθμίσεις συστήματος" title = "ΔιαμόÏφωση" @@ -6332,10 +6868,13 @@ title = "ΣÏνδεση στο Stirling" [setup.selfhosted] link = "ή συνδεθείτε σε έναν self-hosted λογαÏιασμό" subtitle = "Εισαγάγετε τα διαπιστευτήÏια του διακομιστή σας" +changeServerLocked = "Ο οÏγανισμός σας έχει πεÏιοÏίσει αυτήν την εφαÏμογή σε συγκεκÏιμένο διακομιστή" switchToLocal = "ΧÏήση τοπικών εÏγαλείων αντ' αυτοÏ" title = "ΣÏνδεση στον διακομιστή" [setup.selfhosted.unreachable] +changeServer = "ΣÏνδεση σε διαφοÏετικό διακομιστή" +changeServerLocked = "Ο οÏγανισμός σας έχει πεÏιοÏίσει αυτήν την εφαÏμογή σε συγκεκÏιμένο διακομιστή" continueOffline = "ΧÏήση τοπικών εÏγαλείων αντ' αυτοÏ" message = "Δεν ήταν δυνατή η Ï€Ïόσβαση στο {{url}}. Ελέγξτε ότι ο διακομιστής εκτελείται και είναι Ï€Ïοσβάσιμος." retry = "Επανάληψη" @@ -6529,6 +7068,15 @@ saved = "Αποθηκευμένες" text = "Κείμενο" title = "ΤÏπος υπογÏαφής" +[signRequest] +declined = "Το αίτημα υπογÏαφής αποÏÏίφθηκε" +fetchFailed = "Αποτυχία φόÏτωσης αιτήματος υπογÏαφής" +signed = "Το έγγÏαφο υπογÏάφηκε με επιτυχία" + +[signSession] +createFailed = "Αποτυχία δημιουÏγίας αιτήματος υπογÏαφής" +created = "Το αίτημα υπογÏαφής εστάλη" + [signup] accountCreatedSuccessfully = "Ο λογαÏιασμός δημιουÏγήθηκε με επιτυχία! ΜποÏείτε τώÏα να συνδεθείτε." alreadyHaveAccount = "Έχετε ήδη λογαÏιασμό; Συνδεθείτε" @@ -6807,6 +7355,106 @@ title = "ΔιαχωÏισμός PDF ανά κεφάλαια" [splitPdfByChapters] tags = "διαχωÏισμός,κεφάλαια,σελιδοδείκτες,οÏγάνωση" +[storageShare] +accessed = "ΠÏοσπελάστηκε" +accessDenied = "Δεν έχετε Ï€Ïόσβαση σε αυτό το κοινόχÏηστο αÏχείο. Ζητήστε από τον κάτοχο να το κοινοποιήσει σε εσάς." +accessFailed = "Αδυναμία φόÏτωσης δÏαστηÏιότητας." +accessDeniedBody = "Δεν έχετε Ï€Ïόσβαση σε αυτό το αÏχείο. Ζητήστε από τον κάτοχο να το κοινοποιήσει σε εσάς." +accessDeniedTitle = "ΧωÏίς Ï€Ïόσβαση" +accessLimitedCommenter = "Η Ï€Ïόσβαση σχολιαστή έÏχεται σÏντομα. Ζητήστε από τον κάτοχο δικαιώματα επεξεÏγαστή αν χÏειάζεστε λήψη." +accessLimitedTitle = "ΠεÏιοÏισμένη Ï€Ïόσβαση" +accessLimitedViewer = "Αυτός ο σÏνδεσμος είναι μόνο για Ï€Ïοβολή. Ζητήστε από τον κάτοχο Ï€Ïόσβαση επεξεÏγαστή αν χÏειάζεστε λήψη." +createdAt = "ΔημιουÏγήθηκε" +download = "Λήψη" +downloadFailed = "Δεν είναι δυνατή η λήψη Î±Ï…Ï„Î¿Ï Ï„Î¿Ï… αÏχείου." +expiredBody = "Αυτός ο σÏνδεσμος κοινής χÏήσης δεν είναι έγκυÏος ή έχει λήξει." +expiredTitle = "Ο σÏνδεσμος έληξε" +goToLogin = "Μετάβαση στη σÏνδεση" +loadFailed = "Δεν είναι δυνατό το άνοιγμα του κοινόχÏηστου αÏχείου." +loading = "ΦόÏτωση συνδέσμου κοινής χÏήσης..." +loginPrompt = "Συνδεθείτε για Ï€Ïόσβαση σε αυτό το κοινόχÏηστο αÏχείο." +loginRequired = "Απαιτείται σÏνδεση" +openInApp = "Άνοιγμα στο Stirling PDF" +ownerLabel = "Κάτοχος" +ownerUnknown = "Άγνωστος" +requiresLogin = "Αυτό το κοινόχÏηστο αÏχείο απαιτεί σÏνδεση." +roleCommenter = "Σχολιαστής" +roleEditor = "ΕπεξεÏγαστής" +roleViewer = "Θεατής" +shareHeading = "ΚοινόχÏηστο αÏχείο" +titleDefault = "ΚοινόχÏηστο αÏχείο" +tryAgain = "Δοκιμάστε ξανά αÏγότεÏα." +addUser = "ΠÏοσθήκη" +commenterHint = "Η δυνατότητα ÏƒÏ‡Î¿Î»Î¹Î±ÏƒÎ¼Î¿Ï Î­Ïχεται σÏντομα." +copied = "Ο σÏνδεσμος αντιγÏάφηκε στο Ï€ÏόχειÏο" +copy = "ΑντιγÏαφή" +copyFailed = "Η αντιγÏαφή απέτυχε" +description = "ΔημιουÏγήστε έναν σÏνδεσμο κοινής χÏήσης για αυτό το αÏχείο. Οι συνδεδεμένοι χÏήστες με τον σÏνδεσμο μποÏοÏν να έχουν Ï€Ïόσβαση." +downloadsCount = "Λήψεις: {{count}}" +emailWarningBody = "Αυτό μοιάζει με διεÏθυνση email. Αν αυτό το άτομο δεν είναι ήδη χÏήστης του Stirling PDF, δεν θα μποÏεί να έχει Ï€Ïόσβαση στο αÏχείο." +emailWarningConfirm = "Κοινή χÏήση οÏτως ή άλλως" +emailWarningTitle = "ΔιεÏθυνση email" +errorTitle = "Αποτυχία κοινής χÏήσης" +failure = "Δεν ήταν δυνατή η δημιουÏγία συνδέσμου κοινής χÏήσης. Δοκιμάστε ξανά." +fileLabel = "ΑÏχείο" +generate = "ΔημιουÏγία συνδέσμου" +generated = "ΔημιουÏγήθηκε σÏνδεσμος κοινής χÏήσης" +hideActivity = "ΑπόκÏυψη δÏαστηÏιότητας" +invalidUsername = "Εισαγάγετε έγκυÏο όνομα χÏήστη ή διεÏθυνση email." +lastAccessed = "Τελευταία Ï€Ïόσβαση" +linkAccessTitle = "ΠÏόσβαση συνδέσμου κοινής χÏήσης" +linkLabel = "ΣÏνδεσμος κοινής χÏήσης" +linksDisabled = "Οι σÏνδεσμοι κοινής χÏήσης είναι απενεÏγοποιημένοι." +linksDisabledBody = "Οι σÏνδεσμοι κοινής χÏήσης έχουν απενεÏγοποιηθεί από τις Ïυθμίσεις του διακομιστή σας." +manage = "ΔιαχείÏιση κοινής χÏήσης" +manageDescription = "ΔημιουÏγία και διαχείÏιση συνδέσμων για κοινή χÏήση Î±Ï…Ï„Î¿Ï Ï„Î¿Ï… αÏχείου." +manageLoadFailed = "Δεν είναι δυνατή η φόÏτωση συνδέσμων κοινής χÏήσης." +manageTitle = "ΔιαχείÏιση κοινής χÏήσης" +noActivity = "Καμία δÏαστηÏιότητα ακόμα." +noLinks = "Δεν υπάÏχουν ενεÏγοί σÏνδεσμοι κοινής χÏήσης ακόμα." +noSharedUsers = "Κανένας χÏήστης δεν έχει ακόμη Ï€Ïόσβαση." +removeLink = "ΚατάÏγηση συνδέσμου" +removeUser = "ΑφαίÏεση" +revokeFailed = "Δεν είναι δυνατή η αφαίÏεση του συνδέσμου κοινής χÏήσης." +revoked = "Ο σÏνδεσμος κοινής χÏήσης αφαιÏέθηκε" +roleLabel = "Ρόλος" +sharingDisabled = "Η κοινή χÏήση είναι απενεÏγοποιημένη." +sharingDisabledBody = "Η κοινή χÏήση έχει απενεÏγοποιηθεί από τις Ïυθμίσεις του διακομιστή σας." +sharedUsersTitle = "ΧÏήστες με Ï€Ïόσβαση" +title = "Κοινή χÏήση αÏχείου" +unknownUser = "Άγνωστος χÏήστης" +userAddFailed = "Δεν είναι δυνατή η κοινή χÏήση με αυτόν τον χÏήστη." +userAdded = "Ο χÏήστης Ï€Ïοστέθηκε στη λίστα κοινής χÏήσης." +usernameLabel = "Όνομα χÏήστη ή email" +usernamePlaceholder = "Εισαγάγετε όνομα χÏήστη ή email" +userRemoveFailed = "Δεν είναι δυνατή η αφαίÏεση Î±Ï…Ï„Î¿Ï Ï„Î¿Ï… χÏήστη." +userRemoved = "Ο χÏήστης αφαιÏέθηκε από τη λίστα κοινής χÏήσης." +viewActivity = "ΠÏοβολή δÏαστηÏιότητας" +viewed = "ΠÏοβλήθηκε" +viewsCount = "ΠÏοβολές: {{count}}" +downloaded = "Λήφθηκε" +bulkDescription = "ΔημιουÏγήστε έναν σÏνδεσμο για να κάνετε κοινή χÏήση όλων των επιλεγμένων αÏχείων με συνδεδεμένους χÏήστες." +bulkTitle = "Κοινή χÏήση επιλεγμένων αÏχείων" +copyLink = "ΑντιγÏαφή συνδέσμου κοινής χÏήσης" +fileCount = "Επιλέχθηκαν {{count}} αÏχεία" +ownerOnly = "Μόνο ο κάτοχος μποÏεί να διαχειÏίζεται την κοινή χÏήση." +selectSingleFile = "Επιλέξτε ένα μόνο αÏχείο για διαχείÏιση της κοινής χÏήσης." + +[storageUpload] +description = "ΜεταφοÏτώνει το Ï„Ïέχον αÏχείο στον αποθηκευτικό χώÏο του διακομιστή για δική σας Ï€Ïόσβαση." +errorTitle = "Αποτυχία μεταφόÏτωσης" +failure = "Η μεταφόÏτωση απέτυχε. Ελέγξτε τα στοιχεία σÏνδεσης και τις Ïυθμίσεις αποθήκευσης." +fileLabel = "ΑÏχείο" +hint = "Οι δημόσιοι σÏνδεσμοι και οι λειτουÏγίες Ï€Ïόσβασης ελέγχονται από τις Ïυθμίσεις του διακομιστή σας." +success = "ΜεταφοÏτώθηκε στον διακομιστή" +title = "ΜεταφόÏτωση στον διακομιστή" +updateButton = "ΕνημέÏωση στον διακομιστή" +uploadButton = "ΜεταφόÏτωση στον διακομιστή" +bulkDescription = "ΜεταφοÏτώνει τα επιλεγμένα αÏχεία στον αποθηκευτικό χώÏο του διακομιστή σας." +bulkTitle = "ΜεταφόÏτωση επιλεγμένων αÏχείων" +fileCount = "Επιλέχθηκαν {{count}} αÏχεία" +more = " +{{count}} ακόμη" + [storage] approximateSize = "ΠÏοσεγγιστικό μέγεθος" fileTooLarge = "Το αÏχείο είναι Ï€Î¿Î»Ï Î¼ÎµÎ³Î¬Î»Î¿. Το μέγιστο μέγεθος ανά αÏχείο είναι" @@ -7153,6 +7801,30 @@ title = "ΠÏοβολή/ΕπεξεÏγασία PDF" [warning] tooltipTitle = "ΠÏοειδοποίηση" +[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 = "ΜεταφόÏτωση εικόνας υπογÏαφής" + [watermark] completed = "Το υδατογÏάφημα Ï€Ïοστέθηκε" desc = "ΠÏοσθέστε υδατογÏαφήματα κειμένου ή εικόνας σε αÏχεία PDF" @@ -7333,6 +8005,7 @@ activeSession = "ΕνεÏγή συνεδÏία" addMembers = "ΠÏοσθήκη μελών" admin = "ΔιαχειÏιστής" confirmDelete = "Είστε βέβαιοι ότι θέλετε να διαγÏάψετε αυτόν τον χÏήστη; Αυτή η ενέÏγεια δεν μποÏεί να αναιÏεθεί." +confirmUnlock = "Είστε βέβαιοι ότι θέλετε να ξεκλειδώσετε αυτόν τον λογαÏιασμό χÏήστη;" deleteUser = "ΔιαγÏαφή χÏήστη" deleteUserError = "Αποτυχία διαγÏαφής χÏήστη" deleteUserSuccess = "Ο χÏήστης διαγÏάφηκε με επιτυχία" @@ -7341,6 +8014,8 @@ disable = "ΑπενεÏγοποίηση" disabled = "ΑπενεÏγοποιημένος" editRole = "ΕπεξεÏγασία Ïόλου" enable = "ΕνεÏγοποίηση" +locked = "κλειδωμένο" +lockedBadge = "Κλειδωμένο" loading = "ΦόÏτωση μελών..." loginRequired = "ΕνεÏγοποιήστε Ï€Ïώτα τη λειτουÏγία σÏνδεσης" member = "Μέλος" @@ -7350,6 +8025,9 @@ searchMembers = "Αναζήτηση μελών..." status = "Κατάσταση" team = "Ομάδα" title = "ΆνθÏωποι" +unlockAccount = "Ξεκλείδωμα λογαÏιασμοÏ" +unlockUserError = "Αποτυχία ξεκλειδώματος λογαÏÎ¹Î±ÏƒÎ¼Î¿Ï Ï‡Ïήστη" +unlockUserSuccess = "Ο λογαÏιασμός χÏήστη ξεκλειδώθηκε με επιτυχία" user = "ΧÏήστης" [workspace.people.actions] diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index dac1133ba0..52275cd0d0 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -44,6 +44,8 @@ downloadPdf = "Download PDF" downloadUnavailable = "Download unavailable for this item" edit = "Edit" editYourNewFiles = "Edit your new file(s)" +encryptedFileBlocked = "File is password-protected. Unlock it first." +encryptedFilesBlocked = "{{count}} files are password-protected. Unlock them first." exportAndContinue = "Export & Continue" false = "False" fileSavedToDisk = "File saved to disk" @@ -2077,6 +2079,23 @@ keywords = "Keywords: odd, even" numbers = "Numbers/ranges: 5, 10-20" progressions = "Progressions: 3n, 4n+1" +[chat.progress] +thinking = "Thinking..." +analyzing = "Analysing your request..." +calling_engine = "AI is thinking..." +extracting_content = "Extracting content from your documents..." +executing_tool_single = "Running {{tool}}..." +executing_tool_step = "Running {{tool}} (step {{step}} of {{total}})..." +executing_tool_generic = "Running tool..." +executing_tool_generic_step = "Running tool (step {{step}} of {{total}})..." +processing = "Processing extracted content..." + +[chat.toolsUsed] +summary_one = "Ran 1 tool" +summary_other = "Ran {{count}} tools" +summary = "Ran {{count}} tools" +unknownTool = "Unknown tool" + [certSign] allSigned = "All participants have signed. Ready to finalize." awaitingSignatures = "Awaiting signatures" @@ -2791,6 +2810,33 @@ confirmTitle = "Re-run comparison?" unlinkedBody = "Tip: Arrow Up/Down scroll both panes; panning only moves the active pane." unlinkedTitle = "Independent scroll & pan enabled" +[compare.mode] +pixel = "Pixel Comparison" +text = "Text Comparison" + +[compare.pixel] +base = "Original" +changed = "changed" +comparison = "Edited" +diff = "Differences" +diffOnly = "Diff only" +missingInBase = "Missing in original" +missingInComparison = "Missing in edited" +overall = "overall" +overlay = "Overlay" +pageLabel = "Page" +pagesChanged = "pages changed" +sideBySide = "Side-by-side" +sizeMismatch = "Size mismatch" +summaryTitle = "Pixel comparison" + +[compare.pixel.warnings] +noPages = "One or both documents have no pages." +pageCountMismatch = "Page count mismatch: original has {{base}} page(s), edited has {{comparison}}. Extra pages are shown one-sided and marked as fully removed/added." + +[compare.pixel.errors] +canvasContextUnavailable = "Unable to acquire 2D canvas context for pixel comparison." + [compare.too.dissimilar] message = "These documents appear highly dissimilar. Comparison was stopped to save time." @@ -3341,6 +3387,9 @@ successBodyWithName = "Password removed from {{fileName}}" successTitle = "Password removed" title = "Remove password to continue" unlock = "Unlock & Continue" +unlockAll = "Use for all ({{count}})" +unlockAllPartialFail = "Wrong password for: {{names}}" +unlockAllSuccess = "Unlocked {{count}} file(s)." unlockPrompt = "Unlock PDF to continue" [encryptedPdfUnlock.password] @@ -4291,6 +4340,8 @@ welcomeTitle = "You've been invited!" [landing] addFiles = "Add Files" +heroSubtitle = "Drop in or add an existing PDF to get started." +heroTitle = "Stirling PDF" mobileUpload = "Upload from Mobile" openFromComputer = "Open from computer" uploadFromComputer = "Upload from computer" @@ -4426,6 +4477,8 @@ title = "Markdown To PDF" submit = "Merge" tags = "merge,Page operations,Back end,server side" title = "Merge" +viewerModeHint = "Merge needs 2 or more files. Head to the file editor to select them." +goToFileEditor = "Go to file editor" [merge.error] failed = "An error occurred while merging the PDFs." @@ -6640,9 +6693,13 @@ defaultPdfEditorActive = "Stirling PDF is your default PDF editor" defaultPdfEditorChecking = "Checking..." defaultPdfEditorInactive = "Another application is set as default" defaultPdfEditorSet = "Already Default" +defaultStartupView = "Default view on launch" +defaultStartupViewDescription = "Choose which tab is active in the left column when the app starts" defaultToolPickerMode = "Default tool picker mode" defaultToolPickerModeDescription = "Choose whether the tool picker opens in fullscreen or sidebar by default" description = "Configure general application preferences." +defaultViewerZoom = "Default reader zoom" +defaultViewerZoomDescription = "Set the default zoom level when opening PDFs in the reader" hideUnavailableConversions = "Hide unavailable conversions" hideUnavailableConversionsDescription = "Remove disabled conversion options in the Convert tool instead of showing them greyed out." hideUnavailableTools = "Hide unavailable tools" @@ -6665,6 +6722,16 @@ title = "For System Administrators" fullscreen = "Fullscreen" sidebar = "Sidebar" +[settings.general.startupView] +automate = "Automate" +read = "Reader" +tools = "Tools" + +[settings.general.zoomLevel] +auto = "Auto" +fitPage = "Fit page" +fitWidth = "Fit width" + [settings.general.updates] checkForUpdates = "Check for Updates" currentBackendVersion = "Current Backend Version" @@ -6821,10 +6888,13 @@ title = "Sign in to Stirling" [setup.selfhosted] link = "or connect to a self-hosted account" subtitle = "Enter your server credentials" +changeServerLocked = "Your organisation has restricted this app to a specific server" switchToLocal = "Use local tools instead" title = "Sign in to Server" [setup.selfhosted.unreachable] +changeServer = "Connect to a different server" +changeServerLocked = "Your organisation has restricted this app to a specific server" continueOffline = "Use local tools instead" message = "Could not reach {{url}}. Check that the server is running and accessible." retry = "Retry" @@ -7524,6 +7594,11 @@ endpointUnavailable = "This tool is unavailable on your server." endpointUnavailableClickable = "Not available in this mode. Click to sign in." invalidParams = "Fill in the required settings." noFiles = "Add a file to get started." +viewerMode = "Switch to the file editor to select multiple files." +singleFileScope = "Only applying to: {{fileName}}" +scopeThisFile = "this file" +scopeFiles = "files" +selectFilesHint = "Select files in Active Files to run this tool" [tools] noSearchResults = "No tools found" diff --git a/frontend/public/locales/es-ES/translation.toml b/frontend/public/locales/es-ES/translation.toml index 6dcdfdb508..dfa4d79d58 100644 --- a/frontend/public/locales/es-ES/translation.toml +++ b/frontend/public/locales/es-ES/translation.toml @@ -8,6 +8,7 @@ black = "Negro" blue = "Azul" bored = "¿Aburrido de esperar?" cancel = "Cancelar" +confirm = "Confirmar" changedCredsMessage = "¡Se cambiaron las credenciales!" chooseFile = "Elegir Archivo" close = "Cerrar" @@ -146,6 +147,7 @@ insufficientCredits = "Créditos insuficientes. Requeridos: {{requiredCredits}}, loadingCredits = "Comprobando créditos..." loadingProStatus = "Comprobando estado de suscripción..." noticeTopUpOrPlan = "No hay suficientes créditos, por favor recarga o actualiza tu plan" +accessInvite = "Invitar" [account] accountSettings = "Configuración de la cuenta" @@ -1427,6 +1429,34 @@ title = "Procesamiento" description = "Tiempo máximo de espera para un trabajo de procesamiento antes de informar un error." label = "Tiempo de espera de procesamiento (segundos)" +[admin.settings.storage] +description = "Controla el almacenamiento del servidor y las opciones de compartir." +title = "Almacenamiento y uso compartido de archivos" + +[admin.settings.storage.enabled] +description = "Permitir a los usuarios almacenar archivos en el servidor." +label = "Habilitar almacenamiento de archivos en el servidor" + +[admin.settings.storage.sharing.email] +description = "Permitir compartir con direcciones de correo electrónico." +label = "Habilitar compartir por correo electrónico" +mailLink = "Configurar ajustes de correo" +mailNote = "Requiere configuración de correo. " + +[admin.settings.storage.sharing.enabled] +description = "Permitir a los usuarios compartir archivos almacenados." +label = "Habilitar compartir" + +[admin.settings.storage.sharing.links] +description = "Permitir compartir mediante enlaces con inicio de sesión." +frontendUrlLink = "Configurar en Ajustes del sistema" +frontendUrlNote = "Requiere una URL de Frontend. " +label = "Habilitar enlaces para compartir" + +[admin.settings.storage.signing.enabled] +description = "Permitir a los usuarios crear sesiones de firma de documentos con varios participantes. Requiere habilitar el almacenamiento de archivos en el servidor." +label = "Habilitar firma en grupo (Alpha)" + [admin.settings.unsavedChanges] cancel = "Seguir editando" discard = "Descartar cambios" @@ -2059,7 +2089,19 @@ numbers = "Números/rangos: 5, 10-20" progressions = "Progresiones: 3n, 4n+1" [certSign] +allSigned = "Todos los participantes han firmado. Listo para finalizar." +awaitingSignatures = "A la espera de firmas" +signatureProgress = "{{signedCount}}/{{totalCount}} firmas" chooseCertificate = "Elegir archivo de certificado" +declined = "Rechazado" +fetchFailed = "No se pudieron cargar los datos de firma" +finalized = "Finalizado" +notified = "Notificado" +partialNote = "Puedes finalizar antes con las firmas actuales. Los participantes sin firmar serán excluidos." +pending = "Pendiente" +readyToFinalize = "Listo para finalizar" +signed = "Firmado" +viewed = "Visto" chooseJksFile = "Elegir archivo JKS" chooseP12File = "Elegir archivo PKCS12" choosePfxFile = "Elegir archivo PFX" @@ -2082,6 +2124,7 @@ title = "Firma con certificado" invisible = "Invisible" stepTitle = "Apariencia de firma" visible = "Visible" +visibility = "Visibilidad" [certSign.appearance.options] title = "Detalles de la firma" @@ -2188,6 +2231,252 @@ bullet4 = "Puede usar certificados personalizados para verificación" text = "Cuando verifica firmas, la herramienta le indica si son válidas, quién firmó el documento, cuándo se firmó y si el documento ha sido cambiado desde la firma." title = "Verificar firmas" +[certSign.collab.finalize] +button = "Finalizar y cargar el PDF firmado" +early = "Finalizar con las firmas actuales" + +[certSign.collab.sessionDetail] +addButton = "Añadir participantes" +addParticipants = "Añadir participantes" +addParticipantsError = "No se pudieron añadir los participantes" +backToList = "Volver a sesiones" +deleteConfirm = "¿Estás seguro? Esto no se puede deshacer." +deleteError = "No se pudo eliminar la sesión" +deleted = "Sesión eliminada" +deleteSession = "Eliminar sesión" +dueDate = "Fecha límite" +finalizeError = "No se pudo finalizar la sesión" +loadPdfError = "No se pudo cargar el PDF firmado" +loadSignedPdf = "Cargar PDF firmado en archivos activos" +messageLabel = "Mensaje" +noAdditionalInfo = "Sin información adicional" +owner = "Propietario" +participantRemoved = "Participante eliminado" +participants = "Participantes" +participantsAdded = "Participantes añadidos correctamente" +removeParticipant = "Eliminar" +removeParticipantError = "No se pudo eliminar al participante" +selectUsers = "Selecciona usuarios..." +sessionInfo = "Información de la sesión" +workbenchTitle = "Gestión de sesión" + +[certSign.collab.signRequest] +addedToFiles = "Documento añadido a archivos activos" +addSignature = "Añade tu firma" +addToFiles = "Añadir a archivos activos" +advancedSettings = "Configuración avanzada" +backToList = "Volver a solicitudes de firma" +certificateChoice = "Selecciona un certificado para firmar" +changeSignature = "Cambiar firma" +clearSignature = "Borrar firma" +completeAndSign = "Completar y firmar" +createNewSignature = "Crear nueva firma" +declineButton = "Rechazar" +decline = "Rechazar solicitud" +deleteSelected = "Eliminar firma seleccionada" +drawSignature = "Dibuja tu firma abajo" +dueDate = "Fecha límite" +fileTooLarge = "El tamaño del archivo debe ser menor de 5MB" +fontFamily = "Familia tipográfica" +fontSize = "Tamaño de fuente: {{size}}px" +fontSizePlaceholder = "Tamaño" +from = "De" +invalidCertFile = "Selecciona un archivo de certificado P12 o PFX" +invalidFileType = "Selecciona un archivo de imagen" +location = "Ubicación (opcional)" +locationPlaceholder = "¿Desde dónde firmas?" +message = "Mensaje" +noCertificate = "Selecciona un archivo de certificado" +noSignatures = "Coloca al menos una firma en el PDF" +p12File = "Archivo de certificado P12/PFX" +password = "Contraseña del certificado" +passwordPlaceholder = "Introduce la contraseña..." +penColor = "Color del trazo" +penSize = "Grosor del trazo: {{size}}px" +placementActive = "Haz clic en el PDF para colocar" +placeSignatureButton = "Colocar firma en el PDF" +reason = "Motivo (opcional)" +reasonPlaceholder = "¿Por qué firmas?" +removeImage = "Eliminar imagen" +removeCertFile = "Eliminar archivo" +savedSignatures = "Firmas guardadas" +selectFile = "Seleccionar archivo de imagen" +selectSignatureTitle = "Seleccionar o crear firma" +signButton = "Firmar documento" +signatureInfo = "Estos ajustes los configura el propietario del documento" +signaturePlaced = "Firma colocada en la página" +signatureSettings = "Configuración de la firma" +signatureText = "Texto de la firma" +signatureTextPlaceholder = "Escribe tu nombre..." +signatureTypeLabel = "Tipo de firma" +signingTitle = "Firma" +textColor = "Color del texto" +typeSignature = "Escribe tu nombre para crear una firma" +uploadCert = "Certificado personalizado" +uploadCertDesc = "Usa tu propio certificado P12/PFX" +uploadSignature = "Sube la imagen de tu firma" +usePersonalCert = "Certificado personal" +usePersonalCertDesc = "Generado automáticamente para tu cuenta" +useServerCert = "Certificado de la organización" +useServerCertDesc = "Certificado compartido de la organización" +workbenchTitle = "Solicitud de firma" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Elige el color del trazo" +continue = "Continuar" + +[certSign.collab.signRequest.certModal] +description = "Has colocado {{count}} firma(s). Elige tu certificado para completar la firma." +sign = "Firmar documento" +certValidating = "Validando certificado..." +certValidUntil = "Certificado válido hasta {{date}}" +certInvalid = "Certificado no válido: {{error}}" +certInvalidFallback = "Certificado no válido" +certNetworkError = "No se pudo validar el certificado" +title = "Configurar certificado" + +[certSign.collab.signRequest.image] +hint = "Sube una imagen PNG o JPG de tu firma" + +[certSign.collab.signRequest.mode] +move = "Mover firma" +place = "Colocar firma" +title = "Modo de firmar o mover" + +[certSign.collab.signRequest.modeTabs] +draw = "Dibujar" +image = "Subir" +text = "Escribir" + +[certSign.collab.signRequest.placeSignature] +message = "Haz clic en el PDF para colocar tu firma" +title = "Colocar firma" + +[certSign.collab.signRequest.preview] +imageAlt = "Firma seleccionada" +missing = "Sin vista previa" +textFallback = "Firma" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Firma dibujada" +defaultImageLabel = "Firma cargada" +defaultLabel = "Firma" +defaultTextLabel = "Firma escrita" +delete = "Eliminar firma" +none = "No hay firmas guardadas" + +[certSign.collab.signRequest.signatureType] +draw = "Dibujar" +type = "Escribir" +upload = "Subir" + +[certSign.collab.signRequest.steps] +back = "Atrás" +cancelPlacement = "Cancelar colocación" +certificate = "Certificado" +clickMultipleTimes = "Haz clic en el PDF varias veces para colocar firmas. Arrastra cualquier firma para moverla o redimensionarla." +clickToPlace = "Haz clic en el PDF donde quieres que aparezca tu firma." +continue = "Continuar con la selección de certificado" +continueToPlacement = "Continuar con la colocación" +continueToReview = "Continuar con la revisión" +createSignature = "Crear firma" +invisible = "Invisible" +location = "Ubicación:" +multipleSignatures = "Se aplicarán {{count}} firmas al PDF" +oneSignature = "Se aplicará 1 firma al PDF" +placeOnPdf = "Colocar en el PDF" +reason = "Motivo:" +reviewTitle = "Revisar antes de firmar" +signaturePlaced = "Firma colocada en la página {{page}}. Puedes ajustar la posición haciendo clic de nuevo o continuar para revisar." +visible = "Visible" +visibility = "Visibilidad:" +yourSignatures = "Tus firmas ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Color" +fontLabel = "Fuente" +fontSizeLabel = "Tamaño" +fontSizePlaceholder = "16" +label = "Texto de la firma" +modalHint = "Escribe tu nombre y luego haz clic en Continuar para colocarlo en el PDF." +placeholder = "Escribe tu nombre..." + +[certSign.collab.participant] +certValidating = "Validando certificado..." +certValid = "✓ Certificado válido" +certValidUntil = " hasta {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Certificado no válido" +certNetworkError = "No se pudo validar el certificado" + +[certSign.collab.addParticipants] +add = "Añadir {{count}} participante(s)" +back = "Atrás" +configureSignatures = "Configurar la firma" +continue = "Continuar con la configuración de la firma" +reasonHelp = "Preestablece un motivo de firma para estos participantes (opcional, pueden cambiarlo al firmar)" +reasonPlaceholder = "p. ej., Aprobación, Revisión..." +selectUsers = "Seleccionar usuarios" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Incluir página de resumen de firmas" +includeSummaryPageHelp = "Se añadirá al final una página de resumen con todos los metadatos de firmas. Se suprimirán los cuadros de firma del certificado digital en las páginas individuales (las firmas manuscritas no se ven afectadas)." + +[certSign.collab.sessionList] +active = "Activa" +finalized = "Finalizada" + +[certSign.collab.signatureSettings] +description = "Configura cómo aparecerán las firmas para todos los participantes" +title = "Apariencia de la firma" + +[certSign.collab.userSelector] +inviteUsers = "Añadir usuarios" +loadError = "No se pudieron cargar los usuarios" +noTeam = "Sin equipo" +noUsers = "No se encontraron otros usuarios." +placeholder = "Selecciona usuarios..." + +[certSign.mobile] +panelActions = "Acciones" +panelDocument = "Documento" +panelPeople = "Personas" + +[certSign.sessions] +deleted = "Sesión eliminada" +fetchFailed = "No se pudieron cargar los detalles de la sesión" +finalized = "Sesión finalizada" +loaded = "PDF firmado cargado" +pdfNotReady = "El PDF no está listo" +pdfNotReadyDesc = "Se está generando el PDF firmado. Inténtalo de nuevo en un momento." + +[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" + [changeCreds] changePassword = "Está usando las credenciales de inicio de sesión por defecto. Por favor, introduzca una contraseña nueva" changeUsername = "Actualiza tu nombre de usuario. Se cerrará tu sesión tras actualizar." @@ -3242,6 +3531,46 @@ totalSelected = "Total Seleccionados" unsupported = "No Soportado" unzip = "Descomprimir" uploadError = "Error al cargar algunos archivos." +copyCreated = "Copia guardada en este dispositivo." +copyFailed = "No se pudo crear una copia." +leaveShare = "Quitar de mi lista" +leaveShareFailed = "No se pudo quitar el archivo compartido." +leaveShareSuccess = "Eliminado de tu lista de compartidos." +removeBoth = "Eliminar de ambos" +removeFilePrompt = "Este archivo está guardado en este dispositivo y en tu servidor. ¿Dónde quieres eliminarlo?" +removeFileTitle = "Eliminar archivo" +removeLocalOnly = "Solo este dispositivo" +removeServerFailed = "No se pudo eliminar el archivo del servidor." +removeServerOnly = "Solo servidor" +removeServerOnlyPrompt = "Este archivo está almacenado solo en tu servidor. ¿Quieres eliminarlo del servidor?" +removeServerSuccess = "Eliminado del servidor." +removeSharedPrompt = "Este archivo está compartido contigo. Puedes eliminarlo de este dispositivo o de tu lista de compartidos." +removeSharedServerOnlyBlockedPrompt = "Este archivo está compartido contigo y se almacena solo en el servidor." +removeSharedServerOnlyPrompt = "Este archivo está compartido contigo y se almacena solo en el servidor. ¿Eliminarlo de tu lista?" +changesNotUploaded = "Cambios no subidos" +cloudFile = "Archivo en la nube" +filterAll = "Todos" +filterLocal = "Local" +filterSharedByMe = "Compartidos por mí" +filterSharedWithMe = "Compartidos conmigo" +lastSynced = "Última sincronización" +localOnly = "Solo local" +makeCopy = "Hacer una copia" +owner = "Propietario" +ownerUnknown = "Desconocido" +share = "Compartir" +shareSelected = "Compartir seleccionados" +sharedByYou = "Compartidos por ti" +sharedEditNoticeBody = "No tienes derechos de edición sobre la versión en el servidor de este archivo. Cualquier edición que hagas se guardará como una copia local." +sharedEditNoticeConfirm = "Entendido" +sharedEditNoticeTitle = "Copia del servidor de solo lectura" +sharedWithYou = "Compartidos contigo" +sharing = "Uso compartido" +storageState = "Almacenamiento" +synced = "Sincronizado" +updateOnServer = "Actualizar en el servidor" +uploadSelected = "Subir seleccionados" +uploadToServer = "Subir al servidor" [files] addFiles = "Agregar archivos" @@ -3367,6 +3696,77 @@ title = "Acerca de Aplanar PDFs" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Acerca de la firma en grupo" + +[groupSigning.tooltip.finalization] +bullet1 = "Todas las firmas se aplican en el orden de participantes que especificaste" +bullet2 = "Puedes finalizar con firmas parciales si es necesario" +bullet3 = "Una vez finalizada, la sesión no se puede modificar" +description = "Cuando todos los participantes hayan firmado (o decidas finalizar antes), podrás generar el PDF final firmado." +title = "Proceso de finalización" + +[groupSigning.tooltip.roles] +bullet1 = "Propietario (tú): Crea la sesión, configura los valores predeterminados de firma, finaliza el documento" +bullet2 = "Participantes: Crean su firma, eligen el certificado, la colocan en el PDF" +bullet3 = "Los participantes no pueden modificar la visibilidad, el motivo ni la ubicación de la firma" +description = "Controlas los ajustes de apariencia de la firma para todos los participantes." +title = "Roles de los participantes" + +[groupSigning.tooltip.sequential] +bullet1 = "El primer participante debe firmar antes de que el segundo pueda acceder al documento" +bullet2 = "Garantiza el orden de firma adecuado para el cumplimiento legal" +bullet3 = "Puedes reordenar a los participantes arrastrándolos en la lista" +description = "Los participantes firman los documentos en el orden que especifiques. Cada firmante recibe una notificación cuando es su turno." +title = "Firma secuencial" + +[groupSigning.steps] +back = "Atrás" +completed = "Completado" +current = "Actual" +stepLabel = "Paso {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Continuar con la revisión" +invisible = "Las firmas serán invisibles (solo metadatos)" +locationLabel = "Ubicación:" +preview = "Vista previa" +reasonLabel = "Motivo:" +title = "Configurar ajustes de firma" +visible = "Las firmas serán visibles en la página {{page}}" + +[groupSigning.steps.review] +document = "Documento" +dueDate = "Fecha límite (opcional)" +dueDatePlaceholder = "Selecciona la fecha límite..." +invisible = "Invisible (solo metadatos)" +location = "Ubicación:" +logo = "Logotipo:" +logoHidden = "Sin logotipo" +logoShown = "Se muestra el logotipo de Stirling PDF" +participants = "Participantes" +reason = "Motivo:" +send = "Enviar solicitudes de firma" +signatureSettings = "Configuración de la firma" +title = "Revisar detalles de la sesión" +titleShort = "Revisar y enviar" +visibility = "Visibilidad:" +visible = "Visible en la página {{page}}" +participantCount = "{{count}} participante(s) firmará(n) en orden" + +[groupSigning.steps.selectDocument] +continue = "Continuar con la selección de participantes" +noFile = "Selecciona un único archivo PDF de tus archivos activos para crear una sesión de firma." +selectedFile = "Documento seleccionado" +title = "Seleccionar documento" + +[groupSigning.steps.selectParticipants] +continue = "Continuar con la configuración de la firma" +count = "{{count}} participante(s) seleccionado(s)" +label = "Seleccionar participantes" +placeholder = "Elige participantes para firmar..." +title = "Elegir participantes" + [getPdfInfo] downloadJson = "Descargar JSON" downloads = "Descargas" @@ -4460,7 +4860,10 @@ zoomOut = "Alejar" [viewer] cannotPreviewFile = "No se puede previsualizar el archivo" +disableColorFilter = "Desactivar filtro de color" dualPageView = "Vista de Página Doble" +enableDarkFilter = "Activar filtro oscuro" +enableSepiaFilter = "Activar filtro sepia" firstPage = "Primera Página" lastPage = "Última Página" nextPage = "Página Siguiente" @@ -4470,6 +4873,22 @@ singlePageView = "Vista de Página Única" unknownFile = "Archivo desconocido" zoomIn = "Acercar" zoomOut = "Alejar" +resetZoom = "Restablecer zoom" + +[viewer.nonPdf] +fileTypeBadge = "Archivo {{type}}" +convertToPdf = "Convertir a PDF" +loading = "Cargando..." +emptyFile = "Archivo vacío" +csvStats = "{{rows}} filas · {{columns}} columnas · {{size}}" +sortedBy = "Ordenado por: {{column}}" +columnDefault = "Columna {{index}}" +htmlPreviewWarning = "Vista previa de HTML — los recursos externos pueden no cargarse · {{size}}" +htmlPreview = "Vista previa de HTML" +invalidJson = "JSON no válido — mostrando contenido sin procesar" +textStats = "{{lines}} líneas · {{size}}" +lineNumbers = "Números de línea" +renderMarkdown = "Renderizar markdown" [viewer.attachments] title = "Adjuntos" @@ -4531,6 +4950,7 @@ toggleAttachments = "Mostrar/ocultar adjuntos" toggleTheme = "Alternar Tema" language = "Idioma" toggleAnnotations = "Mostrar/ocultar anotaciones" +toggleLayers = "Alternar capas" search = "Buscar en PDF" panMode = "Modo de Desplazamiento" applyRedactionsFirst = "Aplica primero las censuras" @@ -5407,20 +5827,72 @@ title = "Imprimir archivo" 2 = "Introducir nombre de la impresora" [quickAccess] +access = "Acceso" +accessAddPerson = "Añadir otra persona" +accessBack = "Atrás" +accessCopyLink = "Copiar enlace" +accessEmail = "Dirección de correo electrónico" +accessEmailPlaceholder = "nombre@empresa.com" +accessFileLabel = "Archivo" +accessGeneral = "Acceso general" +accessInviteTitle = "Invitar personas" +accessOwner = "Propietario" +accessPanel = "Acceso al documento" +accessPeople = "Personas con acceso" +accessRemove = "Eliminar" +accessRestricted = "Restringido" +accessRestrictedHint = "Solo las personas con acceso pueden abrir" +accessRole = "Rol" +accessRoleCommenter = "Comentarista" +accessRoleEditor = "Editor" +accessRoleViewer = "Lector" +accessSelectedFile = "Archivo seleccionado" +accessSendInvite = "Enviar invitación" +accessTitle = "Acceso al documento" +accessYou = "Tú" account = "Cuenta" +activeSessions = "Sesiones activas" +activeTab = "Activas" activity = "Registro" adminSettings = "Ajustes admin" +allSessions = "Todas las sesiones" allTools = "Herram." automate = "Automatizar" +back = "Atrás" +certSign = "Firma con certificado" +completedSessions = "Sesiones completadas" +completedTab = "Completadas" config = "Conf." +createNew = "Crear nueva solicitud" +createSession = "Crear solicitud de firma" +dueDate = "Fecha límite (opcional)" files = "Archivos" help = "Ayuda" +noActiveSessions = "No hay solicitudes de firma pendientes ni sesiones activas" +noCompletedSessions = "No hay sesiones completadas" +noFile = "Ningún archivo seleccionado" read = "Leer" reader = "Lector" +refresh = "Actualizar" +requestSignatures = "Solicitar firmas" +selectSingleFileToRequest = "Selecciona un único archivo PDF para solicitar firmas" +selectedFile = "Archivo seleccionado" +selectUsers = "Selecciona usuarios para firmar" +selectUsersPlaceholder = "Elige participantes..." +sendingRequest = "Enviando..." settings = "Ajustes" showMeAround = "Muéstrame cómo funciona" sign = "Firmar" +signatureRequests = "Solicitudes de firma" +signYourself = "Firmar tú mismo" +newRequest = "Nueva solicitud" tours = "Recorridos" +wetSign = "Añadir firma" +filterMine = "Mías" +filterOverdue = "Vencidas" +filterSigned = "Firmadas" +filterDeclined = "Rechazadas" +searchDocuments = "Buscar documentos…" [quickAccess.helpMenu] adminTour = "Recorrido de administración" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Tu servidor Stirling-PDF está sin conexión y \"{{en expired = "Su sesión ha caducado. Actualice la página e inténtelo de nuevo." refreshPage = "Refrescar Página" +[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" + +[signatureSettings.tooltip] +header = "Ajustes de apariencia de la firma" + +[signatureSettings.tooltip.location] +bullet1 = "Ejemplos: \"Nueva York, EE. UU.\", \"Oficina de Londres\", \"Remoto\"" +bullet2 = "No es lo mismo que la posición en la página" +bullet3 = "Puede ser obligatorio en ciertas jurisdicciones legales" +description = "Ubicación geográfica opcional donde se aplicó la firma. Se almacena en los metadatos del certificado." +title = "Ubicación de la firma" + +[signatureSettings.tooltip.logo] +bullet1 = "Se muestra junto a la firma y el texto" +bullet2 = "Compatible con formatos PNG, JPG" +bullet3 = "Mejora la apariencia profesional" +description = "Añade un logotipo de empresa a las firmas visibles para la marca y la autenticidad." +title = "Logotipo de la empresa" + +[signatureSettings.tooltip.reason] +bullet1 = "Ejemplos: \"Aprobación\", \"Acuerdo de contrato\", \"Revisión completa\"" +bullet2 = "Visible en las propiedades de firma del PDF" +bullet3 = "Útil para auditorías y cumplimiento" +description = "Texto opcional que explica por qué se firma el documento. Se almacena en los metadatos del certificado." +title = "Motivo de la firma" + +[signatureSettings.tooltip.visibility] +bullet1 = "Visible: La firma aparece en el PDF con apariencia personalizada" +bullet2 = "Invisible: Certificado incrustado sin marca visual" +bullet3 = "Las firmas invisibles siguen proporcionando validación criptográfica" +description = "Controla si la firma es visible en el documento o se incrusta de forma invisible." +title = "Visibilidad de la firma" + [settings.configuration] advanced = "Avanzado" database = "Base de datos" endpoints = "Endpoints" features = "Funciones" +storageSharing = "Almacenamiento y uso compartido de archivos" systemSettings = "Ajustes del sistema" title = "Configuración" @@ -6332,10 +6868,13 @@ title = "Inicie sesión en Stirling" [setup.selfhosted] link = "o conectarse a una cuenta autoalojada" subtitle = "Introduzca las credenciales de su servidor" +changeServerLocked = "Tu organización ha restringido esta aplicación a un servidor específico" switchToLocal = "Usar herramientas locales en su lugar" title = "Inicie sesión en el servidor" [setup.selfhosted.unreachable] +changeServer = "Conectar a un servidor diferente" +changeServerLocked = "Tu organización ha restringido esta aplicación a un servidor específico" continueOffline = "Usar herramientas locales en su lugar" message = "No se pudo acceder a {{url}}. Comprueba que el servidor esté en ejecución y sea accesible." retry = "Reintentar" @@ -6529,6 +7068,15 @@ saved = "Guardadas" text = "Texto" title = "Tipo de Firma" +[signRequest] +declined = "Solicitud de firma rechazada" +fetchFailed = "No se pudo cargar la solicitud de firma" +signed = "Documento firmado correctamente" + +[signSession] +createFailed = "No se pudo crear la solicitud de firma" +created = "Solicitud de firma enviada" + [signup] accountCreatedSuccessfully = "¡Cuenta creada con éxito! Ahora puede iniciar sesión." alreadyHaveAccount = "¿Ya tiene una cuenta? Iniciar sesión" @@ -6807,6 +7355,106 @@ title = "Dividir PDF por Capítulos" [splitPdfByChapters] tags = "dividir,capítulos,marcadores,organizar" +[storageShare] +accessed = "Accedido" +accessDenied = "No tienes acceso a este archivo compartido. Pide al propietario que lo comparta contigo." +accessFailed = "No se pudo cargar la actividad." +accessDeniedBody = "No tienes acceso a este archivo. Pide al propietario que lo comparta contigo." +accessDeniedTitle = "Sin acceso" +accessLimitedCommenter = "El acceso de comentarios llegará pronto. Pide acceso de editor si necesitas descargar." +accessLimitedTitle = "Acceso limitado" +accessLimitedViewer = "Este enlace es solo de visualización. Pide acceso de editor si necesitas descargar." +createdAt = "Creado" +download = "Descargar" +downloadFailed = "No se puede descargar este archivo." +expiredBody = "Este enlace para compartir no es válido o ha caducado." +expiredTitle = "Enlace caducado" +goToLogin = "Ir al inicio de sesión" +loadFailed = "No se puede abrir el archivo compartido." +loading = "Cargando enlace para compartir..." +loginPrompt = "Inicia sesión para acceder a este archivo compartido." +loginRequired = "Se requiere inicio de sesión" +openInApp = "Abrir en Stirling PDF" +ownerLabel = "Propietario" +ownerUnknown = "Desconocido" +requiresLogin = "Este archivo compartido requiere inicio de sesión." +roleCommenter = "Comentarista" +roleEditor = "Editor" +roleViewer = "Lector" +shareHeading = "Archivo compartido" +titleDefault = "Archivo compartido" +tryAgain = "Vuelve a intentarlo más tarde." +addUser = "Añadir" +commenterHint = "La función de comentarios llegará pronto." +copied = "Enlace copiado al portapapeles" +copy = "Copiar" +copyFailed = "Error al copiar" +description = "Crea un enlace para compartir este archivo. Los usuarios con sesión iniciada que tengan el enlace podrán acceder." +downloadsCount = "Descargas: {{count}}" +emailWarningBody = "Parece una dirección de correo electrónico. Si esta persona no es ya usuaria de Stirling PDF, no podrá acceder al archivo." +emailWarningConfirm = "Compartir de todos modos" +emailWarningTitle = "Dirección de correo electrónico" +errorTitle = "Error al compartir" +failure = "No se puede generar un enlace para compartir. Inténtalo de nuevo." +fileLabel = "Archivo" +generate = "Generar enlace" +generated = "Enlace para compartir generado" +hideActivity = "Ocultar actividad" +invalidUsername = "Introduce un nombre de usuario o correo electrónico válido." +lastAccessed = "Último acceso" +linkAccessTitle = "Acceso mediante enlace para compartir" +linkLabel = "Enlace para compartir" +linksDisabled = "Los enlaces para compartir están desactivados." +linksDisabledBody = "Los enlaces para compartir están desactivados por la configuración de tu servidor." +manage = "Gestionar uso compartido" +manageDescription = "Crea y gestiona enlaces para compartir este archivo." +manageLoadFailed = "No se pueden cargar los enlaces para compartir." +manageTitle = "Gestionar uso compartido" +noActivity = "Aún no hay actividad." +noLinks = "Aún no hay enlaces activos para compartir." +noSharedUsers = "Aún no hay usuarios con acceso." +removeLink = "Eliminar enlace" +removeUser = "Eliminar" +revokeFailed = "No se puede eliminar el enlace para compartir." +revoked = "Enlace para compartir eliminado" +roleLabel = "Rol" +sharingDisabled = "El uso compartido está desactivado." +sharingDisabledBody = "El uso compartido se ha desactivado por la configuración del servidor." +sharedUsersTitle = "Usuarios con acceso compartido" +title = "Compartir archivo" +unknownUser = "Usuario desconocido" +userAddFailed = "No se puede compartir con ese usuario." +userAdded = "Usuario añadido a la lista de compartidos." +usernameLabel = "Nombre de usuario o correo electrónico" +usernamePlaceholder = "Introduce un nombre de usuario o correo electrónico" +userRemoveFailed = "No se puede eliminar a ese usuario." +userRemoved = "Usuario eliminado de la lista de compartidos." +viewActivity = "Ver actividad" +viewed = "Visto" +viewsCount = "Vistas: {{count}}" +downloaded = "Descargado" +bulkDescription = "Crea un único enlace para compartir todos los archivos seleccionados con usuarios que hayan iniciado sesión." +bulkTitle = "Compartir archivos seleccionados" +copyLink = "Copiar enlace para compartir" +fileCount = "{{count}} archivos seleccionados" +ownerOnly = "Solo el propietario puede gestionar el uso compartido." +selectSingleFile = "Selecciona un solo archivo para gestionar el uso compartido." + +[storageUpload] +description = "Esto sube el archivo actual al almacenamiento del servidor para tu propio acceso." +errorTitle = "Error al subir" +failure = "La subida ha fallado. Comprueba tu inicio de sesión y la configuración de almacenamiento." +fileLabel = "Archivo" +hint = "Los enlaces públicos y los modos de acceso están controlados por la configuración del servidor." +success = "Subido al servidor" +title = "Subir al servidor" +updateButton = "Actualizar en el servidor" +uploadButton = "Subir al servidor" +bulkDescription = "Esto sube los archivos seleccionados al almacenamiento del servidor." +bulkTitle = "Subir archivos seleccionados" +fileCount = "{{count}} archivos seleccionados" +more = " +{{count}} más" + [storage] approximateSize = "Tamaño aproximado" fileTooLarge = "Archivo demasiado grande. El tamaño máximo por archivo es" @@ -7153,6 +7801,30 @@ title = "Ver/Editar PDF" [warning] tooltipTitle = "Advertencia" +[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" + [watermark] completed = "Marca de agua añadida" desc = "Añadir marcas de agua de texto o imagen a archivos PDF" @@ -7333,6 +8005,7 @@ activeSession = "Sesión activa" addMembers = "Añadir miembros" admin = "Administrador" confirmDelete = "¿Seguro que quieres eliminar a este usuario? Esta acción no se puede deshacer." +confirmUnlock = "¿Seguro que quieres desbloquear esta cuenta de usuario?" deleteUser = "Eliminar usuario" deleteUserError = "No se pudo eliminar el usuario" deleteUserSuccess = "Usuario eliminado correctamente" @@ -7341,6 +8014,8 @@ disable = "Deshabilitar" disabled = "Deshabilitado" editRole = "Editar rol" enable = "Habilitar" +locked = "bloqueado" +lockedBadge = "Bloqueado" loading = "Cargando personas..." loginRequired = "Habilite primero el modo de inicio de sesión" member = "Miembro" @@ -7350,6 +8025,9 @@ searchMembers = "Buscar miembros..." status = "Estado" team = "Equipo" title = "Personas" +unlockAccount = "Desbloquear cuenta" +unlockUserError = "Error al desbloquear la cuenta de usuario" +unlockUserSuccess = "La cuenta de usuario se ha desbloqueado correctamente" user = "Usuario" [workspace.people.actions] diff --git a/frontend/public/locales/eu-ES/translation.toml b/frontend/public/locales/eu-ES/translation.toml index a778d7ac2d..c3a1987c00 100644 --- a/frontend/public/locales/eu-ES/translation.toml +++ b/frontend/public/locales/eu-ES/translation.toml @@ -8,6 +8,7 @@ black = "Beltza" blue = "Urdina" bored = "Itxaroten aspertuta?" cancel = "Utzi" +confirm = "Berretsi" changedCredsMessage = "Kredentzialak aldatu dira!" chooseFile = "Aukeratu fitxategia" close = "Itxi" @@ -146,6 +147,7 @@ insufficientCredits = "Kreditu nahikorik ez. Beharrezkoa: {{requiredCredits}}, E loadingCredits = "Kredituak egiaztatzen..." loadingProStatus = "Harpidetza-egoera egiaztatzen..." noticeTopUpOrPlan = "Kreditu nahikorik ez, mesedez kargatu edo eguneratu plan batera" +accessInvite = "Gonbidatu" [account] accountSettings = "Kontuaren ezarpenak" @@ -1427,6 +1429,34 @@ title = "Prozesatzea" description = "Akatsa jakinarazi aurretik prozesatze-lan baten zain egoteko gehieneko denbora." label = "Prozesatzearen denbora-muga (segundoak)" +[admin.settings.storage] +description = "Kontrolatu zerbitzariaren biltegiratzea eta partekatze aukerak." +title = "Fitxategi biltegiratzea eta partekatzea" + +[admin.settings.storage.enabled] +description = "Baimendu erabiltzaileei fitxategiak zerbitzarian gordetzea." +label = "Gaitu zerbitzariaren fitxategi-biltegiratzea" + +[admin.settings.storage.sharing.email] +description = "Baimendu partekatzea helbide elektronikoekin." +label = "Gaitu posta bidezko partekatzea" +mailLink = "Konfiguratu posta ezarpenak" +mailNote = "Posta konfigurazioa behar da. " + +[admin.settings.storage.sharing.enabled] +description = "Baimendu erabiltzaileei gordetako fitxategiak partekatzea." +label = "Gaitu partekatzea" + +[admin.settings.storage.sharing.links] +description = "Baimendu saioa eskatzen duten esteken bidez partekatzea." +frontendUrlLink = "Konfiguratu sistema ezarpenetan" +frontendUrlNote = "Frontend URL behar da. " +label = "Gaitu partekatzeko estekak" + +[admin.settings.storage.signing.enabled] +description = "Baimendu erabiltzaileei parte-hartzaile anitzeko dokumentu-sinatze saioak sortzea. Zerbitzariaren fitxategi-biltegiratzea gaituta egotea behar da." +label = "Gaitu taldeko sinadura (Alpha)" + [admin.settings.unsavedChanges] cancel = "Editatzen jarraitu" discard = "Aldaketak baztertu" @@ -2059,7 +2089,19 @@ numbers = "Zenbakiak/barrutiak: 5, 10-20" progressions = "Progresioak: 3n, 4n+1" [certSign] +allSigned = "Parte-hartzaile guztiek sinatu dute. Amaitzeko prest." +awaitingSignatures = "Sinaduren zain" +signatureProgress = "{{signedCount}}/{{totalCount}} sinadura" chooseCertificate = "Aukeratu ziurtagiri-fitxategia" +declined = "Ukatua" +fetchFailed = "Ezin izan da sinadura-datuak kargatu" +finalized = "Amaituta" +notified = "Jakinarazita" +partialNote = "Goiz amai dezakezu oraingo sinadurekin. Sinatu gabeko parte-hartzaileak baztertu egingo dira." +pending = "Zain" +readyToFinalize = "Amaitzeko prest" +signed = "Sinatuta" +viewed = "Ikusita" chooseJksFile = "Aukeratu JKS fitxategia" chooseP12File = "Aukeratu PKCS12 fitxategia" choosePfxFile = "Aukeratu PFX fitxategia" @@ -2082,6 +2124,7 @@ title = "Ziurtagiriaren sinadura" invisible = "Ikusezina" stepTitle = "Sinaduraren itxura" visible = "Ikusgai" +visibility = "Ikusgaitasuna" [certSign.appearance.options] title = "Sinaduraren xehetasunak" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Amaitu eta kargatu sinatutako PDFa" +early = "Amaitu uneko sinadurekin" + +[certSign.collab.sessionDetail] +addButton = "Gehitu parte-hartzaileak" +addParticipants = "Gehitu parte-hartzaileak" +addParticipantsError = "Ezin izan da parte-hartzaileak gehitu" +backToList = "Itzuli saioetara" +deleteConfirm = "Ziur zaude? Hau ezin da desegin." +deleteError = "Ezin izan da saioa ezabatu" +deleted = "Saioa ezabatuta" +deleteSession = "Ezabatu saioa" +dueDate = "Epemuga" +finalizeError = "Ezin izan da saioa amaitu" +loadPdfError = "Ezin izan da sinatutako PDFa kargatu" +loadSignedPdf = "Kargatu sinatutako PDFa fitxategi aktiboetan" +messageLabel = "Mezua" +noAdditionalInfo = "Ez dago informazio osagarririk" +owner = "Jabea" +participantRemoved = "Parte-hartzailea kendu da" +participants = "Parte-hartzaileak" +participantsAdded = "Parte-hartzaileak ongi gehitu dira" +removeParticipant = "Kendu" +removeParticipantError = "Ezin izan da parte-hartzailea kendu" +selectUsers = "Hautatu erabiltzaileak..." +sessionInfo = "Saioaren informazioa" +workbenchTitle = "Saio-kudeaketa" + +[certSign.collab.signRequest] +addedToFiles = "Dokumentua fitxategi aktiboetara gehitu da" +addSignature = "Gehitu zure sinadura" +addToFiles = "Gehitu fitxategi aktiboetara" +advancedSettings = "Ezarpen aurreratuak" +backToList = "Itzuli sinadura-eskaeretara" +certificateChoice = "Hautatu sinatzeko ziurtagiri bat" +changeSignature = "Aldatu sinadura" +clearSignature = "Garbitu sinadura" +completeAndSign = "Osatu eta sinatu" +createNewSignature = "Sortu sinadura berria" +declineButton = "Ukatu" +decline = "Ukatu eskaera" +deleteSelected = "Ezabatu hautatutako sinadura" +drawSignature = "Marraztu zure sinadura behean" +dueDate = "Epemuga" +fileTooLarge = "Fitxategiaren tamainak 5MB baino txikiagoa izan behar du" +fontFamily = "Letra-familia" +fontSize = "Letra-tamaina: {{size}}px" +fontSizePlaceholder = "Tamaina" +from = "Nork" +invalidCertFile = "Hautatu P12 edo PFX ziurtagiri-fitxategi bat" +invalidFileType = "Hautatu irudi-fitxategi bat" +location = "Kokapena (aukera)" +locationPlaceholder = "Nondik sinatzen ari zara?" +message = "Mezua" +noCertificate = "Hautatu ziurtagiri-fitxategi bat" +noSignatures = "Jarri gutxienez sinadura bat PDFan" +p12File = "P12/PFX ziurtagiri-fitxategia" +password = "Ziurtagiriaren pasahitza" +passwordPlaceholder = "Idatzi pasahitza..." +penColor = "Arkatzaren kolorea" +penSize = "Arkatzaren tamaina: {{size}}px" +placementActive = "Egin klik PDFan jartzeko" +placeSignatureButton = "Jarri sinadura PDFan" +reason = "Arrazoia (aukera)" +reasonPlaceholder = "Zergatik sinatzen ari zara?" +removeImage = "Kendu irudia" +removeCertFile = "Kendu fitxategia" +savedSignatures = "Gordetako sinadurak" +selectFile = "Hautatu irudi-fitxategia" +selectSignatureTitle = "Hautatu edo sortu sinadura" +signButton = "Sinatu dokumentua" +signatureInfo = "Ezarpen hauek dokumentuaren jabeak konfiguratzen ditu" +signaturePlaced = "Sinadura orrian jarrita" +signatureSettings = "Sinadura-ezarpenak" +signatureText = "Sinadura-testua" +signatureTextPlaceholder = "Idatzi zure izena..." +signatureTypeLabel = "Sinadura mota" +signingTitle = "Sinatzea" +textColor = "Testu-kolorea" +typeSignature = "Idatzi zure izena sinadura sortzeko" +uploadCert = "Pertsonalizatutako ziurtagiria" +uploadCertDesc = "Erabili zure P12/PFX ziurtagiria" +uploadSignature = "Igo zure sinadura-irudia" +usePersonalCert = "Ziurtagiri pertsonala" +usePersonalCertDesc = "Zure konturako automatikoki sortua" +useServerCert = "Erakundeko ziurtagiria" +useServerCertDesc = "Erakundeak partekatutako ziurtagiria" +workbenchTitle = "Sinadura-eskaera" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Aukeratu trazatuaren kolorea" +continue = "Jarraitu" + +[certSign.collab.signRequest.certModal] +description = "{{count}} sinadura jarri dituzu. Aukeratu zure ziurtagiria sinadura osatzeko." +sign = "Sinatu dokumentua" +certValidating = "Ziurtagiria balioztatzen..." +certValidUntil = "Ziurtagiria baliozkoa: {{date}} arte" +certInvalid = "Ziurtagiri baliogabea: {{error}}" +certInvalidFallback = "Ziurtagiri baliogabea" +certNetworkError = "Ezin izan da ziurtagiria balioztatu" +title = "Konfiguratu ziurtagiria" + +[certSign.collab.signRequest.image] +hint = "Igo zure sinaduraren PNG edo JPG irudia" + +[certSign.collab.signRequest.mode] +move = "Mugitu sinadura" +place = "Jarri sinadura" +title = "Sinatu edo mugitu modua" + +[certSign.collab.signRequest.modeTabs] +draw = "Marraztu" +image = "Igo" +text = "Idatzi" + +[certSign.collab.signRequest.placeSignature] +message = "Egin klik PDFan zure sinadura jartzeko" +title = "Jarri sinadura" + +[certSign.collab.signRequest.preview] +imageAlt = "Hautatutako sinadura" +missing = "Aurrebistarik ez" +textFallback = "Sinadura" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Marraztutako sinadura" +defaultImageLabel = "Igotako sinadura" +defaultLabel = "Sinadura" +defaultTextLabel = "Idatzitako sinadura" +delete = "Ezabatu sinadura" +none = "Ez dago gordetako sinadurarik" + +[certSign.collab.signRequest.signatureType] +draw = "Marraztu" +type = "Idatzi" +upload = "Igo" + +[certSign.collab.signRequest.steps] +back = "Atzera" +cancelPlacement = "Utzi kokapena" +certificate = "Ziurtagiria" +clickMultipleTimes = "Egin klik PDFan hainbat aldiz sinadurak jartzeko. Arrastatu edozein sinadura mugitzeko edo tamaina aldatzeko." +clickToPlace = "Egin klik PDFan zure sinadura agertzea nahi duzun tokian." +continue = "Jarraitu ziurtagiria hautatzera" +continueToPlacement = "Jarraitu kokapenera" +continueToReview = "Jarraitu berrikuspenera" +createSignature = "Sortu sinadura" +invisible = "Ikusezina" +location = "Kokapena:" +multipleSignatures = "{{count}} sinadura aplikatuko dira PDFari" +oneSignature = "Sinadura 1 aplikatuko da PDFari" +placeOnPdf = "Jarri PDFan" +reason = "Arrazoia:" +reviewTitle = "Berrikusi sinatu aurretik" +signaturePlaced = "Sinadura {{page}} orrian jarri da. Posizioa doitzeko berriro klik egin edo jarraitu berrikustera." +visible = "Ikusgai" +visibility = "Ikusgaitasuna:" +yourSignatures = "Zure sinadurak ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Kolorea" +fontLabel = "Letra-tipoa" +fontSizeLabel = "Tamaina" +fontSizePlaceholder = "16" +label = "Sinadura-testua" +modalHint = "Idatzi zure izena, eta egin klik Jarraitu botoian PDFan jartzeko." +placeholder = "Idatzi zure izena..." + +[certSign.collab.participant] +certValidating = "Ziurtagiria balioztatzen..." +certValid = "✓ Ziurtagiria baliozkoa" +certValidUntil = " {{date}} arte" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Ziurtagiri baliogabea" +certNetworkError = "Ezin izan da ziurtagiria balioztatu" + +[certSign.collab.addParticipants] +add = "Gehitu {{count}} parte-hartzaile" +back = "Atzera" +configureSignatures = "Konfiguratu sinadura-ezarpenak" +continue = "Jarraitu sinadura-ezarpenetara" +reasonHelp = "Aurrez ezarri sinatzeko arrazoia parte-hartzaile hauentzat (aukerakoa; sinatzean alda dezakete)" +reasonPlaceholder = "adib. Onarpena, Berrikuspena..." +selectUsers = "Hautatu erabiltzaileak" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Sartu sinaduren laburpen orria" +includeSummaryPageHelp = "Azkenean orri bat gehituko da sinadura metadatu guztiekin. Orri indibidualetako ziurtagiri digitalen sinadura-koadroak ezkutatuko dira (sinadura hezeei ez die eragiten)." + +[certSign.collab.sessionList] +active = "Aktiboak" +finalized = "Amaituta" + +[certSign.collab.signatureSettings] +description = "Konfiguratu sinaduren itxura parte-hartzaile guztientzat" +title = "Sinaduraren itxura" + +[certSign.collab.userSelector] +inviteUsers = "Gehitu erabiltzaileak" +loadError = "Ezin izan da erabiltzaileak kargatu" +noTeam = "Talderik ez" +noUsers = "Ez da beste erabiltzailerik aurkitu." +placeholder = "Hautatu erabiltzaileak..." + +[certSign.mobile] +panelActions = "Ekintzak" +panelDocument = "Dokumentua" +panelPeople = "Pertsonak" + +[certSign.sessions] +deleted = "Saioa ezabatuta" +fetchFailed = "Ezin izan da saioaren xehetasunak kargatu" +finalized = "Saioa amaituta" +loaded = "Sinatutako PDFa kargatuta" +pdfNotReady = "PDF prest ez" +pdfNotReadyDesc = "Sinatutako PDFa sortzen ari da. Saiatu berriro une batean." + +[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" + [changeCreds] changePassword = "Saioa hasteko kredentzial lehenetsiak erabiltzen ari zara. Mesedez, sartu pasahitz berria" changeUsername = "Eguneratu zure erabiltzaile-izena. Eguneratu ondoren saioa itxiko da." @@ -3242,6 +3531,46 @@ totalSelected = "Guztira hautatuta" unsupported = "Ez da onartzen" unzip = "Deskonprimitu" uploadError = "Zenbait fitxategi igotzeak huts egin du." +copyCreated = "Kopia gailu honetan gorde da." +copyFailed = "Ezin izan da kopiarik sortu." +leaveShare = "Kendu nire zerrendatik" +leaveShareFailed = "Ezin izan da fitxategi partekatua kendu." +leaveShareSuccess = "Zure partekatuen zerrendatik kendu da." +removeBoth = "Kendu bietatik" +removeFilePrompt = "Fitxategi hau gailu honetan eta zure zerbitzarian gordeta dago. Nondik kendu nahi duzu?" +removeFileTitle = "Kendu fitxategia" +removeLocalOnly = "Gailu hau bakarrik" +removeServerFailed = "Ezin izan da fitxategia zerbitzaritik kendu." +removeServerOnly = "Zerbitzaria bakarrik" +removeServerOnlyPrompt = "Fitxategi hau zure zerbitzarian soilik gordeta dago. Zerbitzaritik kendu nahi duzu?" +removeServerSuccess = "Zerbitzaritik kenduta." +removeSharedPrompt = "Fitxategi hau zurekin partekatuta dago. Gailu honetatik edo zure partekatuen zerrendatik ken dezakezu." +removeSharedServerOnlyBlockedPrompt = "Fitxategi hau zurekin partekatuta dago eta zerbitzarian soilik gordeta dago." +removeSharedServerOnlyPrompt = "Fitxategi hau zurekin partekatuta dago eta zerbitzarian soilik gordeta dago. Zure zerrendatik kendu?" +changesNotUploaded = "Aldaketak ez dira igo" +cloudFile = "Hodeiko fitxategia" +filterAll = "Denak" +filterLocal = "Lokal" +filterSharedByMe = "Nik partekatuak" +filterSharedWithMe = "Nirekin partekatuak" +lastSynced = "Azken sinkronizazioa" +localOnly = "Lokal soilik" +makeCopy = "Egin kopia bat" +owner = "Jabea" +ownerUnknown = "Ezezaguna" +share = "Partekatu" +shareSelected = "Partekatu hautatutakoak" +sharedByYou = "Zuk partekatua" +sharedEditNoticeBody = "Ez duzu zerbitzariko bertsio hau editatzeko baimenik. Egiten dituzun edizioak kopia lokal gisa gordeko dira." +sharedEditNoticeConfirm = "Ulertuta" +sharedEditNoticeTitle = "Zerbitzariko kopia irakurtzeko soilik" +sharedWithYou = "Zurekin partekatua" +sharing = "Partekatzea" +storageState = "Biltegiratzea" +synced = "Sinkronizatuta" +updateOnServer = "Eguneratu zerbitzarian" +uploadSelected = "Igo hautatutakoak" +uploadToServer = "Igo zerbitzarira" [files] addFiles = "Gehitu fitxategiak" @@ -3367,6 +3696,77 @@ title = "PDFak lautzeaz" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Taldeko sinadurari buruz" + +[groupSigning.tooltip.finalization] +bullet1 = "Sinadura guztiak zehaztu duzun parte-hartzaileen ordenan aplikatuko dira" +bullet2 = "Beharrezkoa bada, sinadura partzialekin amai dezakezu" +bullet3 = "Behin amaituta, saioa ezin da aldatu" +description = "Parte-hartzaile guztiek sinatu dutenean (edo goiz amaitzea aukeratzen duzunean), azken sinatutako PDFa sor dezakezu." +title = "Amaitzeko prozesua" + +[groupSigning.tooltip.roles] +bullet1 = "Jabea (zu): saioa sortzen du, sinaduraren lehenetsiak konfiguratzen ditu, dokumentua amaitzen du" +bullet2 = "Parte-hartzaileak: beren sinadura sortu, ziurtagiria aukeratu eta PDFan kokatu" +bullet3 = "Parte-hartzaileek ezin dituzte aldatu sinaduraren ikusgaitasuna, arrazoia edo kokapen-ezarpenak" +description = "Parte-hartzaile guztientzako sinadura-itxuraren ezarpenak zuk kontrolatzen dituzu." +title = "Parte-hartzaileen rolak" + +[groupSigning.tooltip.sequential] +bullet1 = "Lehen parte-hartzaileak sinatu behar du bigarrenak dokumentua atzitu aurretik" +bullet2 = "Betetzen du sinadura-ordena egokia eskakizun legaletarako" +bullet3 = "Parte-hartzaileak berrantola ditzakezu zerrendan arrastatuz" +description = "Parte-hartzaileek zuk zehaztutako ordenan sinatzen dute dokumentua. Sinatzeko txanda dutenean jakinarazpena jasotzen dute." +title = "Sinadura sekuentziala" + +[groupSigning.steps] +back = "Atzera" +completed = "Osatuta" +current = "Unekoa" +stepLabel = "{{number}}. urratsa" + +[groupSigning.steps.configureDefaults] +continue = "Jarraitu berrikuspenara" +invisible = "Sinadurak ikusezinak izango dira (metadatuak baino ez)" +locationLabel = "Kokapena:" +preview = "Aurrebista" +reasonLabel = "Arrazoia:" +title = "Konfiguratu sinadura-ezarpenak" +visible = "Sinadurak ikusgai izango dira {{page}} orrian" + +[groupSigning.steps.review] +document = "Dokumentua" +dueDate = "Epemuga (aukerakoa)" +dueDatePlaceholder = "Hautatu epemuga..." +invisible = "Ikusezina (metadatuak soilik)" +location = "Kokapena:" +logo = "Logotipoa:" +logoHidden = "Logotiporik ez" +logoShown = "Stirling PDF logotipoa erakusten da" +participants = "Parte-hartzaileak" +reason = "Arrazoia:" +send = "Bidali sinadura-eskaerak" +signatureSettings = "Sinadura-ezarpenak" +title = "Berrikusi saioaren xehetasunak" +titleShort = "Berrikusi eta bidali" +visibility = "Ikusgaitasuna:" +visible = "Ikusgai {{page}} orrian" +participantCount = "{{count}} parte-hartzailek sinatuko dute ordenean" + +[groupSigning.steps.selectDocument] +continue = "Jarraitu parte-hartzaileak hautatzera" +noFile = "Hautatu fitxategi aktiboetatik PDF bakar bat sinatze-saioa sortzeko." +selectedFile = "Hautatutako dokumentua" +title = "Hautatu dokumentua" + +[groupSigning.steps.selectParticipants] +continue = "Jarraitu sinadura-ezarpenetara" +count = "{{count}} parte-hartzaile hautatuta" +label = "Hautatu parte-hartzaileak" +placeholder = "Aukeratu parte-hartzaileak sinatzeko..." +title = "Aukeratu parte-hartzaileak" + [getPdfInfo] downloadJson = "Deskargatu JSON" downloads = "Deskargak" @@ -4460,7 +4860,10 @@ zoomOut = "Zoom txikitu" [viewer] cannotPreviewFile = "Ezin da fitxategia aurreikusi" +disableColorFilter = "Desgaitu kolore-iragazkia" dualPageView = "Orri biko ikuspegia" +enableDarkFilter = "Gaitu iragazki iluna" +enableSepiaFilter = "Gaitu sepia iragazkia" firstPage = "Lehen orria" lastPage = "Azken orria" nextPage = "Hurrengo orria" @@ -4470,6 +4873,22 @@ singlePageView = "Orri bakarreko ikuspegia" unknownFile = "Fitxategi ezezaguna" zoomIn = "Zoom handitu" zoomOut = "Zoom txikitu" +resetZoom = "Berrezarri zooma" + +[viewer.nonPdf] +fileTypeBadge = "{{type}} fitxategia" +convertToPdf = "Bihurtu PDFra" +loading = "Kargatzen..." +emptyFile = "Fitxategi hutsa" +csvStats = "{{rows}} errenkada · {{columns}} zutabe · {{size}}" +sortedBy = "Ordenatua: {{column}}" +columnDefault = "{{index}}. zutabea" +htmlPreviewWarning = "HTML aurrebista — kanpoko baliabideak agian ez dira kargatuko · {{size}}" +htmlPreview = "HTML aurrebista" +invalidJson = "JSON baliogabea — edukia gordinean erakusten" +textStats = "{{lines}} lerro · {{size}}" +lineNumbers = "Lerro-zenbakiak" +renderMarkdown = "Errendatu Markdown" [viewer.attachments] title = "Eranskinak" @@ -4531,6 +4950,7 @@ toggleAttachments = "Eranskinak erakutsi/ezkutatu" toggleTheme = "Gaia txandakatu" language = "Hizkuntza" toggleAnnotations = "Oharpenen ikusgarritasuna txandakatu" +toggleLayers = "Txandakatu geruzak" search = "Bilatu PDF" panMode = "Pan modua" applyRedactionsFirst = "Lehenik aplikatu zentsurak" @@ -5407,20 +5827,72 @@ title = "Inprimatu fitxategia" 2 = "Idatzi inprimagailuaren izena" [quickAccess] +access = "Atzipena" +accessAddPerson = "Gehitu beste pertsona bat" +accessBack = "Atzera" +accessCopyLink = "Kopiatu esteka" +accessEmail = "Helbide elektronikoa" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Fitxategia" +accessGeneral = "Atzipen orokorra" +accessInviteTitle = "Gonbidatu pertsonak" +accessOwner = "Jabea" +accessPanel = "Dokumentuaren atzipena" +accessPeople = "Atzipena duten pertsonak" +accessRemove = "Kendu" +accessRestricted = "Mugatua" +accessRestrictedHint = "Atzipena duten pertsonek soilik ireki dezakete" +accessRole = "Rola" +accessRoleCommenter = "Iruzkingilea" +accessRoleEditor = "Editorea" +accessRoleViewer = "Ikuslea" +accessSelectedFile = "Hautatutako fitxategia" +accessSendInvite = "Bidali gonbidapena" +accessTitle = "Dokumentuaren atzipena" +accessYou = "Zu" account = "Kontua" +activeSessions = "Saio aktiboak" +activeTab = "Aktiboak" activity = "Jarduera" adminSettings = "Admin aukerak" +allSessions = "Saio guztiak" allTools = "All Tools" automate = "Autom." +back = "Atzera" +certSign = "Ziurtagiri bidezko sinadura" +completedSessions = "Amaitutako saioak" +completedTab = "Amaituta" config = "Konfig." +createNew = "Sortu eskaera berria" +createSession = "Sortu sinadura-eskaera" +dueDate = "Epemuga (aukerakoa)" files = "Fitx." help = "Laguntza" +noActiveSessions = "Ez dago zain dauden sinadura-eskaerarik edo saio aktiborik" +noCompletedSessions = "Ez dago amaitutako saiorik" +noFile = "Ez da fitxategirik hautatu" read = "Irakurri" reader = "Irakurri" +refresh = "Freskatu" +requestSignatures = "Eskatu sinadurak" +selectSingleFileToRequest = "Hautatu PDF fitxategi bakarra sinadurak eskatzeko" +selectedFile = "Hautatutako fitxategia" +selectUsers = "Hautatu sinatuko duten erabiltzaileak" +selectUsersPlaceholder = "Aukeratu parte-hartzaileak..." +sendingRequest = "Bidaltzen..." settings = "Aukerak" showMeAround = "Erakutsi ingurunea" sign = "Sinatu" +signatureRequests = "Sinadura-eskaerak" +signYourself = "Sinatu zeuk" +newRequest = "Eskaera berria" tours = "Ibilbideak" +wetSign = "Gehitu sinadura" +filterMine = "Nireak" +filterOverdue = "Epea gaindituta" +filterSigned = "Sinatuta" +filterDeclined = "Ukatua" +searchDocuments = "Bilatu dokumentuak…" [quickAccess.helpMenu] adminTour = "Administrazio ibilaldia" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Zure Stirling-PDF zerbitzaria lineaz kanpo dago eta \ expired = "Zure saioa iraungi da. Freskatu orria eta saiatu berriro." refreshPage = "Freskatu orria" +[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" + +[signatureSettings.tooltip] +header = "Sinaduraren itxuraren ezarpenak" + +[signatureSettings.tooltip.location] +bullet1 = "Adibideak: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Ez da orri-posizio bera" +bullet3 = "Lege-eremu jakin batzuetan beharrezkoa izan daiteke" +description = "Aukerako kokapen geografikoa non sinadura aplikatu den. Ziurtagiriaren metadatuetan gordetzen da." +title = "Sinaduraren kokapena" + +[signatureSettings.tooltip.logo] +bullet1 = "Sinaduraren eta testuaren ondoan bistaratzen da" +bullet2 = "PNG, JPG formatuak onartzen ditu" +bullet3 = "Itxura profesionala hobetzen du" +description = "Gehitu enpresaren logotipoa ikusgai dauden sinadurei branding eta egiazkotasunerako." +title = "Enpresaren logotipoa" + +[signatureSettings.tooltip.reason] +bullet1 = "Adibideak: \"Onarpena\", \"Kontratu-akordioa\", \"Berrikuspena amaituta\"" +bullet2 = "PDFko sinadura-propietateetan ikusgai" +bullet3 = "Baliagarria auditoretza-arrastoetarako eta araudia betetzeko" +description = "Dokumentua zergatik sinatzen ari den azaltzen duen aukerako testua. Ziurtagiriaren metadatuetan gordetzen da." +title = "Sinaduraren arrazoia" + +[signatureSettings.tooltip.visibility] +bullet1 = "Ikusgai: Sinadura PDFan agertzen da itxura pertsonalizatuarekin" +bullet2 = "Ikusezina: Ziurtagiria ikusizko markarik gabe kapsulatuta" +bullet3 = "Sinadura ikusezinek ere baliozkotze kriptografikoa ematen dute" +description = "Kontrolatzen du sinadura dokumentuan ikusgai den ala modu ikusezinean kapsulatuta." +title = "Sinaduraren ikusgaitasuna" + [settings.configuration] advanced = "Aurreratua" database = "Datu-basea" endpoints = "Endpoints" features = "Eginbideak" +storageSharing = "Fitxategi biltegiratzea eta partekatzea" systemSettings = "Sistemaren ezarpenak" title = "Konfigurazioa" @@ -6332,10 +6868,13 @@ title = "Hasi saioa Stirlingen" [setup.selfhosted] link = "edo konektatu autoostatutako kontu batera" subtitle = "Sartu zure zerbitzariaren kredentzialak" +changeServerLocked = "Zure erakundeak aplikazio hau zerbitzari jakin batera mugatu du" switchToLocal = "Erabili tokiko tresnak" title = "Hasi saioa zerbitzarian" [setup.selfhosted.unreachable] +changeServer = "Konektatu beste zerbitzari batera" +changeServerLocked = "Zure erakundeak aplikazio hau zerbitzari jakin batera mugatu du" continueOffline = "Erabili tokiko tresnak" message = "Ezin izan da {{url}} atzitu. Egiaztatu zerbitzaria martxan eta atzigarri dagoela." retry = "Saiatu berriro" @@ -6529,6 +7068,15 @@ saved = "Gordeta" text = "Testua" title = "Sinadura mota" +[signRequest] +declined = "Sinadura-eskaera ukatua" +fetchFailed = "Ezin izan da sinadura-eskaera kargatu" +signed = "Dokumentua ongi sinatu da" + +[signSession] +createFailed = "Ezin izan da sinadura-eskaera sortu" +created = "Sinadura-eskaera bidalita" + [signup] accountCreatedSuccessfully = "Kontua ongi sortu da! Orain saioa hasi dezakezu." alreadyHaveAccount = "Kontua baduzu? Hasi saioa" @@ -6807,6 +7355,106 @@ title = "Zatikatu PDF kapituluen arabera" [splitPdfByChapters] tags = "zatikatu,kapituluak,lastermarkak,antolatu" +[storageShare] +accessed = "Atzituta" +accessDenied = "Ez duzu fitxategi partekatu honetarako atzipenik. Eskatu jabeari zurekin partekatzeko." +accessFailed = "Ezin izan da jarduera kargatu." +accessDeniedBody = "Ez duzu fitxategi honetarako atzipenik. Eskatu jabeari zurekin partekatzeko." +accessDeniedTitle = "Atzipenik ez" +accessLimitedCommenter = "Iruzkinetarako atzipena laster egongo da eskuragarri. Deskargatu behar baduzu, eskatu jabeari editore-atzipena." +accessLimitedTitle = "Atzipen mugatua" +accessLimitedViewer = "Esteka hau ikuspegi-soilerako da. Deskargatu behar baduzu, eskatu jabeari editore-atzipena." +createdAt = "Sortua" +download = "Deskargatu" +downloadFailed = "Ezin da fitxategi hau deskargatu." +expiredBody = "Partekatze-esteka hau baliogabea da edo iraungi da." +expiredTitle = "Esteka iraungita" +goToLogin = "Joan saioa-hasiera orrira" +loadFailed = "Ezin da fitxategi partekatua ireki." +loading = "Partekatze-esteka kargatzen..." +loginPrompt = "Hasi saioa fitxategi partekatu honetara sartzeko." +loginRequired = "Saioa behar da" +openInApp = "Ireki Stirling PDF" +ownerLabel = "Jabea" +ownerUnknown = "Ezezaguna" +requiresLogin = "Fitxategi partekatu honek saioa behar du." +roleCommenter = "Iruzkingilea" +roleEditor = "Editorea" +roleViewer = "Ikuslea" +shareHeading = "Fitxategi partekatua" +titleDefault = "Fitxategi partekatua" +tryAgain = "Saiatu berriro geroago." +addUser = "Gehitu" +commenterHint = "Iruzkinak laster egongo dira eskuragarri." +copied = "Esteka arbelean kopiatu da" +copy = "Kopiatu" +copyFailed = "Ezin izan da kopiatu" +description = "Sortu partekatzeko esteka fitxategi honetarako. Saioa hasita duten erabiltzaileek estekaren bidez atzi dezakete." +downloadsCount = "Deskargak: {{count}}" +emailWarningBody = "Honek helbide elektroniko baten itxura du. Pertsona hau Stirling PDF erabiltzailea ez bada, ezin izango du fitxategia atzitu." +emailWarningConfirm = "Partekatu hala ere" +emailWarningTitle = "Helbide elektronikoa" +errorTitle = "Partekatzea huts egin du" +failure = "Ezin izan da partekatzeko estekarik sortu. Saiatu berriro." +fileLabel = "Fitxategia" +generate = "Sortu esteka" +generated = "Partekatzeko esteka sortu da" +hideActivity = "Ezkutatu jarduera" +invalidUsername = "Idatzi erabiltzaile-izen edo helbide elektroniko balioduna." +lastAccessed = "Azken atzipena" +linkAccessTitle = "Partekatzeko estekaren atzipena" +linkLabel = "Partekatzeko esteka" +linksDisabled = "Partekatzeko estekak desgaituta daude." +linksDisabledBody = "Partekatzeko estekak zure zerbitzariaren ezarpenek desgaitu dituzte." +manage = "Kudeatu partekatzea" +manageDescription = "Sortu eta kudeatu fitxategi hau partekatzeko estekak." +manageLoadFailed = "Ezin izan dira partekatzeko estekak kargatu." +manageTitle = "Partekatzea kudeatu" +noActivity = "Oraindik ez dago jarduerarik." +noLinks = "Ez dago partekatzeko esteka aktiborik oraindik." +noSharedUsers = "Oraindik ez du inork atzipenik." +removeLink = "Kendu esteka" +removeUser = "Kendu" +revokeFailed = "Ezin izan da partekatzeko esteka kendu." +revoked = "Partekatze-esteka kendu da" +roleLabel = "Rola" +sharingDisabled = "Partekatzea desgaituta dago." +sharingDisabledBody = "Zerbitzariaren ezarpenek partekatzea desgaitu dute." +sharedUsersTitle = "Partekatutako erabiltzaileak" +title = "Partekatu fitxategia" +unknownUser = "Erabiltzaile ezezaguna" +userAddFailed = "Ezin izan da erabiltzaile horrekin partekatu." +userAdded = "Erabiltzailea partekatutako zerrendan gehitu da." +usernameLabel = "Erabiltzaile-izena edo helbide elektronikoa" +usernamePlaceholder = "Sartu erabiltzaile-izen bat edo helbide elektroniko bat" +userRemoveFailed = "Ezin izan da erabiltzaile hori kendu." +userRemoved = "Erabiltzailea partekatutako zerrendatik kendu da." +viewActivity = "Ikusi jarduera" +viewed = "Ikusita" +viewsCount = "Ikustaldiak: {{count}}" +downloaded = "Deskargatuta" +bulkDescription = "Sortu esteka bakarra hautatutako fitxategi guztiak saioa hasita duten erabiltzaileekin partekatzeko." +bulkTitle = "Partekatu hautatutako fitxategiak" +copyLink = "Kopiatu partekatze-esteka" +fileCount = "{{count}} fitxategi hautatuta" +ownerOnly = "Jabeak bakarrik kudea dezake partekatzea." +selectSingleFile = "Hautatu fitxategi bakarra partekatzea kudeatzeko." + +[storageUpload] +description = "Honek uneko fitxategia zerbitzariaren biltegira igotzen du zure sarbiderako." +errorTitle = "Igoerak huts egin du" +failure = "Igoerak huts egin du. Egiaztatu zure saio-hasiera eta biltegiratze ezarpenak." +fileLabel = "Fitxategia" +hint = "Esteka publikoak eta sarbide-moduak zure zerbitzariaren ezarpenek kontrolatzen dituzte." +success = "Zerbitzarira igo da" +title = "Igo zerbitzarira" +updateButton = "Eguneratu zerbitzarian" +uploadButton = "Igo zerbitzarira" +bulkDescription = "Honek hautatutako fitxategiak zure zerbitzariaren biltegira igotzen ditu." +bulkTitle = "Igo hautatutako fitxategiak" +fileCount = "{{count}} fitxategi hautatuta" +more = " +{{count}} gehiago" + [storage] approximateSize = "Gutxi gorabeherako tamaina" fileTooLarge = "Fitxategia handiegia. Fitxategi bakoitzerako gehienezko tamaina da" @@ -7153,6 +7801,30 @@ title = "Ikusi/Editatu PDF" [warning] tooltipTitle = "Abisua" +[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" + [watermark] completed = "Ur‑marka gehituta" desc = "Gehitu testu edo irudi ur‑markak PDF fitxategiei" @@ -7333,6 +8005,7 @@ activeSession = "Saio aktiboa" addMembers = "Gehitu kideak" admin = "Admin" confirmDelete = "Ziur erabiltzaile hau ezabatu nahi duzula? Ekintza hau ezin da desegin." +confirmUnlock = "Ziur al zaude erabiltzaile-kontu hau desblokeatu nahi duzula?" deleteUser = "Ezabatu erabiltzailea" deleteUserError = "Ezin izan da erabiltzailea ezabatu" deleteUserSuccess = "Erabiltzailea ongi ezabatu da" @@ -7341,6 +8014,8 @@ disable = "Desgaitu" disabled = "Desgaituta" editRole = "Rola editatu" enable = "Gaitu" +locked = "blokeatuta" +lockedBadge = "Blokeatuta" loading = "Pertsonak kargatzen..." loginRequired = "Gaitu saio-hasiera modua lehenik" member = "Kidea" @@ -7350,6 +8025,9 @@ searchMembers = "Bilatu kideak..." status = "Egoera" team = "Taldea" title = "Pertsonak" +unlockAccount = "Desblokeatu kontua" +unlockUserError = "Erabiltzaile-kontua desblokeatzeak huts egin du" +unlockUserSuccess = "Erabiltzaile-kontua arrakastaz desblokeatu da" user = "Erabiltzailea" [workspace.people.actions] diff --git a/frontend/public/locales/fa-IR/translation.toml b/frontend/public/locales/fa-IR/translation.toml index 8ddb7f9abd..8653a58a09 100644 --- a/frontend/public/locales/fa-IR/translation.toml +++ b/frontend/public/locales/fa-IR/translation.toml @@ -8,6 +8,7 @@ black = "سیاه" blue = "آبی" bored = "منتظر ماندن خسته‌کننده است؟" cancel = "انصراÙ" +confirm = "تأیید" changedCredsMessage = "مشخصات تغییر ÛŒØ§ÙØª!" chooseFile = "انتخاب ÙØ§ÛŒÙ„" close = "بستن" @@ -146,6 +147,7 @@ insufficientCredits = "اعتبار کاÙÛŒ نیست. موردنیاز: {{requi loadingCredits = "در حال بررسی اعتبار..." loadingProStatus = "در حال بررسی وضعیت اشتراک..." noticeTopUpOrPlan = "اعتبار کاÙÛŒ نیست، Ù„Ø·ÙØ§Ù‹ اعتبار را شارژ کنید یا طرح خود را ارتقا دهید" +accessInvite = "دعوت" [account] accountSettings = "تنظیمات حساب" @@ -1427,6 +1429,34 @@ title = "پردازش" description = "حداکثر زمان انتظار برای یک کار پردازش پیش از گزارش خطا." label = "مهلت پردازش (ثانیه)" +[admin.settings.storage] +description = "کنترل گزینه‌های ذخیره‌سازی سرور Ùˆ اشتراک‌گذاری." +title = "ذخیره‌سازی ÙØ§ÛŒÙ„ Ùˆ اشتراک‌گذاری" + +[admin.settings.storage.enabled] +description = "به کاربران اجازه دهید ÙØ§ÛŒÙ„‌ها را روی سرور ذخیره کنند." +label = "ÙØ¹Ø§Ù„‌سازی ذخیره‌سازی ÙØ§ÛŒÙ„ روی سرور" + +[admin.settings.storage.sharing.email] +description = "اجازه اشتراک‌گذاری با نشانی‌های ایمیل." +label = "ÙØ¹Ø§Ù„‌سازی اشتراک‌گذاری ایمیلی" +mailLink = "پیکربندی تنظیمات ایمیل" +mailNote = "نیازمند پیکربندی ایمیل است. " + +[admin.settings.storage.sharing.enabled] +description = "به کاربران اجازه دهید ÙØ§ÛŒÙ„‌های ذخیره‌شده را به اشتراک بگذارند." +label = "ÙØ¹Ø§Ù„‌سازی اشتراک‌گذاری" + +[admin.settings.storage.sharing.links] +description = "اجازه اشتراک‌گذاری از طریق لینک‌هایی Ú©Ù‡ نیاز به ورود دارند." +frontendUrlLink = "پیکربندی در تنظیمات سیستم" +frontendUrlNote = "نیازمند یک Frontend URL است. " +label = "ÙØ¹Ø§Ù„‌سازی لینک‌های اشتراک" + +[admin.settings.storage.signing.enabled] +description = "به کاربران اجازه دهید نشست‌های امضای چندشرکت‌کننده بسازند. نیازمند ÙØ¹Ø§Ù„ بودن ذخیره‌سازی ÙØ§ÛŒÙ„ روی سرور است." +label = "ÙØ¹Ø§Ù„‌سازی امضای گروهی (Ø¢Ù„ÙØ§)" + [admin.settings.unsavedChanges] cancel = "ادامه ویرایش" discard = "نادیده Ú¯Ø±ÙØªÙ† تغییرات" @@ -2059,7 +2089,19 @@ numbers = "اعداد/بازه‌ها: 5, 10-20" progressions = "پیشروی‌ها: 3nØŒ 4n+1" [certSign] +allSigned = "همه شرکت‌کنندگان امضا کرده‌اند. آماده نهایی‌سازی." +awaitingSignatures = "در انتظار امضاها" +signatureProgress = "{{signedCount}}/{{totalCount}} امضا" chooseCertificate = "انتخاب ÙØ§ÛŒÙ„ گواهی" +declined = "رد شده" +fetchFailed = "بارگذاری داده‌های امضا ناموÙÙ‚ بود" +finalized = "نهایی شد" +notified = "در انتظار" +partialNote = "می‌توانید زودتر با امضاهای ÙØ¹Ù„ÛŒ نهایی‌سازی کنید. شرکت‌کنندگان امضانشده کنار گذاشته خواهند شد." +pending = "در انتظار" +readyToFinalize = "آماده نهایی‌سازی" +signed = "امضا شد" +viewed = "مشاهده شد" chooseJksFile = "انتخاب ÙØ§ÛŒÙ„ JKS" chooseP12File = "انتخاب ÙØ§ÛŒÙ„ PKCS12" choosePfxFile = "انتخاب ÙØ§ÛŒÙ„ PFX" @@ -2082,6 +2124,7 @@ title = "امضای گواهی" invisible = "نامرئی" stepTitle = "ظاهر امضا" visible = "مرئی" +visibility = "قابلیت مشاهده" [certSign.appearance.options] title = "جزئیات امضا" @@ -2188,6 +2231,252 @@ bullet4 = "امکان Ø§Ø³ØªÙØ§Ø¯Ù‡ از گواهی‌های Ø³ÙØ§Ø±Ø´ÛŒ بر text = "وقتی امضاها را بررسی می‌کنید، ابزار به شما می‌گوید آیا معتبرند، Ú†Ù‡ کسی سند را امضا کرده، Ú†Ù‡ زمانی امضا شده Ùˆ اینکه آیا سند از زمان امضا تغییر کرده است یا نه." title = "بررسی امضاها" +[certSign.collab.finalize] +button = "نهایی‌سازی Ùˆ بارگذاری PDF امضاشده" +early = "نهایی‌سازی با امضاهای ÙØ¹Ù„ÛŒ" + +[certSign.collab.sessionDetail] +addButton = "Ø§ÙØ²ÙˆØ¯Ù† شرکت‌کنندگان" +addParticipants = "Ø§ÙØ²ÙˆØ¯Ù† شرکت‌کنندگان" +addParticipantsError = "Ø§ÙØ²ÙˆØ¯Ù† شرکت‌کنندگان ناموÙÙ‚ بود" +backToList = "بازگشت به نشست‌ها" +deleteConfirm = "مطمئن هستید؟ این کار قابل بازگشت نیست." +deleteError = "حذ٠نشست ناموÙÙ‚ بود" +deleted = "نشست حذ٠شد" +deleteSession = "حذ٠نشست" +dueDate = "تاریخ سررسید" +finalizeError = "نهایی‌سازی نشست ناموÙÙ‚ بود" +loadPdfError = "بارگذاری PDF امضاشده ناموÙÙ‚ بود" +loadSignedPdf = "بارگذاری PDF امضاشده در ÙØ§ÛŒÙ„‌های ÙØ¹Ø§Ù„" +messageLabel = "پیام" +noAdditionalInfo = "اطلاعات اضاÙÛŒ وجود ندارد" +owner = "مالک" +participantRemoved = "شرکت‌کننده حذ٠شد" +participants = "شرکت‌کنندگان" +participantsAdded = "شرکت‌کنندگان با موÙقیت Ø§ÙØ²ÙˆØ¯Ù‡ شدند" +removeParticipant = "حذÙ" +removeParticipantError = "حذ٠شرکت‌کننده ناموÙÙ‚ بود" +selectUsers = "کاربران را انتخاب کنید..." +sessionInfo = "اطلاعات نشست" +workbenchTitle = "مدیریت نشست" + +[certSign.collab.signRequest] +addedToFiles = "سند به ÙØ§ÛŒÙ„‌های ÙØ¹Ø§Ù„ Ø§ÙØ²ÙˆØ¯Ù‡ شد" +addSignature = "Ø§ÙØ²ÙˆØ¯Ù† امضای شما" +addToFiles = "Ø§ÙØ²ÙˆØ¯Ù† به ÙØ§ÛŒÙ„‌های ÙØ¹Ø§Ù„" +advancedSettings = "تنظیمات Ù¾ÛŒØ´Ø±ÙØªÙ‡" +backToList = "بازگشت به درخواست‌های امضا" +certificateChoice = "یک گواهی برای امضا انتخاب کنید" +changeSignature = "تغییر امضا" +clearSignature = "پاک کردن امضا" +completeAndSign = "تکمیل Ùˆ امضا" +createNewSignature = "ایجاد امضای جدید" +declineButton = "رد" +decline = "رد درخواست" +deleteSelected = "حذ٠امضای انتخاب‌شده" +drawSignature = "امضای خود را در پایین رسم کنید" +dueDate = "تاریخ سررسید" +fileTooLarge = "اندازه ÙØ§ÛŒÙ„ باید کمتر از 5MB باشد" +fontFamily = "خانواده Ùونت" +fontSize = "اندازه Ùونت: {{size}}px" +fontSizePlaceholder = "اندازه" +from = "از" +invalidCertFile = "Ù„Ø·ÙØ§Ù‹ یک ÙØ§ÛŒÙ„ گواهی P12 یا PFX انتخاب کنید" +invalidFileType = "Ù„Ø·ÙØ§Ù‹ یک ÙØ§ÛŒÙ„ تصویر انتخاب کنید" +location = "مکان (اختیاری)" +locationPlaceholder = "از کجا امضا می‌کنید؟" +message = "پیام" +noCertificate = "Ù„Ø·ÙØ§Ù‹ یک ÙØ§ÛŒÙ„ گواهی انتخاب کنید" +noSignatures = "Ù„Ø·ÙØ§Ù‹ حداقل یک امضا روی PDF قرار دهید" +p12File = "ÙØ§ÛŒÙ„ گواهی P12/PFX" +password = "گذرواژه گواهی" +passwordPlaceholder = "گذرواژه را وارد کنید..." +penColor = "رنگ قلم" +penSize = "اندازه قلم: {{size}}px" +placementActive = "برای قرار دادن روی PDF کلیک کنید" +placeSignatureButton = "قرار دادن امضا روی PDF" +reason = "دلیل (اختیاری)" +reasonPlaceholder = "چرا امضا می‌کنید؟" +removeImage = "حذ٠تصویر" +removeCertFile = "Ø­Ø°Ù ÙØ§ÛŒÙ„" +savedSignatures = "امضاهای ذخیره‌شده" +selectFile = "انتخاب ÙØ§ÛŒÙ„ تصویر" +selectSignatureTitle = "انتخاب یا ایجاد امضا" +signButton = "امضای سند" +signatureInfo = "این تنظیمات توسط مالک سند پیکربندی شده است" +signaturePlaced = "امضا روی ØµÙØ­Ù‡ قرار Ú¯Ø±ÙØª" +signatureSettings = "تنظیمات امضا" +signatureText = "متن امضا" +signatureTextPlaceholder = "نام خود را وارد کنید..." +signatureTypeLabel = "نوع امضا" +signingTitle = "امضا" +textColor = "رنگ متن" +typeSignature = "برای ایجاد امضا نام خود را تایپ کنید" +uploadCert = "گواهی Ø³ÙØ§Ø±Ø´ÛŒ" +uploadCertDesc = "از گواهی P12/PFX خود Ø§Ø³ØªÙØ§Ø¯Ù‡ کنید" +uploadSignature = "بارگذاری تصویر امضای خود" +usePersonalCert = "گواهی شخصی" +usePersonalCertDesc = "به‌صورت خودکار برای حساب شما ایجاد می‌شود" +useServerCert = "گواهی سازمان" +useServerCertDesc = "گواهی مشترک سازمان" +workbenchTitle = "درخواست امضا" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "انتخاب رنگ قلم" +continue = "ادامه" + +[certSign.collab.signRequest.certModal] +description = "شما {{count}} امضا قرار داده‌اید. برای تکمیل امضا گواهی خود را انتخاب کنید." +sign = "امضای سند" +certValidating = "در حال اعتبارسنجی گواهی..." +certValidUntil = "گواهی تا {{date}} معتبر است" +certInvalid = "گواهی نامعتبر است: {{error}}" +certInvalidFallback = "گواهی نامعتبر" +certNetworkError = "امکان اعتبارسنجی گواهی نبود" +title = "پیکربندی گواهی" + +[certSign.collab.signRequest.image] +hint = "یک تصویر PNG یا JPG از امضای خود بارگذاری کنید" + +[certSign.collab.signRequest.mode] +move = "جابه‌جایی امضا" +place = "قرار دادن امضا" +title = "حالت امضا یا جابه‌جایی" + +[certSign.collab.signRequest.modeTabs] +draw = "رسم" +image = "بارگذاری" +text = "تایپ" + +[certSign.collab.signRequest.placeSignature] +message = "برای قرار دادن امضا روی PDF کلیک کنید" +title = "قرار دادن امضا" + +[certSign.collab.signRequest.preview] +imageAlt = "امضای انتخاب‌شده" +missing = "بدون پیش‌نمایش" +textFallback = "امضا" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "امضای ترسیمی" +defaultImageLabel = "امضای بارگذاری‌شده" +defaultLabel = "امضا" +defaultTextLabel = "امضای تایپی" +delete = "حذ٠امضا" +none = "هیچ امضای ذخیره‌شده‌ای نیست" + +[certSign.collab.signRequest.signatureType] +draw = "رسم" +type = "تایپ" +upload = "بارگذاری" + +[certSign.collab.signRequest.steps] +back = "بازگشت" +cancelPlacement = "لغو قراردهی" +certificate = "گواهی" +clickMultipleTimes = "برای قرار دادن چند امضا چندبار روی PDF کلیک کنید. برای جابه‌جایی یا تغییر اندازه، هر امضا را بکشید." +clickToPlace = "روی PDF در جایی Ú©Ù‡ می‌خواهید امضای شما ظاهر شود کلیک کنید." +continue = "ادامه به انتخاب گواهی" +continueToPlacement = "ادامه به قراردهی" +continueToReview = "ادامه به بازبینی" +createSignature = "ایجاد امضا" +invisible = "نامرئی" +location = "مکان:" +multipleSignatures = "{{count}} امضا به PDF اعمال خواهد شد" +oneSignature = "Û± امضا به PDF اعمال خواهد شد" +placeOnPdf = "قرار دادن روی PDF" +reason = "دلیل:" +reviewTitle = "بازبینی قبل از امضا" +signaturePlaced = "امضا در ØµÙØ­Ù‡ {{page}} قرار Ú¯Ø±ÙØª. می‌توانید با دوباره کلیک کردن موقعیت را تنظیم کنید یا به بازبینی ادامه دهید." +visible = "مرئی" +visibility = "قابلیت مشاهده:" +yourSignatures = "امضاهای شما ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "رنگ" +fontLabel = "Ùونت" +fontSizeLabel = "اندازه" +fontSizePlaceholder = "16" +label = "متن امضا" +modalHint = "نام خود را وارد کنید Ùˆ سپس برای قرار دادن روی PDF روی «ادامه» کلیک کنید." +placeholder = "نام خود را وارد کنید..." + +[certSign.collab.participant] +certValidating = "در حال اعتبارسنجی گواهی..." +certValid = "✓ گواهی معتبر است" +certValidUntil = " تا {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "گواهی نامعتبر" +certNetworkError = "امکان اعتبارسنجی گواهی نبود" + +[certSign.collab.addParticipants] +add = "Ø§ÙØ²ÙˆØ¯Ù† {{count}} شرکت‌کننده" +back = "بازگشت" +configureSignatures = "پیکربندی تنظیمات امضا" +continue = "ادامه به تنظیمات امضا" +reasonHelp = "برای این شرکت‌کنندگان یک دلیل امضا از پیش تعیین کنید (اختیاری؛ هنگام امضا می‌توانند آن را تغییر دهند)" +reasonPlaceholder = "مثلاً: تأیید، بازبینی..." +selectUsers = "انتخاب کاربران" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Ø§ÙØ²ÙˆØ¯Ù† ØµÙØ­Ù‡ خلاصه امضا" +includeSummaryPageHelp = "یک ØµÙØ­Ù‡ خلاصه در انتها با تمام ÙØ±Ø§Ø¯Ø§Ø¯Ù‡â€ŒÙ‡Ø§ÛŒ امضا Ø§ÙØ²ÙˆØ¯Ù‡ می‌شود. جعبه‌های امضای گواهی دیجیتال در ØµÙØ­Ø§Øª جداگانه مخÙÛŒ خواهند شد (امضاهای دستی بی‌تأثیر هستند)." + +[certSign.collab.sessionList] +active = "ÙØ¹Ø§Ù„" +finalized = "نهایی‌شده" + +[certSign.collab.signatureSettings] +description = "چگونگی نمایش امضاها برای همه شرکت‌کنندگان را پیکربندی کنید" +title = "ظاهر امضا" + +[certSign.collab.userSelector] +inviteUsers = "Ø§ÙØ²ÙˆØ¯Ù† کاربران" +loadError = "بارگذاری کاربران ناموÙÙ‚ بود" +noTeam = "بدون تیم" +noUsers = "کاربر دیگری ÛŒØ§ÙØª نشد." +placeholder = "کاربران را انتخاب کنید..." + +[certSign.mobile] +panelActions = "اقدامات" +panelDocument = "سند" +panelPeople = "Ø§ÙØ±Ø§Ø¯" + +[certSign.sessions] +deleted = "نشست حذ٠شد" +fetchFailed = "بارگذاری جزئیات نشست ناموÙÙ‚ بود" +finalized = "نشست نهایی شد" +loaded = "PDF امضاشده بارگذاری شد" +pdfNotReady = "PDF آماده نیست" +pdfNotReadyDesc = "PDF امضاشده در حال تولید است. Ù„Ø·ÙØ§Ù‹ لحظه‌ای دیگر دوباره تلاش کنید." + +[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 Ø³ÙØ§Ø±Ø´ÛŒ" + [changeCreds] changePassword = "شما از مشخصات Ù¾ÛŒØ´â€ŒÙØ±Ø¶ ورود Ø§Ø³ØªÙØ§Ø¯Ù‡ می‌کنید. Ù„Ø·ÙØ§Ù‹ یک رمز عبور جدید وارد کنید" changeUsername = "نام کاربری خود را به‌روزرسانی کنید. پس از به‌روزرسانی از حساب خارج خواهید شد." @@ -3242,6 +3531,46 @@ totalSelected = "مجموع انتخاب‌شده" unsupported = "پشتیبانی‌نشده" unzip = "استخراج" uploadError = "بارگذاری برخی ÙØ§ÛŒÙ„‌ها ناموÙÙ‚ بود." +copyCreated = "رونوشت در این دستگاه ذخیره شد." +copyFailed = "امکان ایجاد رونوشت نبود." +leaveShare = "حذ٠از Ùهرست من" +leaveShareFailed = "امکان Ø­Ø°Ù ÙØ§ÛŒÙ„ مشترک نبود." +leaveShareSuccess = "از Ùهرست اشتراکی شما حذ٠شد." +removeBoth = "حذ٠از هر دو" +removeFilePrompt = "این ÙØ§ÛŒÙ„ در این دستگاه Ùˆ روی سرور شما ذخیره شده است. مایلید آن را از کجا حذ٠کنید؟" +removeFileTitle = "Ø­Ø°Ù ÙØ§ÛŒÙ„" +removeLocalOnly = "Ùقط این دستگاه" +removeServerFailed = "امکان Ø­Ø°Ù ÙØ§ÛŒÙ„ از سرور نبود." +removeServerOnly = "Ùقط سرور" +removeServerOnlyPrompt = "این ÙØ§ÛŒÙ„ Ùقط روی سرور شما ذخیره شده است. مایلید آن را از سرور حذ٠کنید؟" +removeServerSuccess = "از سرور حذ٠شد." +removeSharedPrompt = "این ÙØ§ÛŒÙ„ با شما به اشتراک گذاشته شده است. می‌توانید آن را از این دستگاه یا از Ùهرست اشتراکی خود حذ٠کنید." +removeSharedServerOnlyBlockedPrompt = "این ÙØ§ÛŒÙ„ با شما به اشتراک گذاشته شده Ùˆ Ùقط روی سرور ذخیره شده است." +removeSharedServerOnlyPrompt = "این ÙØ§ÛŒÙ„ با شما به اشتراک گذاشته شده Ùˆ Ùقط روی سرور ذخیره شده است. آن را از Ùهرست خود حذ٠می‌کنید؟" +changesNotUploaded = "تغییرات بارگذاری نشده‌اند" +cloudFile = "ÙØ§ÛŒÙ„ ابری" +filterAll = "همه" +filterLocal = "محلی" +filterSharedByMe = "اشتراک‌گذاری‌شده توسط من" +filterSharedWithMe = "اشتراک‌گذاری‌شده با من" +lastSynced = "آخرین همگام‌سازی" +localOnly = "Ùقط محلی" +makeCopy = "ایجاد رونوشت" +owner = "مالک" +ownerUnknown = "نامشخص" +share = "اشتراک‌گذاری" +shareSelected = "اشتراک‌گذاری موارد انتخاب‌شده" +sharedByYou = "اشتراک‌گذاری‌شده توسط شما" +sharedEditNoticeBody = "شما حق ویرایش نسخه سروری این ÙØ§ÛŒÙ„ را ندارید. هر ویرایشی انجام دهید به‌صورت یک نسخه محلی ذخیره خواهد شد." +sharedEditNoticeConfirm = "متوجه شدم" +sharedEditNoticeTitle = "نسخه Ùقط‌خواندنی روی سرور" +sharedWithYou = "با شما به اشتراک گذاشته شده" +sharing = "اشتراک‌گذاری" +storageState = "ذخیره‌سازی" +synced = "همگام شد" +updateOnServer = "به‌روزرسانی روی سرور" +uploadSelected = "بارگذاری موارد انتخاب‌شده" +uploadToServer = "بارگذاری روی سرور" [files] addFiles = "Ø§ÙØ²ÙˆØ¯Ù† ÙØ§ÛŒÙ„‌ها" @@ -3367,6 +3696,77 @@ title = "درباره تخت‌سازی PDFها" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "درباره امضای گروهی" + +[groupSigning.tooltip.finalization] +bullet1 = "تمام امضاها به‌ترتیبی Ú©Ù‡ مشخص کرده‌اید اعمال می‌شوند" +bullet2 = "در صورت نیاز می‌توانید با امضاهای ناقص نهایی‌سازی کنید" +bullet3 = "پس از نهایی‌سازی، نشست قابل تغییر نیست" +description = "پس از اینکه همه شرکت‌کنندگان امضا کردند (یا اگر تصمیم بگیرید زودتر نهایی‌سازی کنید)ØŒ می‌توانید PDF نهایی امضاشده را تولید کنید." +title = "ÙØ±Ø¢ÛŒÙ†Ø¯ نهایی‌سازی" + +[groupSigning.tooltip.roles] +bullet1 = "مالک (شما): ایجاد نشست، پیکربندی Ù¾ÛŒØ´â€ŒÙØ±Ø¶â€ŒÙ‡Ø§ÛŒ امضا، نهایی‌سازی سند" +bullet2 = "شرکت‌کنندگان: ایجاد امضای خود، انتخاب گواهی، قرار دادن روی PDF" +bullet3 = "شرکت‌کنندگان نمی‌توانند تنظیمات قابلیت مشاهده، دلیل یا مکان امضا را تغییر دهند" +description = "شما تنظیمات ظاهر امضا را برای همه شرکت‌کنندگان کنترل می‌کنید." +title = "نقش‌های شرکت‌کنندگان" + +[groupSigning.tooltip.sequential] +bullet1 = "شرکت‌کننده اول باید امضا کند تا Ù†ÙØ± دوم به سند دسترسی یابد" +bullet2 = "ترتیب صحیح امضا برای انطباق حقوقی را تضمین می‌کند" +bullet3 = "می‌توانید با کشیدن در Ùهرست، ترتیب شرکت‌کنندگان را تغییر دهید" +description = "شرکت‌کنندگان به ترتیبی Ú©Ù‡ مشخص می‌کنید اسناد را امضا می‌کنند. هر امضاکننده هنگامی Ú©Ù‡ نوبتش شود اعلان Ø¯Ø±ÛŒØ§ÙØª می‌کند." +title = "امضای ترتیبی" + +[groupSigning.steps] +back = "بازگشت" +completed = "تکمیل شد" +current = "جاری" +stepLabel = "گام {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "ادامه به بازبینی" +invisible = "امضاها نامرئی خواهند بود (Ùقط ÙØ±Ø§Ø¯Ø§Ø¯Ù‡)" +locationLabel = "مکان:" +preview = "پیش‌نمایش" +reasonLabel = "دلیل:" +title = "پیکربندی تنظیمات امضا" +visible = "امضاها روی ØµÙØ­Ù‡ {{page}} مرئی خواهند بود" + +[groupSigning.steps.review] +document = "سند" +dueDate = "تاریخ سررسید (اختیاری)" +dueDatePlaceholder = "تاریخ سررسید را انتخاب کنید..." +invisible = "نامرئی (Ùقط ÙØ±Ø§Ø¯Ø§Ø¯Ù‡)" +location = "مکان:" +logo = "لوگو:" +logoHidden = "بدون لوگو" +logoShown = "لوگوی Stirling PDF نمایش داده می‌شود" +participants = "شرکت‌کنندگان" +reason = "دلیل:" +send = "ارسال درخواست‌های امضا" +signatureSettings = "تنظیمات امضا" +title = "بازبینی جزئیات نشست" +titleShort = "بازبینی Ùˆ ارسال" +visibility = "قابلیت مشاهده:" +visible = "روی ØµÙØ­Ù‡ {{page}} مرئی" +participantCount = "{{count}} شرکت‌کننده به ترتیب امضا خواهند کرد" + +[groupSigning.steps.selectDocument] +continue = "ادامه به انتخاب شرکت‌کنندگان" +noFile = "Ù„Ø·ÙØ§Ù‹ برای ایجاد یک نشست امضا یک ÙØ§ÛŒÙ„ PDF واحد از ÙØ§ÛŒÙ„‌های ÙØ¹Ø§Ù„ خود انتخاب کنید." +selectedFile = "سند انتخاب‌شده" +title = "انتخاب سند" + +[groupSigning.steps.selectParticipants] +continue = "ادامه به تنظیمات امضا" +count = "{{count}} شرکت‌کننده انتخاب شد" +label = "انتخاب شرکت‌کنندگان" +placeholder = "شرکت‌کنندگان را برای امضا انتخاب کنید..." +title = "انتخاب شرکت‌کنندگان" + [getPdfInfo] downloadJson = "دانلود JSON" downloads = "دانلودها" @@ -4460,7 +4860,10 @@ zoomOut = "کوچک‌نمایی" [viewer] cannotPreviewFile = "امکان پیش‌نمایش ÙØ§ÛŒÙ„ نیست" +disableColorFilter = "ØºÛŒØ±ÙØ¹Ø§Ù„ کردن Ùیلتر رنگ" dualPageView = "نمای Ø¯ÙˆØµÙØ­Ù‡â€ŒØ§ÛŒ" +enableDarkFilter = "ÙØ¹Ø§Ù„‌سازی Ùیلتر تیره" +enableSepiaFilter = "ÙØ¹Ø§Ù„‌سازی Ùیلتر سپیا" firstPage = "ØµÙØ­Ù‡ نخست" lastPage = "ØµÙØ­Ù‡ آخر" nextPage = "ØµÙØ­Ù‡ بعد" @@ -4470,6 +4873,22 @@ singlePageView = "نمای ØªÚ©â€ŒØµÙØ­Ù‡â€ŒØ§ÛŒ" unknownFile = "ÙØ§ÛŒÙ„ ناشناخته" zoomIn = "بزرگ‌نمایی" zoomOut = "کوچک‌نمایی" +resetZoom = "بازنشانی زوم" + +[viewer.nonPdf] +fileTypeBadge = "ÙØ§ÛŒÙ„ {{type}}" +convertToPdf = "تبدیل به PDF" +loading = "در حال بارگذاری..." +emptyFile = "ÙØ§ÛŒÙ„ خالی" +csvStats = "{{rows}} ردی٠· {{columns}} ستون · {{size}}" +sortedBy = "مرتب‌شده بر اساس: {{column}}" +columnDefault = "ستون {{index}}" +htmlPreviewWarning = "پیش‌نمایش HTML — ممکن است منابع خارجی بارگذاری نشوند · {{size}}" +htmlPreview = "پیش‌نمایش HTML" +invalidJson = "JSON نامعتبر — محتوای خام نمایش داده می‌شود" +textStats = "{{lines}} خط · {{size}}" +lineNumbers = "شماره خطوط" +renderMarkdown = "رندر Markdown" [viewer.attachments] title = "پیوست‌ها" @@ -4531,6 +4950,7 @@ toggleAttachments = "نمایش/پنهان‌کردن پیوست‌ها" toggleTheme = "تغییر تم" language = "زبان" toggleAnnotations = "تغییر وضعیت نمایش حاشیه‌نویسی‌ها" +toggleLayers = "تغییر وضعیت لایه‌ها" search = "جستجوی PDF" panMode = "حالت پیمایش" applyRedactionsFirst = "ابتدا سانسورها را اعمال کنید" @@ -5407,20 +5827,72 @@ title = "چاپ ÙØ§ÛŒÙ„" 2 = "نام چاپگر را وارد کنید" [quickAccess] +access = "دسترسی" +accessAddPerson = "Ø§ÙØ²ÙˆØ¯Ù† یک Ù†ÙØ± دیگر" +accessBack = "بازگشت" +accessCopyLink = "Ú©Ù¾ÛŒ لینک" +accessEmail = "نشانی ایمیل" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "ÙØ§ÛŒÙ„" +accessGeneral = "دسترسی عمومی" +accessInviteTitle = "دعوت Ø§ÙØ±Ø§Ø¯" +accessOwner = "مالک" +accessPanel = "دسترسی به سند" +accessPeople = "Ø§ÙØ±Ø§Ø¯ÛŒ Ú©Ù‡ دسترسی دارند" +accessRemove = "حذÙ" +accessRestricted = "محدود" +accessRestrictedHint = "Ùقط Ø§ÙØ±Ø§Ø¯ÛŒ Ú©Ù‡ دسترسی دارند می‌توانند باز کنند" +accessRole = "نقش" +accessRoleCommenter = "نظردهنده" +accessRoleEditor = "ویرایشگر" +accessRoleViewer = "بیننده" +accessSelectedFile = "ÙØ§ÛŒÙ„ انتخاب‌شده" +accessSendInvite = "ارسال دعوت" +accessTitle = "دسترسی به سند" +accessYou = "شما" account = "حساب" +activeSessions = "نشست‌های ÙØ¹Ø§Ù„" +activeTab = "ÙØ¹Ø§Ù„" activity = "ÙØ¹Ø§Ù„یت" adminSettings = "تنظیمات مدیر" +allSessions = "همه نشست‌ها" allTools = "All Tools" automate = "اتوماسیون" +back = "بازگشت" +certSign = "امضای با گواهی" +completedSessions = "نشست‌های تکمیل‌شده" +completedTab = "تکمیل‌شده" config = "پیکربندی" +createNew = "ایجاد درخواست جدید" +createSession = "ایجاد درخواست امضا" +dueDate = "تاریخ سررسید (اختیاری)" files = "ÙØ§ÛŒÙ„‌ها" help = "راهنما" +noActiveSessions = "هیچ درخواست امضای در انتظار یا نشست ÙØ¹Ø§Ù„ÛŒ نیست" +noCompletedSessions = "هیچ نشستی تکمیل نشده است" +noFile = "هیچ ÙØ§ÛŒÙ„ÛŒ انتخاب نشده است" read = "خواندن" reader = "نمایشگر" +refresh = "تازه‌سازی" +requestSignatures = "درخواست امضاها" +selectSingleFileToRequest = "برای درخواست امضاها یک ÙØ§ÛŒÙ„ PDF واحد انتخاب کنید" +selectedFile = "ÙØ§ÛŒÙ„ انتخاب‌شده" +selectUsers = "کاربران را برای امضا انتخاب کنید" +selectUsersPlaceholder = "شرکت‌کنندگان را انتخاب کنید..." +sendingRequest = "در حال ارسال..." settings = "تنظیمات" showMeAround = "راهنمایی‌ام Ú©Ù†" sign = "امضا" +signatureRequests = "درخواست‌های امضا" +signYourself = "خودتان امضا کنید" +newRequest = "درخواست جدید" tours = "تورها" +wetSign = "Ø§ÙØ²ÙˆØ¯Ù† امضای دستی" +filterMine = "مال من" +filterOverdue = "گذشته از موعد" +filterSigned = "امضاشده" +filterDeclined = "رد شده" +searchDocuments = "جستجوی اسناد…" [quickAccess.helpMenu] adminTour = "تور مدیریت" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "سرور Stirling-PDF شما Ø¢Ùلاین است Ùˆ \ expired = "نشست شما به پایان رسیده است. Ù„Ø·ÙØ§Ù‹ ØµÙØ­Ù‡ را تازه‌سازی کرده Ùˆ دوباره تلاش کنید." refreshPage = "تازه‌سازی ØµÙØ­Ù‡" +[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 = "ترتیب امضا" + +[signatureSettings.tooltip] +header = "تنظیمات ظاهر امضا" + +[signatureSettings.tooltip.location] +bullet1 = "نمونه‌ها: \"New York, USA\"ØŒ \"London Office\"ØŒ \"Remote\"" +bullet2 = "با موقعیت ØµÙØ­Ù‡ یکسان نیست" +bullet3 = "ممکن است برای برخی حوزه‌های قضایی لازم باشد" +description = "مکان جغراÙیایی اختیاری Ú©Ù‡ امضا در آن اعمال شده است. در ÙØ±Ø§Ø¯Ø§Ø¯Ù‡ گواهی ذخیره می‌شود." +title = "مکان امضا" + +[signatureSettings.tooltip.logo] +bullet1 = "در کنار امضا Ùˆ متن نمایش داده می‌شود" +bullet2 = "از ÙØ±Ù…ت‌های PNGØŒ JPG پشتیبانی می‌کند" +bullet3 = "ظاهر حرÙه‌ای را ارتقا می‌دهد" +description = "برای برندسازی Ùˆ اصالت، یک لوگوی شرکت به امضاهای مرئی اضاÙÙ‡ کنید." +title = "لوگوی شرکت" + +[signatureSettings.tooltip.reason] +bullet1 = "نمونه‌ها: \"Approval\"ØŒ \"Contract Agreement\"ØŒ \"Review Complete\"" +bullet2 = "در ویژگی‌های امضای PDF قابل مشاهده است" +bullet3 = "برای مسیرهای ممیزی Ùˆ انطباق Ù…Ùید است" +description = "متن اختیاری Ú©Ù‡ توضیح می‌دهد چرا سند امضا می‌شود. در ÙØ±Ø§Ø¯Ø§Ø¯Ù‡ گواهی ذخیره می‌شود." +title = "دلیل امضا" + +[signatureSettings.tooltip.visibility] +bullet1 = "مرئی: امضا با ظاهر Ø³ÙØ§Ø±Ø´ÛŒ روی PDF نمایش داده می‌شود" +bullet2 = "نامرئی: گواهی بدون نشانه بصری جاسازی می‌شود" +bullet3 = "امضاهای نامرئی همچنان اعتبارسنجی رمزنگاری‌شده ÙØ±Ø§Ù‡Ù… می‌کنند" +description = "کنترل می‌کند امضا روی سند مرئی باشد یا نامرئی جاسازی شود." +title = "قابلیت مشاهده امضا" + [settings.configuration] advanced = "Ù¾ÛŒØ´Ø±ÙØªÙ‡" database = "پایگاه داده" endpoints = "نقاط پایانی" features = "قابلیت‌ها" +storageSharing = "ذخیره‌سازی ÙØ§ÛŒÙ„ Ùˆ اشتراک‌گذاری" systemSettings = "تنظیمات سیستم" title = "پیکربندی" @@ -6332,10 +6868,13 @@ title = "ورود به Stirling" [setup.selfhosted] link = "یا به یک حساب خودمیزبان متصل شوید" subtitle = "اطلاعات کاربری سرور خود را وارد کنید" +changeServerLocked = "سازمان شما این برنامه را به یک سرور مشخص محدود کرده است" switchToLocal = "Ø§Ø³ØªÙØ§Ø¯Ù‡ از ابزارهای محلی به‌جای آن" title = "ورود به سرور" [setup.selfhosted.unreachable] +changeServer = "اتصال به سرور دیگر" +changeServerLocked = "سازمان شما این برنامه را به یک سرور مشخص محدود کرده است" continueOffline = "Ø§Ø³ØªÙØ§Ø¯Ù‡ از ابزارهای محلی به‌جای آن" message = "امکان دسترسی به {{url}} نبود. بررسی کنید سرور در حال اجرا Ùˆ قابل دسترسی باشد." retry = "تلاش مجدد" @@ -6529,6 +7068,15 @@ saved = "ذخیره‌شده" text = "متن" title = "نوع امضا" +[signRequest] +declined = "درخواست امضا رد شد" +fetchFailed = "بارگذاری درخواست امضا ناموÙÙ‚ بود" +signed = "سند با موÙقیت امضا شد" + +[signSession] +createFailed = "ایجاد درخواست امضا ناموÙÙ‚ بود" +created = "درخواست امضا ارسال شد" + [signup] accountCreatedSuccessfully = "حساب با موÙقیت ایجاد شد! اکنون می‌توانید وارد شوید." alreadyHaveAccount = "از قبل حساب دارید؟ وارد شوید" @@ -6807,6 +7355,106 @@ title = "تقسیم PDF بر اساس ÙØµÙ„‌ها" [splitPdfByChapters] tags = "تقسیم، ÙØµÙ„‌ها، نشانه‌گذاری، سازماندهی" +[storageShare] +accessed = "دسترسی انجام شد" +accessDenied = "شما به این ÙØ§ÛŒÙ„ مشترک دسترسی ندارید. از مالک بخواهید آن را با شما به اشتراک بگذارد." +accessFailed = "امکان بارگذاری ÙØ¹Ø§Ù„یت نبود." +accessDeniedBody = "شما به این ÙØ§ÛŒÙ„ دسترسی ندارید. از مالک بخواهید آن را با شما به اشتراک بگذارد." +accessDeniedTitle = "بدون دسترسی" +accessLimitedCommenter = "دسترسی نظر دادن به‌زودی اضاÙÙ‡ می‌شود. اگر نیاز به دانلود دارید از مالک دسترسی ویرایشگر بخواهید." +accessLimitedTitle = "دسترسی محدود" +accessLimitedViewer = "این لینک Ùقط قابل مشاهده است. اگر نیاز به دانلود دارید از مالک دسترسی ویرایشگر بخواهید." +createdAt = "ایجاد شد" +download = "دانلود" +downloadFailed = "امکان دانلود این ÙØ§ÛŒÙ„ نبود." +expiredBody = "این لینک اشتراک نامعتبر است یا منقضی شده." +expiredTitle = "لینک منقضی شده است" +goToLogin = "Ø±ÙØªÙ† به ورود" +loadFailed = "امکان باز کردن ÙØ§ÛŒÙ„ مشترک نبود." +loading = "در حال بارگذاری لینک اشتراک..." +loginPrompt = "برای دسترسی به این ÙØ§ÛŒÙ„ مشترک وارد شوید." +loginRequired = "ورود لازم است" +openInApp = "باز کردن در Stirling PDF" +ownerLabel = "مالک" +ownerUnknown = "نامشخص" +requiresLogin = "این ÙØ§ÛŒÙ„ مشترک نیاز به ورود دارد." +roleCommenter = "نظردهنده" +roleEditor = "ویرایشگر" +roleViewer = "بیننده" +shareHeading = "ÙØ§ÛŒÙ„ اشتراک‌گذاری‌شده" +titleDefault = "ÙØ§ÛŒÙ„ اشتراک‌گذاری‌شده" +tryAgain = "Ù„Ø·ÙØ§Ù‹ بعداً دوباره تلاش کنید." +addUser = "Ø§ÙØ²ÙˆØ¯Ù†" +commenterHint = "امکان نظر دادن به‌زودی ÙØ±Ø§Ù‡Ù… می‌شود." +copied = "لینک در کلیپ‌بورد Ú©Ù¾ÛŒ شد" +copy = "Ú©Ù¾ÛŒ" +copyFailed = "Ú©Ù¾ÛŒ ناموÙÙ‚ بود" +description = "برای این ÙØ§ÛŒÙ„ یک لینک اشتراک بسازید. کاربران واردشده با داشتن لینک می‌توانند به آن دسترسی داشته باشند." +downloadsCount = "دانلودها: {{count}}" +emailWarningBody = "به نظر می‌رسد این یک نشانی ایمیل است. اگر این ÙØ±Ø¯ کاربر Stirling PDF نباشد، به ÙØ§ÛŒÙ„ دسترسی نخواهد داشت." +emailWarningConfirm = "با این حال به اشتراک بگذار" +emailWarningTitle = "نشانی ایمیل" +errorTitle = "اشتراک ناموÙÙ‚ بود" +failure = "امکان ایجاد لینک اشتراک نبود. Ù„Ø·ÙØ§Ù‹ دوباره تلاش کنید." +fileLabel = "ÙØ§ÛŒÙ„" +generate = "تولید لینک" +generated = "لینک اشتراک تولید شد" +hideActivity = "پنهان کردن ÙØ¹Ø§Ù„یت" +invalidUsername = "یک نام کاربری یا نشانی ایمیل معتبر وارد کنید." +lastAccessed = "آخرین دسترسی" +linkAccessTitle = "دسترسی لینک اشتراک" +linkLabel = "لینک اشتراک" +linksDisabled = "لینک‌های اشتراک ØºÛŒØ±ÙØ¹Ø§Ù„ هستند." +linksDisabledBody = "لینک‌های اشتراک توسط تنظیمات سرور شما ØºÛŒØ±ÙØ¹Ø§Ù„ شده‌اند." +manage = "مدیریت اشتراک‌گذاری" +manageDescription = "ایجاد Ùˆ مدیریت لینک‌های اشتراک این ÙØ§ÛŒÙ„." +manageLoadFailed = "امکان بارگذاری لینک‌های اشتراک نبود." +manageTitle = "مدیریت اشتراک‌گذاری" +noActivity = "هنوز هیچ ÙØ¹Ø§Ù„یتی نیست." +noLinks = "هنوز لینک اشتراکی ÙØ¹Ø§Ù„ÛŒ نیست." +noSharedUsers = "هنوز هیچ کاربری دسترسی ندارد." +removeLink = "حذ٠لینک" +removeUser = "حذÙ" +revokeFailed = "امکان حذ٠لینک اشتراک نبود." +revoked = "لینک اشتراک حذ٠شد" +roleLabel = "نقش" +sharingDisabled = "اشتراک‌گذاری ØºÛŒØ±ÙØ¹Ø§Ù„ است." +sharingDisabledBody = "اشتراک‌گذاری توسط تنظیمات سرور شما ØºÛŒØ±ÙØ¹Ø§Ù„ شده است." +sharedUsersTitle = "کاربران٠دارای دسترسی" +title = "اشتراک‌گذاری ÙØ§ÛŒÙ„" +unknownUser = "کاربر ناشناخته" +userAddFailed = "اشتراک‌گذاری با آن کاربر ممکن نیست." +userAdded = "کاربر به Ùهرست اشتراک Ø§ÙØ²ÙˆØ¯Ù‡ شد." +usernameLabel = "نام کاربری یا ایمیل" +usernamePlaceholder = "نام کاربری یا ایمیل را وارد کنید" +userRemoveFailed = "حذ٠آن کاربر ممکن نیست." +userRemoved = "کاربر از Ùهرست اشتراک حذ٠شد." +viewActivity = "مشاهده ÙØ¹Ø§Ù„یت" +viewed = "مشاهده‌شده" +viewsCount = "بازدیدها: {{count}}" +downloaded = "دانلود‌شده" +bulkDescription = "یک لینک برای اشتراک همه ÙØ§ÛŒÙ„‌های انتخاب‌شده با کاربران٠واردشده بسازید." +bulkTitle = "اشتراک‌گذاری ÙØ§ÛŒÙ„‌های انتخاب‌شده" +copyLink = "Ú©Ù¾ÛŒ لینک اشتراک" +fileCount = "{{count}} ÙØ§ÛŒÙ„ انتخاب‌شده" +ownerOnly = "Ùقط مالک می‌تواند اشتراک‌گذاری را مدیریت کند." +selectSingleFile = "برای مدیریت اشتراک‌گذاری، تنها یک ÙØ§ÛŒÙ„ را انتخاب کنید." + +[storageUpload] +description = "این کار ÙØ§ÛŒÙ„ ÙØ¹Ù„ÛŒ را برای دسترسی خودتان در ÙØ¶Ø§ÛŒ ذخیره‌سازی سرور بارگذاری می‌کند." +errorTitle = "بارگذاری ناموÙÙ‚ بود" +failure = "بارگذاری ناموÙÙ‚ بود. Ù„Ø·ÙØ§Ù‹ ورود Ùˆ تنظیمات ذخیره‌سازی خود را بررسی کنید." +fileLabel = "ÙØ§ÛŒÙ„" +hint = "لینک‌های عمومی Ùˆ حالت‌های دسترسی توسط تنظیمات سرور شما کنترل می‌شوند." +success = "روی سرور بارگذاری شد" +title = "بارگذاری در سرور" +updateButton = "به‌روزرسانی روی سرور" +uploadButton = "بارگذاری در سرور" +bulkDescription = "این کار ÙØ§ÛŒÙ„‌های انتخاب‌شده را در ÙØ¶Ø§ÛŒ ذخیره‌سازی سرور شما بارگذاری می‌کند." +bulkTitle = "بارگذاری ÙØ§ÛŒÙ„‌های انتخاب‌شده" +fileCount = "{{count}} ÙØ§ÛŒÙ„ انتخاب‌شده" +more = " +{{count}} مورد دیگر" + [storage] approximateSize = "حجم تقریبی" fileTooLarge = "ÙØ§ÛŒÙ„ خیلی بزرگ است. حداکثر اندازه هر ÙØ§ÛŒÙ„ برابر است با" @@ -7153,6 +7801,30 @@ title = "نمایش/ویرایش PDF" [warning] tooltipTitle = "هشدار" +[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 = "بارگذاری تصویر امضا" + [watermark] completed = "واترمارک Ø§ÙØ²ÙˆØ¯Ù‡ شد" desc = "Ø§ÙØ²ÙˆØ¯Ù† واترمارک متنی یا تصویری به ÙØ§ÛŒÙ„‌های PDF" @@ -7333,6 +8005,7 @@ activeSession = "نشست ÙØ¹Ø§Ù„" addMembers = "Ø§ÙØ²ÙˆØ¯Ù† اعضا" admin = "مدیر" confirmDelete = "مطمئنید می‌خواهید این کاربر را حذ٠کنید؟ این اقدام غیرقابل بازگشت است." +confirmUnlock = "آیا مطمئن هستید می‌خواهید Ù‚ÙÙ„ این حساب کاربری را باز کنید؟" deleteUser = "حذ٠کاربر" deleteUserError = "حذ٠کاربر ناموÙÙ‚ بود" deleteUserSuccess = "کاربر با موÙقیت حذ٠شد" @@ -7341,6 +8014,8 @@ disable = "ØºÛŒØ±ÙØ¹Ø§Ù„‌سازی" disabled = "ØºÛŒØ±ÙØ¹Ø§Ù„" editRole = "ویرایش نقش" enable = "ÙØ¹Ø§Ù„‌سازی" +locked = "Ù‚Ùل‌شده" +lockedBadge = "Ù‚Ùل‌شده" loading = "در حال بارگذاری Ø§ÙØ±Ø§Ø¯..." loginRequired = "ابتدا حالت لاگین را ÙØ¹Ø§Ù„ کنید" member = "عضو" @@ -7350,6 +8025,9 @@ searchMembers = "جستجوی اعضا..." status = "وضعیت" team = "تیم" title = "Ø§ÙØ±Ø§Ø¯" +unlockAccount = "باز کردن Ù‚ÙÙ„ حساب" +unlockUserError = "باز کردن Ù‚ÙÙ„ حساب کاربری ناموÙÙ‚ بود" +unlockUserSuccess = "Ù‚ÙÙ„ حساب کاربری با موÙقیت باز شد" user = "کاربر" [workspace.people.actions] diff --git a/frontend/public/locales/fr-FR/translation.toml b/frontend/public/locales/fr-FR/translation.toml index c30c621ea2..0f612d6fd5 100644 --- a/frontend/public/locales/fr-FR/translation.toml +++ b/frontend/public/locales/fr-FR/translation.toml @@ -8,6 +8,7 @@ black = "Noir" blue = "Bleu" bored = "Marre d'attendre ?" cancel = "Annuler" +confirm = "Confirmer" changedCredsMessage = "Les identifiants ont été mis à jour !" chooseFile = "Choisir un fichier" close = "Fermer" @@ -146,6 +147,7 @@ insufficientCredits = "Insufficient credits. Required: {{requiredCredits}}, Avai loadingCredits = "Checking credits..." loadingProStatus = "Checking subscription status..." noticeTopUpOrPlan = "Not enough credits, please top up or upgrade to a plan" +accessInvite = "Inviter" [account] accountSettings = "Paramètres du compte" @@ -1427,6 +1429,34 @@ title = "Traitement" description = "Temps d’attente maximal d’un job de traitement avant de signaler une erreur." label = "Délai de traitement (secondes)" +[admin.settings.storage] +description = "Contrôler les options de stockage et de partage du serveur." +title = "Stockage et partage de fichiers" + +[admin.settings.storage.enabled] +description = "Autoriser les utilisateurs à stocker des fichiers sur le serveur." +label = "Activer le stockage de fichiers sur le serveur" + +[admin.settings.storage.sharing.email] +description = "Autoriser le partage avec des adresses e-mail." +label = "Activer le partage par e-mail" +mailLink = "Configurer les paramètres de messagerie" +mailNote = "Nécessite une configuration de messagerie. " + +[admin.settings.storage.sharing.enabled] +description = "Autoriser les utilisateurs à partager les fichiers stockés." +label = "Activer le partage" + +[admin.settings.storage.sharing.links] +description = "Autoriser le partage via des liens nécessitant une connexion." +frontendUrlLink = "Configurer dans les paramètres système" +frontendUrlNote = "Nécessite une Frontend URL. " +label = "Activer les liens de partage" + +[admin.settings.storage.signing.enabled] +description = "Autoriser les utilisateurs à créer des sessions de signature multi-participants. Nécessite l’activation du stockage de fichiers sur le serveur." +label = "Activer la signature de groupe (Alpha)" + [admin.settings.unsavedChanges] cancel = "Continuer l’édition" discard = "Ignorer les modifications" @@ -1517,7 +1547,7 @@ editStampHint = "Pour changer l’image, supprimez ce tampon et ajoutez-en un no editSwitchToSelect = "Basculez sur Sélectionner et modifier pour modifier cette annotation." editText = "Modifier la zone de texte" editTextMarkup = "Modifier le marquage de texte" -annotationStyle = "Annotation style" +annotationStyle = "Style d'annotation" ellipse = "Ellipse" exit = "Quitter le mode d’annotation" fillColor = "Couleur de remplissage" @@ -1528,19 +1558,19 @@ highlight = "Surligner" imagePreview = "Aperçu" inkHighlighter = "Surligneur à main levée" line = "Ligne" -lineArrow = "Arrow" +lineArrow = "Flèche" noBackground = "Sans arrière-plan" note = "Note" -comment = "Comment" -comments = "Comments" -insertText = "Insert Text" -replaceText = "Replace Text" +comment = "Commentaire" +comments = "Commentaires" +insertText = "Insérer du texte" +replaceText = "Remplacer le texte" noteIcon = "Icône de note" notesStamps = "Notes et tampons" opacity = "Opacité" pen = "Stylo" polygon = "Polygone" -polyline = "Polyline" +polyline = "Ligne brisée" properties = "Propriétés" rectangle = "Rectangle" redo = "Rétablir" @@ -2059,7 +2089,19 @@ numbers = "Nombres/plages : 5, 10-20" progressions = "Progressions : 3n, 4n+1" [certSign] +allSigned = "Tous les participants ont signé. Prêt à finaliser." +awaitingSignatures = "En attente de signatures" +signatureProgress = "{{signedCount}}/{{totalCount}} signatures" chooseCertificate = "Choisir le fichier de certificat" +declined = "Refusé" +fetchFailed = "Échec du chargement des données de signature" +finalized = "Finalisé" +notified = "En attente" +partialNote = "Vous pouvez finaliser plus tôt avec les signatures actuelles. Les participants non signataires seront exclus." +pending = "En attente" +readyToFinalize = "Prêt à finaliser" +signed = "Signé" +viewed = "Vu" chooseJksFile = "Choisir le fichier JKS" chooseP12File = "Choisir le fichier PKCS12" choosePfxFile = "Choisir le fichier PFX" @@ -2082,6 +2124,7 @@ title = "Signer avec un certificat" invisible = "Invisible" stepTitle = "Apparence de la signature" visible = "Visible" +visibility = "Visibilité" [certSign.appearance.options] title = "Détails de la signature" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Finaliser et charger le PDF signé" +early = "Finaliser avec les signatures actuelles" + +[certSign.collab.sessionDetail] +addButton = "Ajouter des participants" +addParticipants = "Ajouter des participants" +addParticipantsError = "Échec de l’ajout des participants" +backToList = "Retour aux sessions" +deleteConfirm = "Êtes-vous sûr ? Cette action est irréversible." +deleteError = "Échec de la suppression de la session" +deleted = "Session supprimée" +deleteSession = "Supprimer la session" +dueDate = "Date d’échéance" +finalizeError = "Échec de la finalisation de la session" +loadPdfError = "Échec du chargement du PDF signé" +loadSignedPdf = "Charger le PDF signé dans les fichiers actifs" +messageLabel = "Message" +noAdditionalInfo = "Aucune information supplémentaire" +owner = "Propriétaire" +participantRemoved = "Participant supprimé" +participants = "Participants" +participantsAdded = "Participants ajoutés avec succès" +removeParticipant = "Supprimer" +removeParticipantError = "Échec de la suppression du participant" +selectUsers = "Sélectionner des utilisateurs…" +sessionInfo = "Infos de la session" +workbenchTitle = "Gestion des sessions" + +[certSign.collab.signRequest] +addedToFiles = "Document ajouté aux fichiers actifs" +addSignature = "Ajouter votre signature" +addToFiles = "Ajouter aux fichiers actifs" +advancedSettings = "Paramètres avancés" +backToList = "Retour aux demandes de signature" +certificateChoice = "Sélectionnez un certificat pour signer" +changeSignature = "Modifier la signature" +clearSignature = "Effacer la signature" +completeAndSign = "Terminer et signer" +createNewSignature = "Créer une nouvelle signature" +declineButton = "Refuser" +decline = "Refuser la demande" +deleteSelected = "Supprimer la signature sélectionnée" +drawSignature = "Dessinez votre signature ci-dessous" +dueDate = "Date d’échéance" +fileTooLarge = "La taille du fichier doit être inférieure à 5 Mo" +fontFamily = "Famille de police" +fontSize = "Taille de police : {{size}}px" +fontSizePlaceholder = "Taille" +from = "De" +invalidCertFile = "Veuillez sélectionner un fichier de certificat P12 ou PFX" +invalidFileType = "Veuillez sélectionner un fichier image" +location = "Emplacement (facultatif)" +locationPlaceholder = "D’où signez-vous ?" +message = "Message" +noCertificate = "Veuillez sélectionner un fichier de certificat" +noSignatures = "Veuillez placer au moins une signature sur le PDF" +p12File = "Fichier de certificat P12/PFX" +password = "Mot de passe du certificat" +passwordPlaceholder = "Saisissez le mot de passe…" +penColor = "Couleur du trait" +penSize = "Taille du trait : {{size}}px" +placementActive = "Cliquez sur le PDF pour placer" +placeSignatureButton = "Placer la signature sur le PDF" +reason = "Raison (facultatif)" +reasonPlaceholder = "Pourquoi signez-vous ?" +removeImage = "Supprimer l’image" +removeCertFile = "Supprimer le fichier" +savedSignatures = "Signatures enregistrées" +selectFile = "Sélectionner un fichier image" +selectSignatureTitle = "Sélectionner ou créer une signature" +signButton = "Signer le document" +signatureInfo = "Ces paramètres sont configurés par le propriétaire du document" +signaturePlaced = "Signature placée sur la page" +signatureSettings = "Paramètres de signature" +signatureText = "Texte de la signature" +signatureTextPlaceholder = "Saisissez votre nom…" +signatureTypeLabel = "Type de signature" +signingTitle = "Signature" +textColor = "Couleur du texte" +typeSignature = "Saisissez votre nom pour créer une signature" +uploadCert = "Certificat personnalisé" +uploadCertDesc = "Utilisez votre propre certificat P12/PFX" +uploadSignature = "Téléverser l’image de votre signature" +usePersonalCert = "Certificat personnel" +usePersonalCertDesc = "Généré automatiquement pour votre compte" +useServerCert = "Certificat d’organisation" +useServerCertDesc = "Certificat d’organisation partagé" +workbenchTitle = "Demande de signature" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Choisir la couleur du trait" +continue = "Continuer" + +[certSign.collab.signRequest.certModal] +description = "Vous avez placé {{count}} signature(s). Choisissez votre certificat pour terminer la signature." +sign = "Signer le document" +certValidating = "Validation du certificat…" +certValidUntil = "Certificat valide jusqu’au {{date}}" +certInvalid = "Certificat invalide : {{error}}" +certInvalidFallback = "Certificat invalide" +certNetworkError = "Impossible de valider le certificat" +title = "Configurer le certificat" + +[certSign.collab.signRequest.image] +hint = "Téléversez une image PNG ou JPG de votre signature" + +[certSign.collab.signRequest.mode] +move = "Déplacer la signature" +place = "Placer la signature" +title = "Mode signature ou déplacement" + +[certSign.collab.signRequest.modeTabs] +draw = "Dessiner" +image = "Téléverser" +text = "Saisir" + +[certSign.collab.signRequest.placeSignature] +message = "Cliquez sur le PDF pour placer votre signature" +title = "Placer la signature" + +[certSign.collab.signRequest.preview] +imageAlt = "Signature sélectionnée" +missing = "Aucun aperçu" +textFallback = "Signature" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Signature dessinée" +defaultImageLabel = "Signature téléversée" +defaultLabel = "Signature" +defaultTextLabel = "Signature saisie" +delete = "Supprimer la signature" +none = "Aucune signature enregistrée" + +[certSign.collab.signRequest.signatureType] +draw = "Dessiner" +type = "Saisir" +upload = "Téléverser" + +[certSign.collab.signRequest.steps] +back = "Retour" +cancelPlacement = "Annuler le placement" +certificate = "Certificat" +clickMultipleTimes = "Cliquez plusieurs fois sur le PDF pour placer des signatures. Faites glisser une signature pour la déplacer ou la redimensionner." +clickToPlace = "Cliquez sur le PDF à l’endroit où vous souhaitez que votre signature apparaisse." +continue = "Continuer vers la sélection du certificat" +continueToPlacement = "Continuer vers le placement" +continueToReview = "Continuer vers la vérification" +createSignature = "Créer une signature" +invisible = "Invisible" +location = "Emplacement :" +multipleSignatures = "{{count}} signatures seront appliquées au PDF" +oneSignature = "1 signature sera appliquée au PDF" +placeOnPdf = "Placer sur le PDF" +reason = "Raison :" +reviewTitle = "Vérifier avant de signer" +signaturePlaced = "Signature placée sur la page {{page}}. Vous pouvez ajuster la position en recliquant ou continuer vers la vérification." +visible = "Visible" +visibility = "Visibilité :" +yourSignatures = "Vos signatures ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Couleur" +fontLabel = "Police" +fontSizeLabel = "Taille" +fontSizePlaceholder = "16" +label = "Texte de la signature" +modalHint = "Saisissez votre nom, puis cliquez sur Continuer pour le placer sur le PDF." +placeholder = "Saisissez votre nom…" + +[certSign.collab.participant] +certValidating = "Validation du certificat…" +certValid = "✓ Certificat valide" +certValidUntil = " jusqu’au {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Certificat invalide" +certNetworkError = "Impossible de valider le certificat" + +[certSign.collab.addParticipants] +add = "Ajouter {{count}} participant(s)" +back = "Retour" +configureSignatures = "Configurer les paramètres de signature" +continue = "Continuer vers les paramètres de signature" +reasonHelp = "Préconfigurer une raison de signature pour ces participants (optionnel, ils peuvent la modifier lors de la signature)" +reasonPlaceholder = "ex. : Approbation, Vérification…" +selectUsers = "Sélectionner des utilisateurs" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Inclure une page de synthèse des signatures" +includeSummaryPageHelp = "Une page récapitulative sera ajoutée à la fin avec toutes les métadonnées de signature. Les cadres de signature du certificat numérique sur les pages individuelles seront supprimés (les signatures manuscrites ne sont pas affectées)." + +[certSign.collab.sessionList] +active = "Actives" +finalized = "Finalisées" + +[certSign.collab.signatureSettings] +description = "Configurer l’apparence des signatures pour tous les participants" +title = "Apparence des signatures" + +[certSign.collab.userSelector] +inviteUsers = "Ajouter des utilisateurs" +loadError = "Échec du chargement des utilisateurs" +noTeam = "Pas d’équipe" +noUsers = "Aucun autre utilisateur trouvé." +placeholder = "Sélectionner des utilisateurs…" + +[certSign.mobile] +panelActions = "Actions" +panelDocument = "Document" +panelPeople = "Personnes" + +[certSign.sessions] +deleted = "Session supprimée" +fetchFailed = "Échec du chargement des détails de la session" +finalized = "Session finalisée" +loaded = "PDF signé chargé" +pdfNotReady = "PDF non prêt" +pdfNotReadyDesc = "Le PDF signé est en cours de génération. Veuillez réessayer dans un instant." + +[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é" + [changeCreds] changePassword = "Vous utilisez les identifiants de connexion par défaut. Veuillez saisir un nouveau mot de passe" changeUsername = "Mettre à jour votre nom d’utilisateur. Vous serez déconnecté après la mise à jour." @@ -2644,7 +2933,7 @@ upgradeButton = "Mettre à niveau le compte" [config.apiKeys] chartAriaLabel = "Utilisation des crédits : inclus {{includedUsed}} sur {{includedTotal}}, achetés {{purchasedUsed}} sur {{purchasedTotal}}" copyKeyAriaLabel = "Copier la clé API" -creditsRemaining = "Credits Remaining" +creditsRemaining = "Crédits restant" description = "Votre clé API pour accéder à la suite d’outils PDF de Stirling. Copiez-la dans votre projet ou actualisez pour en générer une nouvelle." docsDescription = "En savoir plus sur l’intégration avec Stirling PDF :" docsLink = "Documentation API" @@ -2695,12 +2984,12 @@ security = "Configuration de sécurité" system = "Configuration système" [connectionMode.status] -localOffline = "Offline mode running" -localOnline = "Offline mode running" -saas = "Connected to Stirling Cloud" -selfhostedChecking = "Connected to self-hosted server (checking...)" -selfhostedOffline = "Self-hosted server unreachable" -selfhostedOnline = "Connected to self-hosted server" +localOffline = "Mode offline" +localOnline = "Mode offline" +saas = "Connecté au Cloud Stirling" +selfhostedChecking = "Connection en cours à un serveur auto-hébergé" +selfhostedOffline = "Serveur auto-hébergé inaccessible" +selfhostedOnline = "Connecté à un serveur auto-hébergé" [convert] autoRotate = "Rotation automatique" @@ -3242,6 +3531,46 @@ totalSelected = "Total sélectionné" unsupported = "Non pris en charge" unzip = "Décompresser" uploadError = "Échec du téléversement de certains fichiers." +copyCreated = "Copie enregistrée sur cet appareil." +copyFailed = "Impossible de créer une copie." +leaveShare = "Retirer de ma liste" +leaveShareFailed = "Impossible de retirer le fichier partagé." +leaveShareSuccess = "Retiré de votre liste de partages." +removeBoth = "Supprimer des deux" +removeFilePrompt = "Ce fichier est enregistré sur cet appareil et sur votre serveur. D’où souhaitez-vous le supprimer ?" +removeFileTitle = "Supprimer le fichier" +removeLocalOnly = "Cet appareil uniquement" +removeServerFailed = "Impossible de supprimer le fichier du serveur." +removeServerOnly = "Serveur uniquement" +removeServerOnlyPrompt = "Ce fichier est stocké uniquement sur votre serveur. Souhaitez-vous le supprimer du serveur ?" +removeServerSuccess = "Supprimé du serveur." +removeSharedPrompt = "Ce fichier est partagé avec vous. Vous pouvez le retirer de cet appareil ou de votre liste de partages." +removeSharedServerOnlyBlockedPrompt = "Ce fichier est partagé avec vous et stocké uniquement sur le serveur." +removeSharedServerOnlyPrompt = "Ce fichier est partagé avec vous et stocké uniquement sur le serveur. Le retirer de votre liste ?" +changesNotUploaded = "Modifications non téléversées" +cloudFile = "Fichier cloud" +filterAll = "Tous" +filterLocal = "Local" +filterSharedByMe = "Partagé par moi" +filterSharedWithMe = "Partagé avec moi" +lastSynced = "Dernière synchronisation" +localOnly = "Local uniquement" +makeCopy = "Faire une copie" +owner = "Propriétaire" +ownerUnknown = "Inconnu" +share = "Partager" +shareSelected = "Partager la sélection" +sharedByYou = "Partagé par vous" +sharedEditNoticeBody = "Vous n’avez pas de droits d’édition sur la version serveur de ce fichier. Toute modification sera enregistrée comme copie locale." +sharedEditNoticeConfirm = "Compris" +sharedEditNoticeTitle = "Copie serveur en lecture seule" +sharedWithYou = "Partagé avec vous" +sharing = "Partage" +storageState = "Stockage" +synced = "Synchronisé" +updateOnServer = "Mettre à jour sur le serveur" +uploadSelected = "Téléverser la sélection" +uploadToServer = "Téléverser sur le serveur" [files] addFiles = "Ajouter des fichiers" @@ -3367,6 +3696,77 @@ title = "À propos de l’aplatissement des PDF" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "À propos de la signature de groupe" + +[groupSigning.tooltip.finalization] +bullet1 = "Toutes les signatures sont appliquées dans l’ordre des participants que vous avez défini" +bullet2 = "Vous pouvez finaliser avec des signatures partielles si nécessaire" +bullet3 = "Une fois finalisée, la session ne peut plus être modifiée" +description = "Une fois que tous les participants ont signé (ou si vous choisissez de finaliser plus tôt), vous pouvez générer le PDF final signé." +title = "Processus de finalisation" + +[groupSigning.tooltip.roles] +bullet1 = "Propriétaire (vous) : crée la session, configure les paramètres par défaut des signatures, finalise le document" +bullet2 = "Participants : créent leur signature, choisissent le certificat, la placent sur le PDF" +bullet3 = "Les participants ne peuvent pas modifier la visibilité, la raison ou l’emplacement de la signature" +description = "Vous contrôlez les paramètres d’apparence des signatures pour tous les participants." +title = "Rôles des participants" + +[groupSigning.tooltip.sequential] +bullet1 = "Le premier participant doit signer avant que le second puisse accéder au document" +bullet2 = "Garantit un ordre de signature conforme aux exigences légales" +bullet3 = "Vous pouvez réorganiser les participants en les faisant glisser dans la liste" +description = "Les participants signent les documents dans l’ordre que vous spécifiez. Chaque signataire reçoit une notification quand c’est son tour." +title = "Signature séquentielle" + +[groupSigning.steps] +back = "Retour" +completed = "Terminé" +current = "En cours" +stepLabel = "Étape {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Continuer vers la vérification" +invisible = "Les signatures seront invisibles (métadonnées uniquement)" +locationLabel = "Emplacement :" +preview = "Aperçu" +reasonLabel = "Raison :" +title = "Configurer les paramètres de signature" +visible = "Les signatures seront visibles à la page {{page}}" + +[groupSigning.steps.review] +document = "Document" +dueDate = "Date d’échéance (optionnelle)" +dueDatePlaceholder = "Sélectionner une date d’échéance…" +invisible = "Invisible (métadonnées uniquement)" +location = "Emplacement :" +logo = "Logo :" +logoHidden = "Pas de logo" +logoShown = "Logo Stirling PDF affiché" +participants = "Participants" +reason = "Raison :" +send = "Envoyer les demandes de signature" +signatureSettings = "Paramètres de signature" +title = "Vérifier les détails de la session" +titleShort = "Vérifier et envoyer" +visibility = "Visibilité :" +visible = "Visible à la page {{page}}" +participantCount = "{{count}} participant(s) signeront dans l’ordre" + +[groupSigning.steps.selectDocument] +continue = "Continuer vers la sélection des participants" +noFile = "Veuillez sélectionner un seul fichier PDF parmi vos fichiers actifs pour créer une session de signature." +selectedFile = "Document sélectionné" +title = "Sélectionner un document" + +[groupSigning.steps.selectParticipants] +continue = "Continuer vers les paramètres de signature" +count = "{{count}} participant(s) sélectionné(s)" +label = "Sélectionner des participants" +placeholder = "Choisissez des participants pour signer…" +title = "Choisir des participants" + [getPdfInfo] downloadJson = "Télécharger le JSON" downloads = "Téléchargements" @@ -3926,7 +4326,7 @@ version = "Version" accountCreatedSuccess = "Compte créé avec succès ! Vous pouvez maintenant vous connecter." alreadyLoggedIn = "Vous êtes déjà connecté sur" alreadyLoggedIn2 = "appareils. Veuillez vous déconnecter des appareils et réessayer." -backToSignIn = "Back to sign in" +backToSignIn = "Retour à la connexion" cancel = "Annuler" changePasswordWarning = "Veuillez changer votre mot de passe après votre première connexion" credentialsUpdated = "Vos identifiants ont été mis à jour. Veuillez vous reconnecter." @@ -3973,16 +4373,16 @@ pleaseEnterBoth = "Veuillez saisir l’e-mail et le mot de passe" pleaseEnterEmail = "Veuillez saisir votre adresse e-mail" relyingPartyRegistrationNotFound = "Aucun enregistrement de partie de confiance trouvé" rememberme = "Se souvenir de moi" -resetHelp = "Enter your email to receive a secure link to reset your password. If the link has expired, please request a new one." -resetYourPassword = "Reset your password" +resetHelp = "Saisissez votre email pour recevoir un lien de réinitialisation de votre mot de passe. Si le lien a expiré, veuillez en demander un nouveau." +resetYourPassword = "Réinitialisation de mot de passe" saml2RequiresLicense = "La connexion SAML nécessite une licence payante (Server ou Enterprise). Veuillez contacter l’administrateur pour mettre à niveau votre plan." sending = "Envoi…" sendMagicLink = "Envoyer le lien magique" -sendResetLink = "Send reset link" +sendResetLink = "Envoyer le lien de réinitialisation" sessionExpired = "Votre session a expiré. Veuillez vous reconnecter." signin = "Connexion" signInAnonymously = "S’inscrire en tant qu’invité" -subtitle = "Sign back in to Stirling PDF" +subtitle = "Se reconnecter à Stirling PDF" signingIn = "Connexion en cours…" signinTitle = "Veuillez vous connecter" signInWith = "Se connecter avec" @@ -3991,7 +4391,7 @@ ssoSignIn = "Se connecter via l'authentification unique" title = "Connexion" toManySessions = "Vous avez trop de sessions actives." unexpectedError = "Erreur inattendue : {{message}}" -updatePassword = "Update password" +updatePassword = "Mise à jour du mot de passe" useEmailInstead = "Se connecter avec l’e‑mail" useMagicLink = "Utiliser plutôt le lien magique" userIsDisabled = "L'utilisateur est désactivé, la connexion est actuellement bloquée avec ce nom d'utilisateur. Veuillez contacter l'administrateur." @@ -4302,13 +4702,13 @@ workbench = "Voici le Workbench - la zone principale où vous v 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." [onboarding.freeTrial] -afterTrialWithoutPayment = "After your trial ends, you'll continue with our free tier. Add a payment method to keep Pro access." -afterTrialWithPayment = "Your Pro subscription will start automatically when the trial ends." -body = "You have full access to Stirling PDF Pro features during your trial. Enjoy unlimited conversions, larger file sizes, and priority processing." -daysRemaining = "{{days}} days remaining" -daysRemainingSingular = "{{days}} day remaining" -title = "Your 30-Day Pro Trial" -trialEnds = "Trial ends {{date}}" +afterTrialWithoutPayment = "À la fin de la période d'essai, vous basculerez en mode gratuit. Ajoutez une méthode de paiement pour garder l'accès Pro." +afterTrialWithPayment = "Votre abonnement Pro commencera immédiatement à la fin de la période d'essai." +body = "Vous avez accès à toutes les fonctionnalité Pro de Stirling PDF pendant la période d'essai. Profitez de conversion ilimitées, de tailles de fichier plus importantes et de la priorité des tâches." +daysRemaining = "{{days}} jours restants" +daysRemainingSingular = "{{days}} jour restant" +title = "Votre période d'essai Pro de 30 jours" +trialEnds = "La période d'essai fini le {{date}}" [onboarding.buttons] back = "Retour" @@ -4460,7 +4860,10 @@ zoomOut = "Zoom arrière" [viewer] cannotPreviewFile = "Impossible d’afficher un aperçu du fichier" +disableColorFilter = "Désactiver le filtre de couleur" dualPageView = "Vue double page" +enableDarkFilter = "Activer le filtre sombre" +enableSepiaFilter = "Activer le filtre sépia" firstPage = "Première page" lastPage = "Dernière page" nextPage = "Page suivante" @@ -4470,6 +4873,22 @@ singlePageView = "Vue page unique" unknownFile = "Fichier inconnu" zoomIn = "Zoom avant" zoomOut = "Zoom arrière" +resetZoom = "Réinitialiser le zoom" + +[viewer.nonPdf] +fileTypeBadge = "Fichier {{type}}" +convertToPdf = "Convertir en PDF" +loading = "Chargement…" +emptyFile = "Fichier vide" +csvStats = "{{rows}} lignes · {{columns}} colonnes · {{size}}" +sortedBy = "Trié par : {{column}}" +columnDefault = "Colonne {{index}}" +htmlPreviewWarning = "Aperçu HTML — les ressources externes peuvent ne pas se charger · {{size}}" +htmlPreview = "Aperçu HTML" +invalidJson = "JSON invalide — affichage du contenu brut" +textStats = "{{lines}} lignes · {{size}}" +lineNumbers = "Numéros de ligne" +renderMarkdown = "Afficher le markdown" [viewer.attachments] title = "Pièces jointes" @@ -4481,29 +4900,29 @@ empty = "Aucune pièce jointe dans ce document" noMatch = "Aucune pièce jointe ne correspond à votre recherche" [viewer.comments] -title = "Comments" -hint = "Place comments with the Comment, Insert Text, or Replace Text tools. They will appear here by page." -placeholder = "Type your comment..." +title = "Commentaires" +hint = "Commentez avec Commentaire, Insérer du texte ou remplacer le texte. Vos commentaires apparaîtrons ici par page." +placeholder = "Saisissez votre commentaire..." pageLabel = "Page {{page}}" -oneComment = "1 comment" -nComments = "{{count}} comments" -addCommentPlaceholder = "Add comment..." -addLink = "Add link" -goToLink = "Go to link" -addComment = "Add comment" -viewComment = "View comment" -addReplyPlaceholder = "Add reply..." -saveReply = "Save reply" -send = "Send" -moreActions = "More actions" -typeComment = "Comment" -typeInsertText = "Insert Text" -typeReplaceText = "Replace Text" -locateAnnotation = "Locate in document" -deleteTitle = "Remove annotation from comments?" -deleteDescription = "This annotation has a comment attached. You can remove just the comment from the sidebar while keeping the annotation, or delete everything." -removeCommentOnly = "Remove comment only" -deleteAnnotationAndComment = "Delete annotation & comment" +oneComment = "1 commentaire" +nComments = "{{count}} commentaires" +addCommentPlaceholder = "Ajouter un commentaire..." +addLink = "Ajouter un lien" +goToLink = "Suivre le lien" +addComment = "Ajouter un commentaire" +viewComment = "Voir le commentaire" +addReplyPlaceholder = "Répondre..." +saveReply = "Sauvegarder la réponse" +send = "Envoyer" +moreActions = "Plus d'actions" +typeComment = "Commentaire" +typeInsertText = "Insérer du text" +typeReplaceText = "Remplacer le commentaire" +locateAnnotation = "Chercher dans le document" +deleteTitle = "Supprimer l'annotation des commentaires ?" +deleteDescription = "Cette annotation a un commentaire. Vous pouvez supprimer seulement le commentaire de la bare latérale en gardant l'annotation ou supprimer les deux." +removeCommentOnly = "Supprimer seulement le commentaire" +deleteAnnotationAndComment = "Supprimer l'annotation et le commentaire" [viewer.formBar] title = "Champs de formulaire" @@ -4531,6 +4950,7 @@ toggleAttachments = "Afficher/Masquer les pièces jointes" toggleTheme = "Changer de thème" language = "Langue" toggleAnnotations = "Afficher/masquer les annotations" +toggleLayers = "Basculer les calques" search = "Rechercher dans le PDF" panMode = "Mode panoramique" applyRedactionsFirst = "Appliquez d’abord les caviardages" @@ -4551,7 +4971,7 @@ exitRedaction = "Quitter le mode de caviardage" save = "Enregistrer" downloadAll = "Tout télécharger" saveAll = "Tout enregistrer" -saveAs = "Save As" +saveAs = "Souvegarder sous" [textAlign] left = "Gauche" @@ -5407,20 +5827,72 @@ title = "Imprimer le fichier" 2 = "Entrez le nom de l'imprimante" [quickAccess] +access = "Accès" +accessAddPerson = "Ajouter une autre personne" +accessBack = "Retour" +accessCopyLink = "Copier le lien" +accessEmail = "Adresse e-mail" +accessEmailPlaceholder = "nom@entreprise.com" +accessFileLabel = "Fichier" +accessGeneral = "Accès général" +accessInviteTitle = "Inviter des personnes" +accessOwner = "Propriétaire" +accessPanel = "Accès au document" +accessPeople = "Personnes ayant accès" +accessRemove = "Supprimer" +accessRestricted = "Restreint" +accessRestrictedHint = "Seules les personnes ayant accès peuvent ouvrir" +accessRole = "Rôle" +accessRoleCommenter = "Commentateur" +accessRoleEditor = "Éditeur" +accessRoleViewer = "Lecteur" +accessSelectedFile = "Fichier sélectionné" +accessSendInvite = "Envoyer l’invitation" +accessTitle = "Accès au document" +accessYou = "Vous" account = "Compte" +activeSessions = "Sessions actives" +activeTab = "Actives" activity = "Activité" adminSettings = "Réglages admin" +allSessions = "Toutes les sessions" allTools = "Outils" automate = "Auto" +back = "Retour" +certSign = "Signature avec certificat" +completedSessions = "Sessions terminées" +completedTab = "Terminées" config = "Config" +createNew = "Créer une nouvelle demande" +createSession = "Créer une demande de signature" +dueDate = "Date d’échéance (optionnelle)" files = "Fichiers" help = "Aide" +noActiveSessions = "Aucune demande de signature en attente ni session active" +noCompletedSessions = "Aucune session terminée" +noFile = "Aucun fichier sélectionné" read = "Lire" reader = "Lecteur" +refresh = "Actualiser" +requestSignatures = "Demander des signatures" +selectSingleFileToRequest = "Sélectionnez un seul fichier PDF pour demander des signatures" +selectedFile = "Fichier sélectionné" +selectUsers = "Sélectionnez des utilisateurs pour signer" +selectUsersPlaceholder = "Choisissez des participants…" +sendingRequest = "Envoi…" settings = "Réglages" showMeAround = "Faites-moi visiter" sign = "Signer" +signatureRequests = "Demandes de signature" +signYourself = "Signer vous-même" +newRequest = "Nouvelle demande" tours = "Visites guidées" +wetSign = "Ajouter une signature" +filterMine = "Les miens" +filterOverdue = "En retard" +filterSigned = "Signé" +filterDeclined = "Refusé" +searchDocuments = "Rechercher des documents…" [quickAccess.helpMenu] adminTour = "Visite administrateur" @@ -6050,25 +6522,89 @@ toolNotAvailableLocally = "Your Stirling-PDF server is offline and \"{{endpoint} expired = "Votre session a expiré. Veuillez recharger la page et réessayer." refreshPage = "Rafraichir la page" +[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" + +[signatureSettings.tooltip] +header = "Paramètres d’apparence des signatures" + +[signatureSettings.tooltip.location] +bullet1 = "Exemples : « New York, USA », « Bureau de Londres », « À distance »" +bullet2 = "Diffère de la position sur la page" +bullet3 = "Peut être requis dans certaines juridictions" +description = "Emplacement géographique optionnel où la signature a été apposée. Stocké dans les métadonnées du certificat." +title = "Emplacement de la signature" + +[signatureSettings.tooltip.logo] +bullet1 = "Affiché à côté de la signature et du texte" +bullet2 = "Prend en charge les formats PNG, JPG" +bullet3 = "Améliore l’apparence professionnelle" +description = "Ajoutez un logo d’entreprise aux signatures visibles pour l’image de marque et l’authenticité." +title = "Logo de l’entreprise" + +[signatureSettings.tooltip.reason] +bullet1 = "Exemples : « Approbation », « Contrat », « Vérification terminée »" +bullet2 = "Visible dans les propriétés de signature du PDF" +bullet3 = "Utile pour les pistes d’audit et la conformité" +description = "Texte optionnel expliquant pourquoi le document est signé. Stocké dans les métadonnées du certificat." +title = "Raison de la signature" + +[signatureSettings.tooltip.visibility] +bullet1 = "Visible : La signature apparaît sur le PDF avec une apparence personnalisée" +bullet2 = "Invisible : Certificat intégré sans marque visuelle" +bullet3 = "Les signatures invisibles fournissent tout de même une validation cryptographique" +description = "Contrôle si la signature est visible sur le document ou intégrée de manière invisible." +title = "Visibilité de la signature" + [settings.configuration] advanced = "Avancé" database = "Base de données" endpoints = "Endpoints" features = "Fonctionnalités" +storageSharing = "Stockage et partage de fichiers" systemSettings = "Paramètres système" title = "Configuration" [settings.connection] -localDescription = "You are using the local backend without an account. Some tools requiring cloud processing or a self-hosted server are unavailable." +localDescription = "Vous utilisez le backend local sans compte. Certains outils nécessitant le cloud ou un serveur auto-hébergé ne sont pas disponibles." logout = "Se déconnecter" server = "Serveur" -signIn = "Sign In" +signIn = "Se connecter" title = "Mode de connexion" user = "Connecté en tant que" [settings.connection.mode] -local = "Local Only" -saas = "Stirling Cloud" +local = "En local seulement" +saas = "Cloud Stirling" selfhosted = "Auto-hébergé" [settings.planBilling] @@ -6332,10 +6868,13 @@ title = "Se connecter à Stirling" [setup.selfhosted] link = "ou connectez-vous à un compte auto-hébergé" subtitle = "Saisissez les identifiants du serveur" +changeServerLocked = "Votre organisation a limité cette application à un serveur spécifique" switchToLocal = "Use local tools instead" title = "Se connecter au serveur" [setup.selfhosted.unreachable] +changeServer = "Se connecter à un autre serveur" +changeServerLocked = "Votre organisation a limité cette application à un serveur spécifique" continueOffline = "Use local tools instead" message = "Could not reach {{url}}. Check that the server is running and accessible." retry = "Retry" @@ -6529,6 +7068,15 @@ saved = "Enregistrées" text = "Texte" title = "Type de signature" +[signRequest] +declined = "Demande de signature refusée" +fetchFailed = "Échec du chargement de la demande de signature" +signed = "Document signé avec succès" + +[signSession] +createFailed = "Échec de la création de la demande de signature" +created = "Demande de signature envoyée" + [signup] accountCreatedSuccessfully = "Compte créé avec succès ! Vous pouvez maintenant vous connecter." alreadyHaveAccount = "Vous avez déjà un compte ? Connectez-vous" @@ -6807,6 +7355,106 @@ title = "Diviser un PDF par Chapitres" [splitPdfByChapters] tags = "séparer,chapitres,split,chapters,bookmarks,organize" +[storageShare] +accessed = "Accédé" +accessDenied = "Vous n’avez pas accès à ce fichier partagé. Demandez au propriétaire de le partager avec vous." +accessFailed = "Impossible de charger l’activité." +accessDeniedBody = "Vous n’avez pas accès à ce fichier. Demandez au propriétaire de le partager avec vous." +accessDeniedTitle = "Aucun accès" +accessLimitedCommenter = "L’accès en tant que commentateur arrive bientôt. Demandez l’accès éditeur si vous devez télécharger." +accessLimitedTitle = "Accès limité" +accessLimitedViewer = "Ce lien est en lecture seule. Demandez l’accès éditeur si vous devez télécharger." +createdAt = "Créé" +download = "Télécharger" +downloadFailed = "Impossible de télécharger ce fichier." +expiredBody = "Ce lien de partage est invalide ou a expiré." +expiredTitle = "Lien expiré" +goToLogin = "Aller à la connexion" +loadFailed = "Impossible d’ouvrir le fichier partagé." +loading = "Chargement du lien de partage…" +loginPrompt = "Connectez-vous pour accéder à ce fichier partagé." +loginRequired = "Connexion requise" +openInApp = "Ouvrir dans Stirling PDF" +ownerLabel = "Propriétaire" +ownerUnknown = "Inconnu" +requiresLogin = "Ce fichier partagé nécessite une connexion." +roleCommenter = "Commentateur" +roleEditor = "Éditeur" +roleViewer = "Lecteur" +shareHeading = "Fichier partagé" +titleDefault = "Fichier partagé" +tryAgain = "Veuillez réessayer plus tard." +addUser = "Ajouter" +commenterHint = "Les commentaires arrivent bientôt." +copied = "Lien copié dans le presse-papiers" +copy = "Copier" +copyFailed = "Échec de la copie" +description = "Créez un lien de partage pour ce fichier. Les utilisateurs connectés munis du lien peuvent y accéder." +downloadsCount = "Téléchargements : {{count}}" +emailWarningBody = "Cela ressemble à une adresse e-mail. Si cette personne n’est pas déjà utilisatrice de Stirling PDF, elle ne pourra pas accéder au fichier." +emailWarningConfirm = "Partager quand même" +emailWarningTitle = "Adresse e-mail" +errorTitle = "Échec du partage" +failure = "Impossible de générer un lien de partage. Veuillez réessayer." +fileLabel = "Fichier" +generate = "Générer le lien" +generated = "Lien de partage généré" +hideActivity = "Masquer l’activité" +invalidUsername = "Saisissez un nom d’utilisateur ou une adresse e-mail valide." +lastAccessed = "Dernier accès" +linkAccessTitle = "Accès via lien de partage" +linkLabel = "Lien de partage" +linksDisabled = "Les liens de partage sont désactivés." +linksDisabledBody = "Les liens de partage sont désactivés par les paramètres de votre serveur." +manage = "Gérer le partage" +manageDescription = "Créer et gérer des liens pour partager ce fichier." +manageLoadFailed = "Impossible de charger les liens de partage." +manageTitle = "Gérer le partage" +noActivity = "Aucune activité pour le moment." +noLinks = "Aucun lien de partage actif pour le moment." +noSharedUsers = "Aucun utilisateur n’a encore accès." +removeLink = "Supprimer le lien" +removeUser = "Supprimer" +revokeFailed = "Impossible de supprimer le lien de partage." +revoked = "Lien de partage supprimé" +roleLabel = "Rôle" +sharingDisabled = "Le partage est désactivé." +sharingDisabledBody = "Le partage a été désactivé par les paramètres de votre serveur." +sharedUsersTitle = "Utilisateurs ayant accès" +title = "Partager le fichier" +unknownUser = "Utilisateur inconnu" +userAddFailed = "Impossible de partager avec cet utilisateur." +userAdded = "Utilisateur ajouté à la liste de partage." +usernameLabel = "Nom d’utilisateur ou adresse e-mail" +usernamePlaceholder = "Saisissez un nom d’utilisateur ou une adresse e-mail" +userRemoveFailed = "Impossible de supprimer cet utilisateur." +userRemoved = "Utilisateur supprimé de la liste de partage." +viewActivity = "Voir l’activité" +viewed = "Consulté" +viewsCount = "Vues : {{count}}" +downloaded = "Téléchargé" +bulkDescription = "Créer un seul lien pour partager tous les fichiers sélectionnés avec les utilisateurs connectés." +bulkTitle = "Partager les fichiers sélectionnés" +copyLink = "Copier le lien de partage" +fileCount = "{{count}} fichiers sélectionnés" +ownerOnly = "Seul le propriétaire peut gérer le partage." +selectSingleFile = "Sélectionnez un seul fichier pour gérer le partage." + +[storageUpload] +description = "Cela téléverse le fichier actuel vers le stockage du serveur pour votre accès personnel." +errorTitle = "Échec du téléversement" +failure = "Échec du téléversement. Veuillez vérifier votre connexion et vos paramètres de stockage." +fileLabel = "Fichier" +hint = "Les liens publics et les modes d’accès sont contrôlés par les paramètres de votre serveur." +success = "Téléversé sur le serveur" +title = "Téléverser sur le serveur" +updateButton = "Mettre à jour sur le serveur" +uploadButton = "Téléverser sur le serveur" +bulkDescription = "Cela téléverse les fichiers sélectionnés vers le stockage de votre serveur." +bulkTitle = "Téléverser les fichiers sélectionnés" +fileCount = "{{count}} fichiers sélectionnés" +more = " +{{count}} autres" + [storage] approximateSize = "Taille approximative" fileTooLarge = "Fichier trop volumineux. Taille maximale par fichier :" @@ -7153,6 +7801,30 @@ title = "Afficher/modifier un PDF" [warning] tooltipTitle = "Avertissement" +[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" + [watermark] completed = "Filigrane ajouté" desc = "Ajouter des filigranes texte ou image aux fichiers PDF" @@ -7333,6 +8005,7 @@ activeSession = "Session active" addMembers = "Ajouter des membres" admin = "Admin" confirmDelete = "Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action ne peut pas être annulée." +confirmUnlock = "Êtes-vous sûr de vouloir déverrouiller ce compte utilisateur ?" deleteUser = "Supprimer l’utilisateur" deleteUserError = "Échec de la suppression de l’utilisateur" deleteUserSuccess = "Utilisateur supprimé avec succès" @@ -7341,6 +8014,8 @@ disable = "Désactiver" disabled = "Désactivé" editRole = "Modifier le rôle" enable = "Activer" +locked = "verrouillé" +lockedBadge = "Verrouillé" loading = "Chargement des personnes..." loginRequired = "Activez d’abord le mode connexion" member = "Membre" @@ -7350,6 +8025,9 @@ searchMembers = "Rechercher des membres..." status = "Statut" team = "Équipe" title = "Personnes" +unlockAccount = "Déverrouiller le compte" +unlockUserError = "Échec du déverrouillage du compte utilisateur" +unlockUserSuccess = "Compte utilisateur déverrouillé avec succès" user = "Utilisateur" [workspace.people.actions] diff --git a/frontend/public/locales/ga-IE/translation.toml b/frontend/public/locales/ga-IE/translation.toml index 9bfb8e663d..d5995fc772 100644 --- a/frontend/public/locales/ga-IE/translation.toml +++ b/frontend/public/locales/ga-IE/translation.toml @@ -8,6 +8,7 @@ black = "Dubh" blue = "Gorm" bored = "Leamh Ag Feitheamh?" cancel = "Cealaigh" +confirm = "Deimhnigh" changedCredsMessage = "Dintiúir athraithe!" chooseFile = "Roghnaigh Comhad" close = "Dún" @@ -146,6 +147,7 @@ insufficientCredits = "Creidiúintí neamhleor. Riachtanach: {{requiredCredits}} loadingCredits = "Ag seiceáil creidiúintí..." loadingProStatus = "Ag seiceáil stádais síntiúis..." noticeTopUpOrPlan = "Níl go leor creidiúintí ann; cuir creidiúintí leis nó uasghrádaigh go plean" +accessInvite = "Cuir cuireadh" [account] accountSettings = "Socruithe cuntas" @@ -1427,6 +1429,34 @@ title = "Próiseáil" description = "Uasmhéid ama le fanacht le tasc próiseála sula dtuairiscítear earráid." label = "Teorainn Ama Próiseála (soicind)" +[admin.settings.storage] +description = "Rialaigh stóráil agus roghanna comhroinnte an fhreastalaí." +title = "Stóráil Comhad agus Comhroinnt" + +[admin.settings.storage.enabled] +description = "Ceadaigh d'úsáideoirí comhaid a stóráil ar an bhfreastalaí." +label = "Cumasaigh stóráil comhad ar an bhfreastalaí" + +[admin.settings.storage.sharing.email] +description = "Ceadaigh comhroinnt le seoltaí ríomhphoist." +label = "Cumasaigh Comhroinnt trí Ríomhphost" +mailLink = "Cumraigh Socruithe Ríomhphoist" +mailNote = "Éilíonn cumraíocht ríomhphoist. " + +[admin.settings.storage.sharing.enabled] +description = "Ceadaigh d'úsáideoirí comhaid stóráilte a roinnt." +label = "Cumasaigh Comhroinnt" + +[admin.settings.storage.sharing.links] +description = "Ceadaigh comhroinnt trí naisc a éilíonn logáil isteach." +frontendUrlLink = "Cumraigh i Socruithe an Chórais" +frontendUrlNote = "Éilíonn Frontend URL. " +label = "Cumasaigh naisc chomhroinnte" + +[admin.settings.storage.signing.enabled] +description = "Ceadaigh d'úsáideoirí seisiúin sínithe doiciméid il-rannpháirtithe a chruthú. Ní mór stóráil chomhad freastalaí a bheith cumasaithe." +label = "Cumasaigh Sínithe Grúpa (Alfa)" + [admin.settings.unsavedChanges] cancel = "Lean ar eagarthóireacht" discard = "Cuir athruithe i leataobh" @@ -2059,7 +2089,19 @@ numbers = "Uimhreacha/raonta: 5, 10-20" progressions = "Forásanna: 3n, 4n+1" [certSign] +allSigned = "Tá gach rannpháirtí tar éis síniú. Réidh le críochnú." +awaitingSignatures = "Ag feitheamh le sínithe" +signatureProgress = "{{signedCount}}/{{totalCount}} sínithe" chooseCertificate = "Roghnaigh Comhad Teastais" +declined = "Diúltaithe" +fetchFailed = "Theip ar shonraí sínithe a lódáil" +finalized = "Críochnaithe" +notified = "Ar feitheamh" +partialNote = "Is féidir leat críochnú go luath leis na sínithe reatha. Ní áireofar rannpháirtithe gan síniú." +pending = "Ar feitheamh" +readyToFinalize = "Réidh le críochnú" +signed = "Sínithe" +viewed = "Feicthe" chooseJksFile = "Roghnaigh Comhad JKS" chooseP12File = "Roghnaigh Comhad PKCS12" choosePfxFile = "Roghnaigh Comhad PFX" @@ -2082,6 +2124,7 @@ title = "Síniú Teastais" invisible = "Dofheicthe" stepTitle = "Cuma Sínithe" visible = "Infheicthe" +visibility = "Infheictheacht" [certSign.appearance.options] title = "Sonraí Sínithe" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Críochnaigh agus Luchtaigh an PDF Sínithe" +early = "Críochnaigh leis na Sínithe Reatha" + +[certSign.collab.sessionDetail] +addButton = "Cuir Rannpháirtithe Leis" +addParticipants = "Cuir Rannpháirtithe Leis" +addParticipantsError = "Theip ar rannpháirtithe a chur leis" +backToList = "Ar ais go Seisiúin" +deleteConfirm = "An bhfuil tú cinnte? Ní féidir é seo a chur ar ceal." +deleteError = "Theip ar an seisiún a scriosadh" +deleted = "Scriosadh an seisiún" +deleteSession = "Scrios an Seisiún" +dueDate = "Spriocdháta" +finalizeError = "Theip ar an seisiún a chríochnú" +loadPdfError = "Theip ar an PDF sínithe a lódáil" +loadSignedPdf = "Luchtaigh an PDF Sínithe isteach sna Comhaid Ghníomhacha" +messageLabel = "Teachtaireacht" +noAdditionalInfo = "Níl a thuilleadh eolais" +owner = "Úinéir" +participantRemoved = "Baineadh rannpháirtí" +participants = "Rannpháirtithe" +participantsAdded = "Cuireadh rannpháirtithe leis go rathúil" +removeParticipant = "Bain" +removeParticipantError = "Theip ar rannpháirtí a bhaint" +selectUsers = "Roghnaigh úsáideoirí..." +sessionInfo = "Eolas Seisiúin" +workbenchTitle = "Bainistíocht Seisiúin" + +[certSign.collab.signRequest] +addedToFiles = "Cuireadh an doiciméad le comhaid ghníomhacha" +addSignature = "Cuir Do Shíniú Leis" +addToFiles = "Cuir le Comhaid Ghníomhacha" +advancedSettings = "Ardsocruithe" +backToList = "Ar ais go hIarratais Shínithe" +certificateChoice = "Roghnaigh teastas le síniú leis" +changeSignature = "Athraigh an síniú" +clearSignature = "Glan an Síniú" +completeAndSign = "Críochnaigh & Sínigh" +createNewSignature = "Cruthaigh Síniú Nua" +declineButton = "Diúltaigh" +decline = "Diúltaigh an Iarratas" +deleteSelected = "Scrios an síniú roghnaithe" +drawSignature = "Tarraing do shíniú thíos" +dueDate = "Spriocdháta" +fileTooLarge = "Ní mór do mhéid an chomhaid a bheith níos lú ná 5MB" +fontFamily = "Teaghlach Cló" +fontSize = "Méid Cló: {{size}}px" +fontSizePlaceholder = "Méid" +from = "Ó" +invalidCertFile = "Roghnaigh comhad teastais P12 nó PFX le do thoil" +invalidFileType = "Roghnaigh comhad íomhá le do thoil" +location = "Suíomh (Roghnach)" +locationPlaceholder = "Cá bhfuil tú ag síniú?" +message = "Teachtaireacht" +noCertificate = "Roghnaigh comhad teastais le do thoil" +noSignatures = "Cuir ar a laghad síniú amháin ar an PDF" +p12File = "Comhad Teastais P12/PFX" +password = "Focal Faire an Teastais" +passwordPlaceholder = "Iontráil focal faire..." +penColor = "Dath Peann" +penSize = "Méid Peann: {{size}}px" +placementActive = "Cliceáil ar an PDF chun é a chur" +placeSignatureButton = "Cuir Síniú ar an PDF" +reason = "Fáth (Roghnach)" +reasonPlaceholder = "Cén fáth atá tú ag síniú?" +removeImage = "Bain Ãomhá" +removeCertFile = "Bain Comhad" +savedSignatures = "Sínithe Sábháilte" +selectFile = "Roghnaigh Comhad Ãomhá" +selectSignatureTitle = "Roghnaigh nó Cruthaigh Síniú" +signButton = "Sínigh an Doiciméad" +signatureInfo = "Cumraíonn úinéir an doiciméid na socruithe seo" +signaturePlaced = "Cuireadh síniú ar an leathanach" +signatureSettings = "Socruithe Sínithe" +signatureText = "Téacs an Sínithe" +signatureTextPlaceholder = "Iontráil d'ainm..." +signatureTypeLabel = "Cineál an Sínithe" +signingTitle = "Síniú" +textColor = "Dath Téacs" +typeSignature = "Clóscríobh d'ainm chun síniú a chruthú" +uploadCert = "Teastas Saincheaptha" +uploadCertDesc = "Úsáid do theastas P12/PFX féin" +uploadSignature = "Uasluchtaigh íomhá do shínithe" +usePersonalCert = "Teastas Pearsanta" +usePersonalCertDesc = "Ginte go huathoibríoch do do chuntas" +useServerCert = "Teastas Eagraíochta" +useServerCertDesc = "Teastas eagraíochta roinnte" +workbenchTitle = "Iarratas Sínithe" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Roghnaigh dath an stróic" +continue = "Lean ar aghaidh" + +[certSign.collab.signRequest.certModal] +description = "Tá {{count}} síniú(í) curtha agat. Roghnaigh do theastas chun an síniú a chur i gcrích." +sign = "Sínigh an Doiciméad" +certValidating = "Teastas á bhailíochtú..." +certValidUntil = "Teastas bailí go dtí {{date}}" +certInvalid = "Teastas neamhbhailí: {{error}}" +certInvalidFallback = "Teastas neamhbhailí" +certNetworkError = "Níorbh fhéidir an teastas a bhailíochtú" +title = "Cumraigh an Teastas" + +[certSign.collab.signRequest.image] +hint = "Uasluchtaigh íomhá PNG nó JPG de do shíniú" + +[certSign.collab.signRequest.mode] +move = "Bog an Síniú" +place = "Cuir an Síniú" +title = "Mód síniúcháin nó bogtha" + +[certSign.collab.signRequest.modeTabs] +draw = "Tarraing" +image = "Uasluchtaigh" +text = "Clóscríobh" + +[certSign.collab.signRequest.placeSignature] +message = "Cliceáil ar an PDF chun do shíniú a chur" +title = "Cuir Síniú" + +[certSign.collab.signRequest.preview] +imageAlt = "Síniú roghnaithe" +missing = "Gan réamhamharc" +textFallback = "Síniú" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Síniú tarraingthe" +defaultImageLabel = "Síniú uasluchtaithe" +defaultLabel = "Síniú" +defaultTextLabel = "Síniú clóscríofa" +delete = "Scrios síniú" +none = "Níl aon síniú sábháilte" + +[certSign.collab.signRequest.signatureType] +draw = "Tarraing" +type = "Clóscríobh" +upload = "Uasluchtaigh" + +[certSign.collab.signRequest.steps] +back = "Ar ais" +cancelPlacement = "Cealaigh Socrú" +certificate = "Teastas" +clickMultipleTimes = "Cliceáil ar an PDF arís agus arís eile chun sínithe a chur. Tarraing aon síniú chun é a bhogadh nó a athmhéadú." +clickToPlace = "Cliceáil ar an PDF san áit ar mhaith leat go mbeadh do shíniú le feiceáil." +continue = "Lean ar aghaidh go Roghnú Teastais" +continueToPlacement = "Lean ar aghaidh go Socrú" +continueToReview = "Lean ar aghaidh go hAthbhreithniú" +createSignature = "Cruthaigh Síniú" +invisible = "Dofheicthe" +location = "Suíomh:" +multipleSignatures = "Cuirfear {{count}} sínithe i bhfeidhm ar an PDF" +oneSignature = "Cuirfear 1 síniú i bhfeidhm ar an PDF" +placeOnPdf = "Cuir ar an PDF" +reason = "Fáth:" +reviewTitle = "Athbhreithniú Roimh Shíniú" +signaturePlaced = "Cuireadh an síniú ar leathanach {{page}}. Is féidir leat an suíomh a choigeartú trí chliceáil arís nó lean ar aghaidh chun athbhreithniú a dhéanamh." +visible = "Infheicthe" +visibility = "Infheictheacht:" +yourSignatures = "Do Shínithe ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Dath" +fontLabel = "Cló" +fontSizeLabel = "Méid" +fontSizePlaceholder = "16" +label = "Téacs an Sínithe" +modalHint = "Iontráil d'ainm, ansin cliceáil Lean ar aghaidh chun é a chur ar an PDF." +placeholder = "Iontráil d'ainm..." + +[certSign.collab.participant] +certValidating = "Teastas á bhailíochtú..." +certValid = "✓ Teastas bailí" +certValidUntil = " go dtí {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Teastas neamhbhailí" +certNetworkError = "Níorbh fhéidir an teastas a bhailíochtú" + +[certSign.collab.addParticipants] +add = "Cuir {{count}} rannpháirtí leis" +back = "Ar ais" +configureSignatures = "Cumraigh Socruithe Sínithe" +continue = "Lean ar aghaidh go Socruithe Sínithe" +reasonHelp = "Réamhshocraigh fáth sínithe do na rannpháirtithe seo (roghnach, is féidir leo a athrú agus iad ag síniú)" +reasonPlaceholder = "m.sh. Formheas, Athbhreithniú..." +selectUsers = "Roghnaigh Úsáideoirí" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Cuir Leathanach Achoimre Síniú san áireamh" +includeSummaryPageHelp = "Cuirfear leathanach achoimre leis ag an deireadh le gach meiteashonraí sínithe. Cuirfear boscaí sínithe teastais dhigiteacha ar leathanaigh aonair ar ceal (ní théann sé i bhfeidhm ar shínithe fliucha)." + +[certSign.collab.sessionList] +active = "Gníomhach" +finalized = "Críochnaithe" + +[certSign.collab.signatureSettings] +description = "Cumraigh conas a bheidh cuma ar shínithe do gach rannpháirtí" +title = "Cuma an Sínithe" + +[certSign.collab.userSelector] +inviteUsers = "Cuir Úsáideoirí Leis" +loadError = "Theip ar úsáideoirí a lódáil" +noTeam = "Gan Foireann" +noUsers = "Níor aimsíodh aon úsáideoirí eile." +placeholder = "Roghnaigh úsáideoirí..." + +[certSign.mobile] +panelActions = "Gníomhartha" +panelDocument = "Doiciméad" +panelPeople = "Daoine" + +[certSign.sessions] +deleted = "Scriosadh an seisiún" +fetchFailed = "Theip ar shonraí an tseisiúin a lódáil" +finalized = "Seisiún críochnaithe" +loaded = "Luchtaíodh an PDF sínithe" +pdfNotReady = "Níl an PDF Réidh" +pdfNotReadyDesc = "Tá an PDF sínithe á ghiniúint. Bain triail eile as ar ball." + +[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" + [changeCreds] changePassword = "Tá dintiúir réamhshocraithe logáil isteach á úsáid agat. Cuir isteach pasfhocal nua le do thoil" changeUsername = "Nuashonraigh d'ainm úsáideora. Logálfar amach thú tar éis an nuashonraithe." @@ -3242,6 +3531,46 @@ totalSelected = "Iomlán Roghnaithe" unsupported = "Gan tacaíocht" unzip = "Dízipeáil" uploadError = "Theip ar uaslódáil roinnt comhad." +copyCreated = "Cóip sábháilte ar an ngléas seo." +copyFailed = "Níorbh fhéidir cóip a chruthú." +leaveShare = "Bain de mo liosta" +leaveShareFailed = "Níorbh fhéidir an comhad comhroinnte a bhaint." +leaveShareSuccess = "Baineadh de do liosta comhroinnte." +removeBoth = "Bain ón dá cheann" +removeFilePrompt = "Tá an comhad seo sábháilte ar an ngléas seo agus ar do fhreastalaí. Cá mba mhaith leat é a bhaint as?" +removeFileTitle = "Bain comhad" +removeLocalOnly = "An gléas seo amháin" +removeServerFailed = "Níorbh fhéidir an comhad a bhaint den fhreastalaí." +removeServerOnly = "Freastalaí amháin" +removeServerOnlyPrompt = "Níl an comhad seo stóráilte ach ar do fhreastalaí. Ar mhaith leat é a bhaint den fhreastalaí?" +removeServerSuccess = "Baineadh den fhreastalaí." +removeSharedPrompt = "Tá an comhad seo roinnte leat. Is féidir leat é a bhaint den ghléas seo nó de do liosta comhroinnte." +removeSharedServerOnlyBlockedPrompt = "Tá an comhad seo roinnte leat agus stóráilte ar an bhfreastalaí amháin." +removeSharedServerOnlyPrompt = "Tá an comhad seo roinnte leat agus stóráilte ar an bhfreastalaí amháin. Bain é de do liosta?" +changesNotUploaded = "Níor uasluchtaíodh na hathruithe" +cloudFile = "Comhad sa scamall" +filterAll = "Uile" +filterLocal = "Ãitiúil" +filterSharedByMe = "Roinnte agam" +filterSharedWithMe = "Roinnte liom" +lastSynced = "Sioncrónaithe go deireanach" +localOnly = "Ãitiúil amháin" +makeCopy = "Déan cóip" +owner = "Úinéir" +ownerUnknown = "Anaithnid" +share = "Comhroinn" +shareSelected = "Comhroinn Roghnaithe" +sharedByYou = "Roinnte agat" +sharedEditNoticeBody = "Níl cearta eagarthóireachta agat ar leagan freastalaí an chomhaid seo. Sábhálfar aon eagarthóireacht a dhéanfaidh tú mar chóip áitiúil." +sharedEditNoticeConfirm = "Tuigim" +sharedEditNoticeTitle = "Leagan freastalaí inléite amháin" +sharedWithYou = "Roinnte leat" +sharing = "Comhroinnt" +storageState = "Stóráil" +synced = "Sioncrónaithe" +updateOnServer = "Nuashonraigh ar an bhfreastalaí" +uploadSelected = "Uasluchtaigh Roghnaithe" +uploadToServer = "Uasluchtaigh chuig an bhfreastalaí" [files] addFiles = "Cuir comhaid leis" @@ -3367,6 +3696,77 @@ title = "Maidir le Maolú PDFanna" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Maidir le Sínithe Grúpa" + +[groupSigning.tooltip.finalization] +bullet1 = "Cuirtear gach síniú i bhfeidhm san ord rannpháirtíochta a shonraigh tú" +bullet2 = "Is féidir leat críochnú le cuid de na sínithe más gá" +bullet3 = "Nuair a bheidh sé críochnaithe, ní féidir an seisiún a mhodhnú" +description = "Nuair a bheidh gach rannpháirtí tar éis síniú (nó má roghnaíonn tú críochnú go luath), is féidir leat an PDF sínithe deiridh a ghiniúint." +title = "Próiseas Críochnaithe" + +[groupSigning.tooltip.roles] +bullet1 = "Úinéir (tusa): Cruthaíonn seisiún, cumraíonn réamhshocruithe sínithe, críochnaíonn doiciméad" +bullet2 = "Rannpháirtithe: Cruthaíonn a síniú, roghnaíonn teastas, cuireann ar an PDF é" +bullet3 = "Ní féidir le rannpháirtithe infheictheacht, fáth ná socruithe suímh an tsínithe a athrú" +description = "Rialaíonn tú socruithe cuma an tsínithe do gach rannpháirtí." +title = "Róil Rannpháirtithe" + +[groupSigning.tooltip.sequential] +bullet1 = "Ní mór don chéad rannpháirtí síniú sula mbeidh an dara duine in ann rochtain a fháil ar an doiciméad" +bullet2 = "Cinntíonn ord sínithe cuí ar mhaithe le comhlíonadh dlí" +bullet3 = "Is féidir leat rannpháirtithe a athordú trí iad a tharraingt sa liosta" +description = "Síníonn rannpháirtithe doiciméid san ord a shonraíonn tú. Faigheann gach sínitheoir fógra nuair is é a seal é." +title = "Sínithe Seicheamhach" + +[groupSigning.steps] +back = "Ar ais" +completed = "Críochnaithe" +current = "Reatha" +stepLabel = "Céim {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Lean ar aghaidh go hAthbhreithniú" +invisible = "Beidh sínithe dofheicthe (meiteashonraí amháin)" +locationLabel = "Suíomh:" +preview = "Réamhamharc" +reasonLabel = "Fáth:" +title = "Cumraigh Socruithe Sínithe" +visible = "Beidh sínithe infheicthe ar leathanach {{page}}" + +[groupSigning.steps.review] +document = "Doiciméad" +dueDate = "Spriocdháta (Roghnach)" +dueDatePlaceholder = "Roghnaigh spriocdháta..." +invisible = "Dofheicthe (meiteashonraí amháin)" +location = "Suíomh:" +logo = "Lógó:" +logoHidden = "Gan lógó" +logoShown = "Lógó Stirling PDF le feiceáil" +participants = "Rannpháirtithe" +reason = "Fáth:" +send = "Seol Iarratais Shínithe" +signatureSettings = "Socruithe Sínithe" +title = "Athbhreithnigh Sonraí an tSeisiúin" +titleShort = "Athbhreithnigh & Seol" +visibility = "Infheictheacht:" +visible = "Infheicthe ar leathanach {{page}}" +participantCount = "Sínneoidh {{count}} rannpháirtí in ord" + +[groupSigning.steps.selectDocument] +continue = "Lean ar aghaidh go Roghnú Rannpháirtithe" +noFile = "Roghnaigh comhad PDF aonair ó do chomhaid ghníomhacha chun seisiún sínithe a chruthú." +selectedFile = "Doiciméad roghnaithe" +title = "Roghnaigh Doiciméad" + +[groupSigning.steps.selectParticipants] +continue = "Lean ar aghaidh go Socruithe Sínithe" +count = "Roghnaíodh {{count}} rannpháirtí" +label = "Roghnaigh rannpháirtithe" +placeholder = "Roghnaigh rannpháirtithe le síniú..." +title = "Roghnaigh Rannpháirtithe" + [getPdfInfo] downloadJson = "Ãosluchtaigh ceol JSON" downloads = "Ãoslódálacha" @@ -4460,7 +4860,10 @@ zoomOut = "Súmáil Amach" [viewer] cannotPreviewFile = "Ní féidir an comhad a réamhamharc." +disableColorFilter = "Díchumasaigh Scagaire Datha" dualPageView = "Amharc Dhá Leathanach" +enableDarkFilter = "Cumasaigh Scagaire Dorcha" +enableSepiaFilter = "Cumasaigh Scagaire Sepia" firstPage = "An Chéad Leathanach" lastPage = "An Leathanach Deireanach" nextPage = "Leathanach Ar Aghaidh" @@ -4470,6 +4873,22 @@ singlePageView = "Amharc Leathanach Aonair" unknownFile = "Comhad anaithnid" zoomIn = "Súmáil Isteach" zoomOut = "Súmáil Amach" +resetZoom = "Athshocraigh zúmáil" + +[viewer.nonPdf] +fileTypeBadge = "Comhad {{type}}" +convertToPdf = "Tiontaigh go PDF" +loading = "à lódáil..." +emptyFile = "Comhad folamh" +csvStats = "{{rows}} sraith · {{columns}} colún · {{size}}" +sortedBy = "Sórtáilte de réir: {{column}}" +columnDefault = "Colún {{index}}" +htmlPreviewWarning = "Réamhamharc HTML — seans nach lódálfar acmhainní seachtracha · {{size}}" +htmlPreview = "Réamhamharc HTML" +invalidJson = "JSON neamhbhailí — á thaispeáint amh" +textStats = "{{lines}} líne · {{size}}" +lineNumbers = "Uimhreacha línte" +renderMarkdown = "Rindreáil Markdown" [viewer.attachments] title = "Iatáin" @@ -4531,6 +4950,7 @@ toggleAttachments = "Scoránaigh Iatáin" toggleTheme = "Téama a Athsholáthar" language = "Teanga" toggleAnnotations = "Infheictheacht Anótálacha a Athrú" +toggleLayers = "Scoránaigh Sraitheanna" search = "Cuardaigh PDF" panMode = "Mód Pan" applyRedactionsFirst = "Cuir eagarthóireachtaí i bhfeidhm ar dtús" @@ -5407,20 +5827,72 @@ title = "Priontáil Comhad" 2 = "Cuir isteach Ainm an Phrintéara" [quickAccess] +access = "Rochtain" +accessAddPerson = "Cuir duine eile leis" +accessBack = "Ar ais" +accessCopyLink = "Cóipeáil nasc" +accessEmail = "Seoladh Ríomhphoist" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Comhad" +accessGeneral = "Rochtain Ghinearálta" +accessInviteTitle = "Tabhair cuireadh do dhaoine" +accessOwner = "Úinéir" +accessPanel = "Rochtain doiciméid" +accessPeople = "Daoine a bhfuil rochtain acu" +accessRemove = "Bain" +accessRestricted = "Srianta" +accessRestrictedHint = "Ní féidir ach le daoine a bhfuil rochtain acu a oscailt" +accessRole = "Ról" +accessRoleCommenter = "Tráchtaire" +accessRoleEditor = "Eagarthóir" +accessRoleViewer = "Amharcóir" +accessSelectedFile = "Comhad roghnaithe" +accessSendInvite = "Seol Cuireadh" +accessTitle = "Rochtain ar Dhoiciméad" +accessYou = "Tusa" account = "Cuntas" +activeSessions = "Seisiúin Ghníomhacha" +activeTab = "Gníomhach" activity = "Stair" adminSettings = "Socruí riar." +allSessions = "Gach Seisiún" allTools = "All Tools" automate = "Auto" +back = "Ar ais" +certSign = "Sínigh le Teastas" +completedSessions = "Seisiúin Chríochnaithe" +completedTab = "Críochnaithe" config = "Cumraigh" +createNew = "Cruthaigh Iarratas Nua" +createSession = "Cruthaigh Iarratas Sínithe" +dueDate = "Spriocdháta (roghnach)" files = "Comhaid" help = "Cabhair" +noActiveSessions = "Níl aon iarratais sínithe ar feitheamh ná seisiúin ghníomhacha" +noCompletedSessions = "Níl aon seisiúin chríochnaithe" +noFile = "Níl comhad roghnaithe" read = "Léigh" reader = "Léamh" +refresh = "Athnuaigh" +requestSignatures = "Iarraigh Sínithe" +selectSingleFileToRequest = "Roghnaigh comhad PDF aonair chun sínithe a iarraidh" +selectedFile = "Comhad roghnaithe" +selectUsers = "Roghnaigh úsáideoirí le síniú" +selectUsersPlaceholder = "Roghnaigh rannpháirtithe..." +sendingRequest = "à sheoladh..." settings = "Socruí" showMeAround = "Taispeáin dom timpeall" sign = "Sínigh" +signatureRequests = "Iarratais Shínithe" +signYourself = "Sínigh Tú Féin" +newRequest = "Iarratas Nua" tours = "Turais" +wetSign = "Cuir Síniú Leis" +filterMine = "Mise" +filterOverdue = "Thar téarma" +filterSigned = "Sínithe" +filterDeclined = "Diúltaithe" +searchDocuments = "Cuardaigh doiciméid…" [quickAccess.helpMenu] adminTour = "Turas an Riarthóra" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Tá do fhreastalaí Stirling-PDF as líne agus níl \ expired = "Tá do sheisiún imithe in éag. Athnuaigh an leathanach agus bain triail eile as." refreshPage = "Athnuaigh an Leathanach" +[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" + +[signatureSettings.tooltip] +header = "Socruithe Cuma Sínithe" + +[signatureSettings.tooltip.location] +bullet1 = "Examples: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Ní ionann é agus suíomh an leathanaigh" +bullet3 = "D'fhéadfadh sé a bheith riachtanach i ndlínsí áirithe" +description = "Suíomh geografach roghnach ina cuireadh an síniú i bhfeidhm. Stóráiltear i meiteashonraí an teastais." +title = "Suíomh an tSínithe" + +[signatureSettings.tooltip.logo] +bullet1 = "Le taispeáint in aice leis an síniú agus an téacs" +bullet2 = "Tacaíonn sé le formáidí PNG, JPG" +bullet3 = "Feabhsaíonn sé cuma ghairmiúil" +description = "Cuir lógó cuideachta le sínithe infheicthe chun brandáil agus barántúlacht a threisiú." +title = "Lógó Cuideachta" + +[signatureSettings.tooltip.reason] +bullet1 = "Examples: \"Approval\", \"Contract Agreement\", \"Review Complete\"" +bullet2 = "Le feiceáil in airíonna an tsínithe PDF" +bullet3 = "Fóinteach do rianta iniúchta agus comhlíontacht" +description = "Téacs roghnach ag míniú cén fáth a bhfuil an doiciméad á shíniú. Stóráiltear i meiteashonraí an teastais." +title = "Fáth an tSínithe" + +[signatureSettings.tooltip.visibility] +bullet1 = "Infheicthe: Tá an síniú le feiceáil ar an PDF le cuma shaincheaptha" +bullet2 = "Dofheicthe: Teastas leabaithe gan marc amhairc" +bullet3 = "Soláthraíonn sínithe dofheicthe bailíochtú cripteagrafach fós" +description = "Rialaíonn sé an bhfuil an síniú infheicthe ar an doiciméad nó leabaithe go dofheicthe." +title = "Infheictheacht an tSínithe" + [settings.configuration] advanced = "Ardroghanna" database = "Bunachar Sonraí" endpoints = "Deirphointí" features = "Gnéithe" +storageSharing = "Stóráil Comhad agus Comhroinnt" systemSettings = "Socruithe Córais" title = "Cumraíocht" @@ -6332,10 +6868,13 @@ title = "Sínigh isteach i Stirling" [setup.selfhosted] link = "nó ceangail le cuntas féinóstáilte" subtitle = "Cuir isteach dintiúir do fhreastalaí" +changeServerLocked = "Tá do eagraíocht tar éis an aip seo a shrianadh le freastalaí ar leith" switchToLocal = "Úsáid uirlisí áitiúla ina n-ionad" title = "Sínigh isteach chuig an bhFreastalaí" [setup.selfhosted.unreachable] +changeServer = "Ceangail le freastalaí eile" +changeServerLocked = "Tá do eagraíocht tar éis an aip seo a shrianadh le freastalaí ar leith" continueOffline = "Úsáid uirlisí áitiúla ina n-ionad" message = "Níorbh fhéidir {{url}} a bhaint amach. Seiceáil go bhfuil an freastalaí ag rith agus inrochtana." retry = "Atriail" @@ -6529,6 +7068,15 @@ saved = "Sábháilte" text = "Téacs" title = "Cineál Sínithe" +[signRequest] +declined = "Diúltaíodh an t-iarratas sínithe" +fetchFailed = "Theip ar iarratas sínithe a lódáil" +signed = "D'éirigh le síniú an doiciméid" + +[signSession] +createFailed = "Theip ar iarratas sínithe a chruthú" +created = "Seoladh an t-iarratas sínithe" + [signup] accountCreatedSuccessfully = "Cruthaíodh an cuntas go rathúil! Is féidir leat logáil isteach anois." alreadyHaveAccount = "An bhfuil cuntas agat cheana? Logáil isteach" @@ -6807,6 +7355,106 @@ title = "Scoil PDF de réir Caibidlí" [splitPdfByChapters] tags = "scoilt, caibidlí, leabharmharcanna, eagraigh" +[storageShare] +accessed = "Rochtain déanta" +accessDenied = "Níl rochtain agat ar an gcomhad comhroinnte seo. Iarr ar an úinéir é a roinnt leat." +accessFailed = "Níorbh fhéidir an ghníomhaíocht a lódáil." +accessDeniedBody = "Níl rochtain agat ar an gcomhad seo. Iarr ar an úinéir é a roinnt leat." +accessDeniedTitle = "Gan rochtain" +accessLimitedCommenter = "Tá rochtain tráchtála ag teacht go luath. Iarr ar an úinéir rochtain eagarthóra má theastaíonn uait íoslódáil." +accessLimitedTitle = "Rochtain theoranta" +accessLimitedViewer = "Is le haghaidh amhairc amháin an nasc seo. Iarr ar an úinéir rochtain eagarthóra má theastaíonn uait íoslódáil." +createdAt = "Cruthaithe" +download = "Ãoslódáil" +downloadFailed = "Níorbh fhéidir an comhad seo a íoslódáil." +expiredBody = "Tá an nasc comhroinnte seo neamhbhailí nó tá sé imithe in éag." +expiredTitle = "Nasc imithe in éag" +goToLogin = "Téigh go dtí an logáil isteach" +loadFailed = "Níorbh fhéidir an comhad comhroinnte a oscailt." +loading = "Nasc comhroinnte á lódáil..." +loginPrompt = "Sínigh isteach chun rochtain a fháil ar an gcomhad comhroinnte seo." +loginRequired = "Logáil isteach de dhíth" +openInApp = "Oscail i Stirling PDF" +ownerLabel = "Úinéir" +ownerUnknown = "Anaithnid" +requiresLogin = "Éilíonn an comhad comhroinnte seo logáil isteach." +roleCommenter = "Tráchtaire" +roleEditor = "Eagarthóir" +roleViewer = "Amharcóir" +shareHeading = "Comhad comhroinnte" +titleDefault = "Comhad comhroinnte" +tryAgain = "Bain triail eile as níos déanaí." +addUser = "Cuir leis" +commenterHint = "Tá tráchtáil ag teacht go luath." +copied = "Cóipeáladh an nasc go dtí an ghearrthaisce" +copy = "Cóipeáil" +copyFailed = "Theip ar chóipeáil" +description = "Cruthaigh nasc comhroinnte don chomhad seo. Is féidir le húsáideoirí sínithe isteach leis an nasc rochtain a fháil air." +downloadsCount = "Ãoslódálacha: {{count}}" +emailWarningBody = "Is cosúil gur seoladh ríomhphoist é seo. Mura bhfuil an duine seo ina úsáideoir Stirling PDF cheana, ní bheidh sé in ann rochtain a fháil ar an gcomhad." +emailWarningConfirm = "Comhroinn mar sin féin" +emailWarningTitle = "Seoladh ríomhphoist" +errorTitle = "Theip ar chomhroinnt" +failure = "Níorbh fhéidir nasc comhroinnte a ghiniúint. Bain triail eile as." +fileLabel = "Comhad" +generate = "Gin Nasc" +generated = "Gineadh nasc comhroinnte" +hideActivity = "Folaigh gníomhaíocht" +invalidUsername = "Iontráil ainm úsáideora bailí nó seoladh ríomhphoist." +lastAccessed = "Rochtain dheireanach" +linkAccessTitle = "Rochtain an naisc chomhroinnte" +linkLabel = "Nasc comhroinnte" +linksDisabled = "Tá naisc chomhroinnte díchumasaithe." +linksDisabledBody = "Tá naisc chomhroinnte díchumasaithe ag socruithe do fhreastalaí." +manage = "Bainistigh comhroinnt" +manageDescription = "Cruthaigh agus bainistigh naisc chun an comhad seo a roinnt." +manageLoadFailed = "Níorbh fhéidir naisc chomhroinnte a lódáil." +manageTitle = "Bainistigh Comhroinnt" +noActivity = "Níl aon ghníomhaíocht fós." +noLinks = "Níl aon naisc chomhroinnte gníomhacha fós." +noSharedUsers = "Níl aon úsáideoirí le rochtain fós." +removeLink = "Bain nasc" +removeUser = "Bain" +revokeFailed = "Níorbh fhéidir an nasc comhroinnte a bhaint." +revoked = "Baineadh an nasc roinnte" +roleLabel = "Ról" +sharingDisabled = "Tá roinnt díchumasaithe." +sharingDisabledBody = "Díchumasaíodh roinnt de bharr socruithe do fhreastalaí." +sharedUsersTitle = "Úsáideoirí roinnte" +title = "Roinn comhad" +unknownUser = "Úsáideoir anaithnid" +userAddFailed = "Níorbh fhéidir a roinnt leis an úsáideoir sin." +userAdded = "Cuireadh an t-úsáideoir leis an liosta roinnte." +usernameLabel = "Ainm úsáideora nó ríomhphost" +usernamePlaceholder = "Cuir isteach ainm úsáideora nó ríomhphost" +userRemoveFailed = "Níorbh fhéidir an t-úsáideoir sin a bhaint." +userRemoved = "Baineadh an t-úsáideoir den liosta roinnte." +viewActivity = "Féach ar ghníomhaíocht" +viewed = "Feicthe" +viewsCount = "Amharcanna: {{count}}" +downloaded = "Ãosluchtaithe" +bulkDescription = "Cruthaigh nasc amháin chun na comhaid roghnaithe go léir a roinnt le húsáideoirí logáilte isteach." +bulkTitle = "Roinn na comhaid roghnaithe" +copyLink = "Cóipeáil an nasc roinnte" +fileCount = "{{count}} comhad roghnaithe" +ownerOnly = "Ní féidir ach leis an úinéir roinnt a bhainistiú." +selectSingleFile = "Roghnaigh comhad aonair chun roinnt a bhainistiú." + +[storageUpload] +description = "Uaslódálann sé seo an comhad reatha chuig stóráil an fhreastalaí ionas gur féidir leat é a rochtain." +errorTitle = "Theip ar an uaslódáil" +failure = "Theip ar an uaslódáil. Seiceáil do shocruithe logála isteach agus stórála le do thoil." +fileLabel = "Comhad" +hint = "Rialaítear na naisc phoiblí agus na módanna rochtana le socruithe do fhreastalaí." +success = "Uaslódáladh chuig an bhfreastalaí" +title = "Uaslódáil chuig an bhfreastalaí" +updateButton = "Nuashonraigh ar an bhfreastalaí" +uploadButton = "Uaslódáil chuig an bhfreastalaí" +bulkDescription = "Uaslódálann sé seo na comhaid roghnaithe chuig stóráil do fhreastalaí." +bulkTitle = "Uaslódáil na comhaid roghnaithe" +fileCount = "{{count}} comhad roghnaithe" +more = " +{{count}} tuilleadh" + [storage] approximateSize = "Méid thart" fileTooLarge = "Comhad ró‑mhór. Is é an méid uasta in aghaidh an chomhaid ná" @@ -7153,6 +7801,30 @@ title = "Amharc/Cuir PDF in Eagar" [warning] tooltipTitle = "Rabhadh" +[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" + [watermark] completed = "Comhartha uisce curtha leis" desc = "Cuir comharthaí uisce téacs nó íomhá le comhaid PDF" @@ -7333,6 +8005,7 @@ activeSession = "Seisiún gníomhach" addMembers = "Cuir Baill Leis" admin = "Riarthóir" confirmDelete = "An bhfuil tú cinnte gur mian leat an t-úsáideoir seo a scriosadh? Ní féidir an gníomh seo a chur ar ceal." +confirmUnlock = "An bhfuil tú cinnte gur mian leat an cuntas úsáideora seo a dhíghlasáil?" deleteUser = "Scrios Úsáideoir" deleteUserError = "Theip ar an úsáideoir a scriosadh" deleteUserSuccess = "Scriosadh an t-úsáideoir go rathúil" @@ -7341,6 +8014,8 @@ disable = "Díchumasaigh" disabled = "Díchumasaithe" editRole = "Cuir Ról in Eagar" enable = "Cumasaigh" +locked = "faoi ghlas" +lockedBadge = "Faoi ghlas" loading = "Daoine á lódáil..." loginRequired = "Cumasaigh mód logála isteach ar dtús" member = "Ball" @@ -7350,6 +8025,9 @@ searchMembers = "Cuardaigh baill..." status = "Stádas" team = "Foireann" title = "Daoine" +unlockAccount = "Díghlasáil an cuntas" +unlockUserError = "Theip ar dhíghlasáil an chuntais úsáideora" +unlockUserSuccess = "Díghlasáladh an cuntas úsáideora go rathúil" user = "Úsáideoir" [workspace.people.actions] diff --git a/frontend/public/locales/hi-IN/translation.toml b/frontend/public/locales/hi-IN/translation.toml index c1bbe520af..012c6a72ef 100644 --- a/frontend/public/locales/hi-IN/translation.toml +++ b/frontend/public/locales/hi-IN/translation.toml @@ -8,6 +8,7 @@ black = "काला" blue = "नीला" bored = "इंतज़ार करते हà¥à¤ बोर हो रहे हैं?" cancel = "रदà¥à¤¦ करें" +confirm = "पà¥à¤·à¥à¤Ÿà¤¿ करें" changedCredsMessage = "कà¥à¤°à¥‡à¤¡à¥‡à¤‚शियलà¥à¤¸ बदल दिठगà¤!" chooseFile = "फ़ाइल चà¥à¤¨à¥‡à¤‚" close = "बंद करें" @@ -146,6 +147,7 @@ insufficientCredits = "परà¥à¤¯à¤¾à¤ªà¥à¤¤ कà¥à¤°à¥‡à¤¡à¤¿à¤Ÿà¥à¤¸ न loadingCredits = "कà¥à¤°à¥‡à¤¡à¤¿à¤Ÿà¥à¤¸ की जाà¤à¤š हो रही है..." loadingProStatus = "सदसà¥à¤¯à¤¤à¤¾ सà¥à¤¥à¤¿à¤¤à¤¿ की जाà¤à¤š हो रही है..." noticeTopUpOrPlan = "परà¥à¤¯à¤¾à¤ªà¥à¤¤ कà¥à¤°à¥‡à¤¡à¤¿à¤Ÿà¥à¤¸ नहीं हैं, कृपया टॉप अप करें या किसी पà¥à¤²à¤¾à¤¨ में अपगà¥à¤°à¥‡à¤¡ करें" +accessInvite = "आमंतà¥à¤°à¤¿à¤¤ करें" [account] accountSettings = "खाता सेटिंगà¥à¤¸" @@ -1427,6 +1429,34 @@ title = "पà¥à¤°à¤¸à¤‚सà¥à¤•रण" description = "तà¥à¤°à¥à¤Ÿà¤¿ रिपोरà¥à¤Ÿ करने से पहले पà¥à¤°à¥‹à¤¸à¥‡à¤¸à¤¿à¤‚ग जॉब के लिठपà¥à¤°à¤¤à¥€à¤•à¥à¤·à¤¾ का अधिकतम समय।" label = "पà¥à¤°à¥‹à¤¸à¥‡à¤¸à¤¿à¤‚ग टाइमआउट (सेकंड)" +[admin.settings.storage] +description = "सरà¥à¤µà¤° भंडारण और शेयरिंग विकलà¥à¤ª नियंतà¥à¤°à¤¿à¤¤ करें।" +title = "फ़ाइल भंडारण और शेयरिंग" + +[admin.settings.storage.enabled] +description = "उपयोगकरà¥à¤¤à¤¾à¤“ं को सरà¥à¤µà¤° पर फ़ाइलें संगà¥à¤°à¤¹à¥€à¤¤ करने की अनà¥à¤®à¤¤à¤¿ दें।" +label = "सरà¥à¤µà¤° फ़ाइल भंडारण सकà¥à¤·à¤® करें" + +[admin.settings.storage.sharing.email] +description = "ईमेल पतों के साथ शेयरिंग की अनà¥à¤®à¤¤à¤¿ दें।" +label = "ईमेल शेयरिंग सकà¥à¤·à¤® करें" +mailLink = "मेल सेटिंगà¥à¤¸ कॉनà¥à¤«à¤¼à¤¿à¤—र करें" +mailNote = "मेल कॉनà¥à¤«à¤¼à¤¿à¤—रेशन आवशà¥à¤¯à¤• है। " + +[admin.settings.storage.sharing.enabled] +description = "उपयोगकरà¥à¤¤à¤¾à¤“ं को संगà¥à¤°à¤¹à¥€à¤¤ फ़ाइलें साà¤à¤¾ करने की अनà¥à¤®à¤¤à¤¿ दें।" +label = "शेयरिंग सकà¥à¤·à¤® करें" + +[admin.settings.storage.sharing.links] +description = "साइन-इन लिंक के माधà¥à¤¯à¤® से शेयरिंग की अनà¥à¤®à¤¤à¤¿ दें।" +frontendUrlLink = "सिसà¥à¤Ÿà¤® सेटिंगà¥à¤¸ में कॉनà¥à¤«à¤¼à¤¿à¤—र करें" +frontendUrlNote = "à¤à¤• Frontend URL आवशà¥à¤¯à¤• है। " +label = "शेयर लिंक सकà¥à¤·à¤® करें" + +[admin.settings.storage.signing.enabled] +description = "उपयोगकरà¥à¤¤à¤¾à¤“ं को बहà¥-पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° सतà¥à¤° बनाने की अनà¥à¤®à¤¤à¤¿ दें। इसके लिठसरà¥à¤µà¤° फ़ाइल भंडारण सकà¥à¤·à¤® होना आवशà¥à¤¯à¤• है।" +label = "समूह हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° सकà¥à¤·à¤® करें (अलà¥à¤«à¤¾)" + [admin.settings.unsavedChanges] cancel = "संपादन जारी रखें" discard = "परिवरà¥à¤¤à¤¨ तà¥à¤¯à¤¾à¤—ें" @@ -2059,7 +2089,19 @@ numbers = "संखà¥à¤¯à¤¾à¤à¤/रेंज: 5, 10-20" progressions = "पà¥à¤°à¥‹à¤—à¥à¤°à¥‡à¤¶à¤¨: 3n, 4n+1" [certSign] +allSigned = "सभी पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ियों ने हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° कर दिठहैं। अंतिम रूप देने के लिठतैयार।" +awaitingSignatures = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¥‹à¤‚ की पà¥à¤°à¤¤à¥€à¤•à¥à¤·à¤¾" +signatureProgress = "{{signedCount}}/{{totalCount}} हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°" chooseCertificate = "पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° फ़ाइल चà¥à¤¨à¥‡à¤‚" +declined = "असà¥à¤µà¥€à¤•ृत" +fetchFailed = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° डेटा लोड करने में विफल" +finalized = "अंतिम रूप दिया गया" +notified = "लंबित" +partialNote = "आप वरà¥à¤¤à¤®à¤¾à¤¨ हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¥‹à¤‚ के साथ पहले ही अंतिम रूप दे सकते हैं। बिना हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° वाले पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ियों को बाहर रखा जाà¤à¤—ा।" +pending = "लंबित" +readyToFinalize = "अंतिम रूप देने के लिठतैयार" +signed = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤" +viewed = "देखा गया" chooseJksFile = "JKS फ़ाइल चà¥à¤¨à¥‡à¤‚" chooseP12File = "PKCS12 फ़ाइल चà¥à¤¨à¥‡à¤‚" choosePfxFile = "PFX फ़ाइल चà¥à¤¨à¥‡à¤‚" @@ -2082,6 +2124,7 @@ title = "पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°" invisible = "अदृशà¥à¤¯" stepTitle = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° का रूप" visible = "दृशà¥à¤¯à¤®à¤¾à¤¨" +visibility = "दृशà¥à¤¯à¤¤à¤¾" [certSign.appearance.options] title = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° विवरण" @@ -2188,6 +2231,252 @@ bullet4 = "सतà¥à¤¯à¤¾à¤ªà¤¨ के लिठकसà¥à¤Ÿà¤® पà¥à¤°à¤® text = "जब आप हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¥‹à¤‚ की जाà¤à¤š करते हैं, तो टूल बताता है कि वे वैध हैं या नहीं, किसने दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ पर हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° किà¤, कब किà¤, और हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° के बाद दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ में कोई बदलाव हà¥à¤† है या नहीं।" title = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° की जाà¤à¤š" +[certSign.collab.finalize] +button = "अंतिम रूप दें और हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤ PDF लोड करें" +early = "वरà¥à¤¤à¤®à¤¾à¤¨ हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¥‹à¤‚ के साथ अंतिम रूप दें" + +[certSign.collab.sessionDetail] +addButton = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी जोड़ें" +addParticipants = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी जोड़ें" +addParticipantsError = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी जोड़ने में विफल" +backToList = "सतà¥à¤°à¥‹à¤‚ पर वापस" +deleteConfirm = "कà¥à¤¯à¤¾ आप सà¥à¤¨à¤¿à¤¶à¥à¤šà¤¿à¤¤ हैं? इसे वापस नहीं लिया जा सकता।" +deleteError = "सतà¥à¤° हटाने में विफल" +deleted = "सतà¥à¤° हटाया गया" +deleteSession = "सतà¥à¤° हटाà¤à¤‚" +dueDate = "नियत तिथि" +finalizeError = "सतà¥à¤° अंतिम रूप देने में विफल" +loadPdfError = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤ PDF लोड करने में विफल" +loadSignedPdf = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤ PDF को सकà¥à¤°à¤¿à¤¯ फ़ाइलों में लोड करें" +messageLabel = "संदेश" +noAdditionalInfo = "कोई अतिरिकà¥à¤¤ जानकारी नहीं" +owner = "मालिक" +participantRemoved = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी हटाया गया" +participants = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी" +participantsAdded = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी सफलतापूरà¥à¤µà¤• जोड़े गà¤" +removeParticipant = "हटाà¤à¤" +removeParticipantError = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी हटाने में विफल" +selectUsers = "उपयोगकरà¥à¤¤à¤¾ चà¥à¤¨à¥‡à¤‚..." +sessionInfo = "सतà¥à¤° जानकारी" +workbenchTitle = "सतà¥à¤° पà¥à¤°à¤¬à¤‚धन" + +[certSign.collab.signRequest] +addedToFiles = "दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ सकà¥à¤°à¤¿à¤¯ फ़ाइलों में जोड़ा गया" +addSignature = "अपना हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° जोड़ें" +addToFiles = "सकà¥à¤°à¤¿à¤¯ फ़ाइलों में जोड़ें" +advancedSettings = "उनà¥à¤¨à¤¤ सेटिंगà¥à¤¸" +backToList = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° अनà¥à¤°à¥‹à¤§à¥‹à¤‚ पर वापस" +certificateChoice = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करने के लिठà¤à¤• पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° चà¥à¤¨à¥‡à¤‚" +changeSignature = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° बदलें" +clearSignature = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° साफ़ करें" +completeAndSign = "पूरà¥à¤£ करें और हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करें" +createNewSignature = "नया हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° बनाà¤à¤‚" +declineButton = "असà¥à¤µà¥€à¤•ार करें" +decline = "अनà¥à¤°à¥‹à¤§ असà¥à¤µà¥€à¤•ार करें" +deleteSelected = "चयनित हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° हटाà¤à¤" +drawSignature = "नीचे अपना हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° बनाà¤à¤‚" +dueDate = "नियत तिथि" +fileTooLarge = "फ़ाइल आकार 5MB से कम होना चाहिà¤" +fontFamily = "फ़ॉनà¥à¤Ÿ परिवार" +fontSize = "फ़ॉनà¥à¤Ÿ आकार: {{size}}px" +fontSizePlaceholder = "आकार" +from = "पà¥à¤°à¥‡à¤·à¤•" +invalidCertFile = "कृपया P12 या PFX पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° फ़ाइल चà¥à¤¨à¥‡à¤‚" +invalidFileType = "कृपया à¤à¤• छवि फ़ाइल चà¥à¤¨à¥‡à¤‚" +location = "सà¥à¤¥à¤¾à¤¨ (वैकलà¥à¤ªà¤¿à¤•)" +locationPlaceholder = "आप कहाठसे हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° कर रहे हैं?" +message = "संदेश" +noCertificate = "कृपया à¤à¤• पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° फ़ाइल चà¥à¤¨à¥‡à¤‚" +noSignatures = "कृपया PDF पर कम से कम à¤à¤• हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° रखें" +p12File = "P12/PFX पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° फ़ाइल" +password = "पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° पासवरà¥à¤¡" +passwordPlaceholder = "पासवरà¥à¤¡ दरà¥à¤œ करें..." +penColor = "पेन का रंग" +penSize = "पेन आकार: {{size}}px" +placementActive = "रखने के लिठPDF पर कà¥à¤²à¤¿à¤• करें" +placeSignatureButton = "PDF पर हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° रखें" +reason = "कारण (वैकलà¥à¤ªà¤¿à¤•)" +reasonPlaceholder = "आप कà¥à¤¯à¥‹à¤‚ हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° कर रहे हैं?" +removeImage = "छवि हटाà¤à¤" +removeCertFile = "फ़ाइल हटाà¤à¤" +savedSignatures = "सहेजे गठहसà¥à¤¤à¤¾à¤•à¥à¤·à¤°" +selectFile = "छवि फ़ाइल चà¥à¤¨à¥‡à¤‚" +selectSignatureTitle = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° चà¥à¤¨à¥‡à¤‚ या बनाà¤à¤" +signButton = "दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ पर हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करें" +signatureInfo = "ये सेटिंगà¥à¤¸ दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ के मालिक दà¥à¤µà¤¾à¤°à¤¾ कॉनà¥à¤«à¤¼à¤¿à¤—र की गई हैं" +signaturePlaced = "पृषà¥à¤  पर हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° रखा गया" +signatureSettings = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° सेटिंगà¥à¤¸" +signatureText = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° पाठ" +signatureTextPlaceholder = "अपना नाम दरà¥à¤œ करें..." +signatureTypeLabel = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° पà¥à¤°à¤•ार" +signingTitle = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करना" +textColor = "पाठ का रंग" +typeSignature = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° बनाने के लिठअपना नाम टाइप करें" +uploadCert = "कसà¥à¤Ÿà¤® पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤°" +uploadCertDesc = "अपना P12/PFX पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° उपयोग करें" +uploadSignature = "अपने हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° की छवि अपलोड करें" +usePersonalCert = "वà¥à¤¯à¤•à¥à¤¤à¤¿à¤—त पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤°" +usePersonalCertDesc = "आपके खाते के लिठसà¥à¤µà¤¤à¤ƒ जनरेटेड" +useServerCert = "संगठन पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤°" +useServerCertDesc = "साà¤à¤¾ संगठन पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤°" +workbenchTitle = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° अनà¥à¤°à¥‹à¤§" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "सà¥à¤Ÿà¥à¤°à¥‹à¤• का रंग चà¥à¤¨à¥‡à¤‚" +continue = "जारी रखें" + +[certSign.collab.signRequest.certModal] +description = "आपने {{count}} हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° रखे हैं। हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° पूरà¥à¤£ करने के लिठअपना पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° चà¥à¤¨à¥‡à¤‚।" +sign = "दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ पर हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करें" +certValidating = "पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° सतà¥à¤¯à¤¾à¤ªà¤¿à¤¤ किया जा रहा है..." +certValidUntil = "पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° {{date}} तक मानà¥à¤¯" +certInvalid = "पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° अमानà¥à¤¯: {{error}}" +certInvalidFallback = "अमानà¥à¤¯ पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤°" +certNetworkError = "पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° सतà¥à¤¯à¤¾à¤ªà¤¿à¤¤ नहीं कर सके" +title = "पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° कॉनà¥à¤«à¤¼à¤¿à¤—र करें" + +[certSign.collab.signRequest.image] +hint = "अपने हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° की PNG या JPG छवि अपलोड करें" + +[certSign.collab.signRequest.mode] +move = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° सà¥à¤¥à¤¾à¤¨à¤¾à¤‚तरित करें" +place = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° रखें" +title = "साइन या मूव मोड" + +[certSign.collab.signRequest.modeTabs] +draw = "डà¥à¤°à¤¾" +image = "अपलोड" +text = "टाइप" + +[certSign.collab.signRequest.placeSignature] +message = "अपने हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° रखने के लिठPDF पर कà¥à¤²à¤¿à¤• करें" +title = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° रखें" + +[certSign.collab.signRequest.preview] +imageAlt = "चयनित हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°" +missing = "कोई पूरà¥à¤µà¤¾à¤µà¤²à¥‹à¤•न नहीं" +textFallback = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "डà¥à¤°à¥‰ किया गया हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°" +defaultImageLabel = "अपलोड किया हà¥à¤† हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°" +defaultLabel = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°" +defaultTextLabel = "टाइप किया हà¥à¤† हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°" +delete = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° हटाà¤à¤" +none = "कोई सहेजे गठहसà¥à¤¤à¤¾à¤•à¥à¤·à¤° नहीं" + +[certSign.collab.signRequest.signatureType] +draw = "डà¥à¤°à¤¾" +type = "टाइप" +upload = "अपलोड" + +[certSign.collab.signRequest.steps] +back = "वापस" +cancelPlacement = "रखना रदà¥à¤¦ करें" +certificate = "पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤°" +clickMultipleTimes = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° रखने के लिठPDF पर कई बार कà¥à¤²à¤¿à¤• करें। किसी भी हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° को खींचकर सà¥à¤¥à¤¾à¤¨à¤¾à¤‚तरित या आकार बदलें।" +clickToPlace = "जहाठआप अपना हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° दिखाना चाहते हैं वहाठPDF पर कà¥à¤²à¤¿à¤• करें।" +continue = "पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° चयन पर जारी रखें" +continueToPlacement = "रखने पर जारी रखें" +continueToReview = "समीकà¥à¤·à¤¾ पर जारी रखें" +createSignature = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° बनाà¤à¤‚" +invisible = "अदृशà¥à¤¯" +location = "सà¥à¤¥à¤¾à¤¨:" +multipleSignatures = "{{count}} हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° PDF पर लागू होंगे" +oneSignature = "1 हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° PDF पर लागू होगा" +placeOnPdf = "PDF पर रखें" +reason = "कारण:" +reviewTitle = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करने से पहले समीकà¥à¤·à¤¾ करें" +signaturePlaced = "पृषà¥à¤  {{page}} पर हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° रखा गया। आप पà¥à¤¨à¤ƒ कà¥à¤²à¤¿à¤• करके सà¥à¤¥à¤¿à¤¤à¤¿ समायोजित कर सकते हैं या समीकà¥à¤·à¤¾ पर आगे बढ़ें।" +visible = "दृशà¥à¤¯à¤®à¤¾à¤¨" +visibility = "दृशà¥à¤¯à¤¤à¤¾:" +yourSignatures = "आपके हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "रंग" +fontLabel = "फ़ॉनà¥à¤Ÿ" +fontSizeLabel = "आकार" +fontSizePlaceholder = "16" +label = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° पाठ" +modalHint = "अपना नाम दरà¥à¤œ करें, फिर इसे PDF पर रखने के लिठजारी रखें पर कà¥à¤²à¤¿à¤• करें।" +placeholder = "अपना नाम दरà¥à¤œ करें..." + +[certSign.collab.participant] +certValidating = "पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° सतà¥à¤¯à¤¾à¤ªà¤¿à¤¤ किया जा रहा है..." +certValid = "✓ पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° मानà¥à¤¯" +certValidUntil = " {{date}} तक" +certInvalid = "✗ {{error}}" +certInvalidFallback = "अमानà¥à¤¯ पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤°" +certNetworkError = "पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° सतà¥à¤¯à¤¾à¤ªà¤¿à¤¤ नहीं कर सके" + +[certSign.collab.addParticipants] +add = "{{count}} पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी जोड़ें" +back = "वापस" +configureSignatures = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° सेटिंगà¥à¤¸ कॉनà¥à¤«à¤¼à¤¿à¤—र करें" +continue = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° सेटिंगà¥à¤¸ पर जारी रखें" +reasonHelp = "इन पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ियों के लिठपहले से हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° का कारण निरà¥à¤§à¤¾à¤°à¤¿à¤¤ करें (वैकलà¥à¤ªà¤¿à¤•, वे हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करते समय बदल सकते हैं)" +reasonPlaceholder = "जैसे: मंजूरी, समीकà¥à¤·à¤¾..." +selectUsers = "उपयोगकरà¥à¤¤à¤¾ चà¥à¤¨à¥‡à¤‚" + +[certSign.collab.sessionCreation] +includeSummaryPage = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° सारांश पृषà¥à¤  शामिल करें" +includeSummaryPageHelp = "अंत में सभी हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° मेटाडेटा के साथ à¤à¤• सारांश पृषà¥à¤  जोड़ा जाà¤à¤—ा। वà¥à¤¯à¤•à¥à¤¤à¤¿à¤—त पृषà¥à¤ à¥‹à¤‚ पर डिजिटल पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° बॉकà¥à¤¸ दबा दिठजाà¤à¤‚गे (वेट हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¥‹à¤‚ पर कोई पà¥à¤°à¤­à¤¾à¤µ नहीं पड़ेगा)।" + +[certSign.collab.sessionList] +active = "सकà¥à¤°à¤¿à¤¯" +finalized = "अंतिम रूप दिया गया" + +[certSign.collab.signatureSettings] +description = "सभी पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ियों के लिठहसà¥à¤¤à¤¾à¤•à¥à¤·à¤° कैसे दिखेंगे, कॉनà¥à¤«à¤¼à¤¿à¤—र करें" +title = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° रूप" + +[certSign.collab.userSelector] +inviteUsers = "उपयोगकरà¥à¤¤à¤¾ जोड़ें" +loadError = "उपयोगकरà¥à¤¤à¤¾ लोड करने में विफल" +noTeam = "कोई टीम नहीं" +noUsers = "अनà¥à¤¯ कोई उपयोगकरà¥à¤¤à¤¾ नहीं मिला।" +placeholder = "उपयोगकरà¥à¤¤à¤¾ चà¥à¤¨à¥‡à¤‚..." + +[certSign.mobile] +panelActions = "कà¥à¤°à¤¿à¤¯à¤¾à¤à¤‚" +panelDocument = "दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼" +panelPeople = "लोग" + +[certSign.sessions] +deleted = "सतà¥à¤° हटाया गया" +fetchFailed = "सतà¥à¤° विवरण लोड करने में विफल" +finalized = "सतà¥à¤° अंतिम रूप दिया गया" +loaded = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤ PDF लोड हो गया" +pdfNotReady = "PDF तैयार नहीं है" +pdfNotReadyDesc = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤ PDF जनरेट किया जा रहा है। कृपया थोड़ी देर बाद पà¥à¤¨à¤ƒ पà¥à¤°à¤¯à¤¾à¤¸ करें।" + +[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 अपलोड करें" + [changeCreds] changePassword = "आप डिफ़ॉलà¥à¤Ÿ लॉगिन कà¥à¤°à¥‡à¤¡à¥‡à¤‚शियलà¥à¤¸ का उपयोग कर रहे हैं। कृपया à¤à¤• नया पासवरà¥à¤¡ दरà¥à¤œ करें" changeUsername = "अपना उपयोगकरà¥à¤¤à¤¾ नाम अपडेट करें। अपडेट के बाद आप लॉगआउट हो जाà¤à¤à¤—े।" @@ -3242,6 +3531,46 @@ totalSelected = "कà¥à¤² चयनित" unsupported = "असमरà¥à¤¥à¤¿à¤¤" unzip = "अनज़िप" uploadError = "कà¥à¤› फ़ाइलें अपलोड करने में विफल।" +copyCreated = "कॉपी इस डिवाइस पर सहेजी गई।" +copyFailed = "कॉपी नहीं बना सके।" +leaveShare = "मेरी सूची से हटाà¤à¤" +leaveShareFailed = "साà¤à¤¾ फ़ाइल नहीं हटा सके।" +leaveShareSuccess = "आपकी साà¤à¤¾ सूची से हटाया गया।" +removeBoth = "दोनों से हटाà¤à¤" +removeFilePrompt = "यह फ़ाइल इस डिवाइस और आपके सरà¥à¤µà¤° पर सहेजी गई है। आप इसे कहाठसे हटाना चाहेंगे?" +removeFileTitle = "फ़ाइल हटाà¤à¤" +removeLocalOnly = "केवल इस डिवाइस से" +removeServerFailed = "फ़ाइल को सरà¥à¤µà¤° से नहीं हटा सके।" +removeServerOnly = "केवल सरà¥à¤µà¤° से" +removeServerOnlyPrompt = "यह फ़ाइल केवल आपके सरà¥à¤µà¤° पर संगà¥à¤°à¤¹à¥€à¤¤ है। कà¥à¤¯à¤¾ आप इसे सरà¥à¤µà¤° से हटाना चाहते हैं?" +removeServerSuccess = "सरà¥à¤µà¤° से हटाया गया।" +removeSharedPrompt = "यह फ़ाइल आपके साथ साà¤à¤¾ की गई है। आप इसे इस डिवाइस से या अपनी साà¤à¤¾ सूची से हटा सकते हैं।" +removeSharedServerOnlyBlockedPrompt = "यह फ़ाइल आपके साथ साà¤à¤¾ की गई है और केवल सरà¥à¤µà¤° पर संगà¥à¤°à¤¹à¥€à¤¤ है।" +removeSharedServerOnlyPrompt = "यह फ़ाइल आपके साथ साà¤à¤¾ की गई है और केवल सरà¥à¤µà¤° पर संगà¥à¤°à¤¹à¥€à¤¤ है। कà¥à¤¯à¤¾ इसे अपनी सूची से हटाà¤à¤?" +changesNotUploaded = "परिवरà¥à¤¤à¤¨ अपलोड नहीं हà¥à¤" +cloudFile = "कà¥à¤²à¤¾à¤‰à¤¡ फ़ाइल" +filterAll = "सभी" +filterLocal = "सà¥à¤¥à¤¾à¤¨à¥€à¤¯" +filterSharedByMe = "मेरे दà¥à¤µà¤¾à¤°à¤¾ साà¤à¤¾" +filterSharedWithMe = "मेरे साथ साà¤à¤¾" +lastSynced = "अंतिम सिंक" +localOnly = "केवल सà¥à¤¥à¤¾à¤¨à¥€à¤¯" +makeCopy = "à¤à¤• पà¥à¤°à¤¤à¤¿ बनाà¤à¤" +owner = "मालिक" +ownerUnknown = "अजà¥à¤žà¤¾à¤¤" +share = "शेयर करें" +shareSelected = "चयनित साà¤à¤¾ करें" +sharedByYou = "आपके दà¥à¤µà¤¾à¤°à¤¾ साà¤à¤¾" +sharedEditNoticeBody = "आपके पास इस फ़ाइल के सरà¥à¤µà¤° संसà¥à¤•रण को संपादित करने का अधिकार नहीं है। आपके दà¥à¤µà¤¾à¤°à¤¾ किठगठकिसी भी संपादन को à¤à¤• सà¥à¤¥à¤¾à¤¨à¥€à¤¯ पà¥à¤°à¤¤à¤¿ के रूप में सहेजा जाà¤à¤—ा।" +sharedEditNoticeConfirm = "ठीक है" +sharedEditNoticeTitle = "सिरà¥à¤« पढ़ने योगà¥à¤¯ सरà¥à¤µà¤° पà¥à¤°à¤¤à¤¿" +sharedWithYou = "आपके साथ साà¤à¤¾" +sharing = "शेयरिंग" +storageState = "भंडारण" +synced = "सिंक किया गया" +updateOnServer = "सरà¥à¤µà¤° पर अपडेट करें" +uploadSelected = "चयनित अपलोड करें" +uploadToServer = "सरà¥à¤µà¤° पर अपलोड करें" [files] addFiles = "फ़ाइलें जोड़ें" @@ -3367,6 +3696,77 @@ title = "PDF फà¥à¤²à¥ˆà¤Ÿà¤¨ करने के बारे में" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "समूह हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° के बारे में" + +[groupSigning.tooltip.finalization] +bullet1 = "सभी हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° आपके दà¥à¤µà¤¾à¤°à¤¾ निरà¥à¤¦à¤¿à¤·à¥à¤Ÿ पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी कà¥à¤°à¤® में लागू होते हैं" +bullet2 = "आवशà¥à¤¯à¤• होने पर आप आंशिक हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¥‹à¤‚ के साथ अंतिम रूप दे सकते हैं" +bullet3 = "à¤à¤• बार अंतिम रूप देने के बाद, सतà¥à¤° संशोधित नहीं किया जा सकता" +description = "जब सभी पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° कर लें (या आप पहले अंतिम रूप देना चà¥à¤¨à¥‡à¤‚), तो आप अंतिम हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤ PDF जनरेट कर सकते हैं।" +title = "अंतिमकरण पà¥à¤°à¤•à¥à¤°à¤¿à¤¯à¤¾" + +[groupSigning.tooltip.roles] +bullet1 = "मालिक (आप): सतà¥à¤° बनाते हैं, डिफ़ॉलà¥à¤Ÿ हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° कॉनà¥à¤«à¤¼à¤¿à¤—र करते हैं, दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ को अंतिम रूप देते हैं" +bullet2 = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी: अपना हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° बनाते हैं, पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° चà¥à¤¨à¤¤à¥‡ हैं, PDF पर रखते हैं" +bullet3 = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° की दृशà¥à¤¯à¤¤à¤¾, कारण, या सà¥à¤¥à¤¾à¤¨ सेटिंगà¥à¤¸ संशोधित नहीं कर सकते" +description = "आप सभी पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ियों के लिठहसà¥à¤¤à¤¾à¤•à¥à¤·à¤° रूप सेटिंगà¥à¤¸ नियंतà¥à¤°à¤¿à¤¤ करते हैं।" +title = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी भूमिकाà¤à¤‚" + +[groupSigning.tooltip.sequential] +bullet1 = "दूसरा पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ तक पहà¥à¤à¤šà¤¨à¥‡ से पहले पहले को हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करना होगा" +bullet2 = "कानूनी अनà¥à¤ªà¤¾à¤²à¤¨ के लिठसही हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° कà¥à¤°à¤® सà¥à¤¨à¤¿à¤¶à¥à¤šà¤¿à¤¤ करता है" +bullet3 = "आप सूची में पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ियों को खींचकर उनका कà¥à¤°à¤® बदल सकते हैं" +description = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी आपके दà¥à¤µà¤¾à¤°à¤¾ निरà¥à¤¦à¤¿à¤·à¥à¤Ÿ कà¥à¤°à¤® में दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼à¥‹à¤‚ पर हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करते हैं। जब किसी की बारी आती है तो पà¥à¤°à¤¤à¥à¤¯à¥‡à¤• साइनर को सूचना मिलती है।" +title = "कà¥à¤°à¤®à¤µà¤¾à¤° हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°" + +[groupSigning.steps] +back = "वापस" +completed = "पूरà¥à¤£" +current = "वरà¥à¤¤à¤®à¤¾à¤¨" +stepLabel = "चरण {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "समीकà¥à¤·à¤¾ पर जारी रखें" +invisible = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° अदृशà¥à¤¯ होंगे (केवल मेटाडेटा)" +locationLabel = "सà¥à¤¥à¤¾à¤¨:" +preview = "पूरà¥à¤µà¤¾à¤µà¤²à¥‹à¤•न" +reasonLabel = "कारण:" +title = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° सेटिंगà¥à¤¸ कॉनà¥à¤«à¤¼à¤¿à¤—र करें" +visible = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° पृषà¥à¤  {{page}} पर दृशà¥à¤¯ होंगे" + +[groupSigning.steps.review] +document = "दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼" +dueDate = "नियत तिथि (वैकलà¥à¤ªà¤¿à¤•)" +dueDatePlaceholder = "नियत तिथि चà¥à¤¨à¥‡à¤‚..." +invisible = "अदृशà¥à¤¯ (केवल मेटाडेटा)" +location = "सà¥à¤¥à¤¾à¤¨:" +logo = "लोगो:" +logoHidden = "कोई लोगो नहीं" +logoShown = "Stirling PDF लोगो दिखाया गया" +participants = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी" +reason = "कारण:" +send = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° अनà¥à¤°à¥‹à¤§ भेजें" +signatureSettings = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° सेटिंगà¥à¤¸" +title = "सतà¥à¤° विवरण की समीकà¥à¤·à¤¾ करें" +titleShort = "समीकà¥à¤·à¤¾ करें और भेजें" +visibility = "दृशà¥à¤¯à¤¤à¤¾:" +visible = "पृषà¥à¤  {{page}} पर दृशà¥à¤¯" +participantCount = "{{count}} पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी कà¥à¤°à¤® में हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करेंगे" + +[groupSigning.steps.selectDocument] +continue = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी चयन पर जारी रखें" +noFile = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° सतà¥à¤° बनाने के लिठकृपया अपनी सकà¥à¤°à¤¿à¤¯ फ़ाइलों से à¤à¤•ल PDF फ़ाइल चà¥à¤¨à¥‡à¤‚।" +selectedFile = "चयनित दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼" +title = "दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ चà¥à¤¨à¥‡à¤‚" + +[groupSigning.steps.selectParticipants] +continue = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° सेटिंगà¥à¤¸ पर जारी रखें" +count = "{{count}} पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी चà¥à¤¨à¥‡ गà¤" +label = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी चà¥à¤¨à¥‡à¤‚" +placeholder = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करने के लिठपà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी चà¥à¤¨à¥‡à¤‚..." +title = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी चà¥à¤¨à¥‡à¤‚" + [getPdfInfo] downloadJson = "JSON डाउनलोड करें" downloads = "डाउनलोड" @@ -4460,7 +4860,10 @@ zoomOut = "ज़ूम आउट" [viewer] cannotPreviewFile = "फ़ाइल का पूरà¥à¤µà¤¾à¤µà¤²à¥‹à¤•न नहीं किया जा सकता" +disableColorFilter = "रंग फ़िलà¥à¤Ÿà¤° अकà¥à¤·à¤® करें" dualPageView = "दोहरा पृषà¥à¤  दृशà¥à¤¯" +enableDarkFilter = "डारà¥à¤• फ़िलà¥à¤Ÿà¤° सकà¥à¤·à¤® करें" +enableSepiaFilter = "सेपिया फ़िलà¥à¤Ÿà¤° सकà¥à¤·à¤® करें" firstPage = "पहला पृषà¥à¤ " lastPage = "अंतिम पृषà¥à¤ " nextPage = "अगला पृषà¥à¤ " @@ -4470,6 +4873,22 @@ singlePageView = "à¤à¤•ल पृषà¥à¤  दृशà¥à¤¯" unknownFile = "अजà¥à¤žà¤¾à¤¤ फ़ाइल" zoomIn = "ज़ूम इन" zoomOut = "ज़ूम आउट" +resetZoom = "ज़ूम रीसेट करें" + +[viewer.nonPdf] +fileTypeBadge = "{{type}} फ़ाइल" +convertToPdf = "PDF में बदलें" +loading = "लोड हो रहा है..." +emptyFile = "खाली फ़ाइल" +csvStats = "{{rows}} पंकà¥à¤¤à¤¿à¤¯à¤¾à¤ · {{columns}} सà¥à¤¤à¤‚भ · {{size}}" +sortedBy = "इसके अनà¥à¤¸à¤¾à¤° कà¥à¤°à¤®à¤¬à¤¦à¥à¤§: {{column}}" +columnDefault = "सà¥à¤¤à¤‚भ {{index}}" +htmlPreviewWarning = "HTML पूरà¥à¤µà¤¾à¤µà¤²à¥‹à¤•न — बाहरी संसाधन लोड नहीं हो सकते · {{size}}" +htmlPreview = "HTML पूरà¥à¤µà¤¾à¤µà¤²à¥‹à¤•न" +invalidJson = "अवैध JSON — कचà¥à¤šà¥€ सामगà¥à¤°à¥€ दिखाई जा रही है" +textStats = "{{lines}} पंकà¥à¤¤à¤¿à¤¯à¤¾à¤ · {{size}}" +lineNumbers = "पंकà¥à¤¤à¤¿ संखà¥à¤¯à¤¾à¤à¤" +renderMarkdown = "Markdown रेंडर करें" [viewer.attachments] title = "संलगà¥à¤¨à¤•" @@ -4531,6 +4950,7 @@ toggleAttachments = "संलगà¥à¤¨à¤• दिखाà¤à¤‚/छिपाà¤à¤‚ toggleTheme = "थीम टॉगल करें" language = "भाषा" toggleAnnotations = "à¤à¤¨à¥‹à¤Ÿà¥‡à¤¶à¤¨ दृशà¥à¤¯à¤¤à¤¾ टॉगल करें" +toggleLayers = "परतें टॉगल करें" search = "PDF खोजें" panMode = "पैन मोड" applyRedactionsFirst = "पहले रिडैकà¥à¤¶à¤¨ लागू करें" @@ -5407,20 +5827,72 @@ title = "फ़ाइल पà¥à¤°à¤¿à¤‚ट करें" 2 = "पà¥à¤°à¤¿à¤‚टर नाम दरà¥à¤œ करें" [quickAccess] +access = "पहà¥à¤à¤š" +accessAddPerson = "à¤à¤• और वà¥à¤¯à¤•à¥à¤¤à¤¿ जोड़ें" +accessBack = "वापस" +accessCopyLink = "लिंक कॉपी करें" +accessEmail = "ईमेल पता" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "फ़ाइल" +accessGeneral = "सामानà¥à¤¯ पहà¥à¤à¤š" +accessInviteTitle = "लोगों को आमंतà¥à¤°à¤¿à¤¤ करें" +accessOwner = "मालिक" +accessPanel = "दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ पहà¥à¤à¤š" +accessPeople = "जिनà¥à¤¹à¥‡à¤‚ पहà¥à¤à¤š है" +accessRemove = "हटाà¤à¤" +accessRestricted = "पà¥à¤°à¤¤à¤¿à¤¬à¤‚धित" +accessRestrictedHint = "केवल जिनके पास पहà¥à¤à¤š है वे खोल सकते हैं" +accessRole = "भूमिका" +accessRoleCommenter = "टिपà¥à¤ªà¤£à¥€à¤•ार" +accessRoleEditor = "संपादक" +accessRoleViewer = "दरà¥à¤¶à¤•" +accessSelectedFile = "चयनित फ़ाइल" +accessSendInvite = "आमंतà¥à¤°à¤£ भेजें" +accessTitle = "दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ पहà¥à¤à¤š" +accessYou = "आप" account = "खाता" +activeSessions = "सकà¥à¤°à¤¿à¤¯ सतà¥à¤°" +activeTab = "सकà¥à¤°à¤¿à¤¯" activity = "गतिविधि" adminSettings = "à¤à¤¡à¤®à¤¿à¤¨ सेटिंगà¥à¤¸" +allSessions = "सभी सतà¥à¤°" allTools = "All Tools" automate = "सà¥à¤µà¤šà¤¾à¤²à¤¿à¤¤ करें" +back = "वापस" +certSign = "पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°" +completedSessions = "पूरà¥à¤£ सतà¥à¤°" +completedTab = "पूरà¥à¤£" config = "कॉनà¥à¤«à¤¼à¤¿à¤—" +createNew = "नया अनà¥à¤°à¥‹à¤§ बनाà¤à¤" +createSession = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° अनà¥à¤°à¥‹à¤§ बनाà¤à¤‚" +dueDate = "नियत तिथि (वैकलà¥à¤ªà¤¿à¤•)" files = "फ़ाइलें" help = "सहायता" +noActiveSessions = "कोई लंबित हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° अनà¥à¤°à¥‹à¤§ या सकà¥à¤°à¤¿à¤¯ सतà¥à¤° नहीं" +noCompletedSessions = "कोई पूरà¥à¤£ सतà¥à¤° नहीं" +noFile = "कोई फ़ाइल चयनित नहीं" read = "पढ़ें" reader = "रीडर" +refresh = "रिफà¥à¤°à¥‡à¤¶" +requestSignatures = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° का अनà¥à¤°à¥‹à¤§ करें" +selectSingleFileToRequest = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° अनà¥à¤°à¥‹à¤§ के लिठà¤à¤•ल PDF फ़ाइल चà¥à¤¨à¥‡à¤‚" +selectedFile = "चयनित फ़ाइल" +selectUsers = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° के लिठउपयोगकरà¥à¤¤à¤¾ चà¥à¤¨à¥‡à¤‚" +selectUsersPlaceholder = "पà¥à¤°à¤¤à¤¿à¤­à¤¾à¤—ी चà¥à¤¨à¥‡à¤‚..." +sendingRequest = "भेजा जा रहा है..." settings = "सेटिंगà¥à¤¸" showMeAround = "मà¥à¤à¥‡ दिखाà¤à¤" sign = "साइन" +signatureRequests = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° अनà¥à¤°à¥‹à¤§" +signYourself = "सà¥à¤µà¤¯à¤‚ हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° करें" +newRequest = "नया अनà¥à¤°à¥‹à¤§" tours = "टूर" +wetSign = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° जोड़ें" +filterMine = "मेरे" +filterOverdue = "अतिदेय" +filterSigned = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤" +filterDeclined = "असà¥à¤µà¥€à¤•ृत" +searchDocuments = "दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ खोजें…" [quickAccess.helpMenu] adminTour = "à¤à¤¡à¤®à¤¿à¤¨ टूर" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "आपका Stirling-PDF सरà¥à¤µà¤° ऑफ़ expired = "आपका सतà¥à¤° समापà¥à¤¤ हो गया है। कृपया पृषà¥à¤  को रिफà¥à¤°à¥‡à¤¶ करें और पà¥à¤¨: पà¥à¤°à¤¯à¤¾à¤¸ करें।" refreshPage = "पृषà¥à¤  रिफà¥à¤°à¥‡à¤¶ करें" +[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 = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° कà¥à¤°à¤®" + +[signatureSettings.tooltip] +header = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° रूप सेटिंगà¥à¤¸" + +[signatureSettings.tooltip.location] +bullet1 = "उदाहरण: \"नà¥à¤¯à¥‚यॉरà¥à¤•, USA\", \"लंदन ऑफिस\", \"रिमोट\"" +bullet2 = "पृषà¥à¤  सà¥à¤¥à¤¿à¤¤à¤¿ जैसा नहीं है" +bullet3 = "कà¥à¤› कानूनी अधिकार कà¥à¤·à¥‡à¤¤à¥à¤°à¥‹à¤‚ के लिठआवशà¥à¤¯à¤• हो सकता है" +description = "वैकलà¥à¤ªà¤¿à¤• भौगोलिक सà¥à¤¥à¤¾à¤¨ जहाठहसà¥à¤¤à¤¾à¤•à¥à¤·à¤° लागू किया गया। पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° मेटाडेटा में संगà¥à¤°à¤¹à¥€à¤¤à¥¤" +title = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° सà¥à¤¥à¤¾à¤¨" + +[signatureSettings.tooltip.logo] +bullet1 = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° और पाठ के साथ पà¥à¤°à¤¦à¤°à¥à¤¶à¤¿à¤¤" +bullet2 = "PNG, JPG फ़ॉरà¥à¤®à¥ˆà¤Ÿ समरà¥à¤¥à¤¿à¤¤" +bullet3 = "वà¥à¤¯à¤¾à¤µà¤¸à¤¾à¤¯à¤¿à¤• रूप को बेहतर बनाता है" +description = "बà¥à¤°à¤¾à¤‚डिंग और पà¥à¤°à¤¾à¤®à¤¾à¤£à¤¿à¤•ता के लिठदृशà¥à¤¯à¤®à¤¾à¤¨ हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¥‹à¤‚ में कंपनी लोगो जोड़ें।" +title = "कंपनी लोगो" + +[signatureSettings.tooltip.reason] +bullet1 = "उदाहरण: \"मंजूरी\", \"अनà¥à¤¬à¤‚ध समà¤à¥Œà¤¤à¤¾\", \"समीकà¥à¤·à¤¾ पूरà¥à¤£\"" +bullet2 = "PDF हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° गà¥à¤£à¥‹à¤‚ में दृशà¥à¤¯" +bullet3 = "ऑडिट टà¥à¤°à¥‡à¤² और अनà¥à¤ªà¤¾à¤²à¤¨ के लिठउपयोगी" +description = "दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ पर हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° कà¥à¤¯à¥‹à¤‚ किठजा रहे हैं, इसका वैकलà¥à¤ªà¤¿à¤• पाठ। पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° मेटाडेटा में संगà¥à¤°à¤¹à¥€à¤¤à¥¤" +title = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° का कारण" + +[signatureSettings.tooltip.visibility] +bullet1 = "दृशà¥à¤¯à¤®à¤¾à¤¨: कसà¥à¤Ÿà¤® रूप के साथ PDF पर हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° दिखाई देंगे" +bullet2 = "अदृशà¥à¤¯: बिना दृशà¥à¤¯ चिहà¥à¤¨ के पà¥à¤°à¤®à¤¾à¤£à¤ªà¤¤à¥à¤° à¤à¤®à¥à¤¬à¥‡à¤¡ होगा" +bullet3 = "अदृशà¥à¤¯ हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° अभी भी कà¥à¤°à¤¿à¤ªà¥à¤Ÿà¥‹à¤—à¥à¤°à¤¾à¤«à¤¼à¤¿à¤• मानà¥à¤¯à¤¤à¤¾ पà¥à¤°à¤¦à¤¾à¤¨ करते हैं" +description = "नियंतà¥à¤°à¤¿à¤¤ करता है कि हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ पर दृशà¥à¤¯à¤®à¤¾à¤¨ हो या अदृशà¥à¤¯ रूप से à¤à¤®à¥à¤¬à¥‡à¤¡ हो।" +title = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° की दृशà¥à¤¯à¤¤à¤¾" + [settings.configuration] advanced = "à¤à¤¡à¤µà¤¾à¤‚सà¥à¤¡" database = "डेटाबेस" endpoints = "à¤à¤‚डपॉइंटà¥à¤¸" features = "फ़ीचरà¥à¤¸" +storageSharing = "फ़ाइल भंडारण और शेयरिंग" systemSettings = "सिसà¥à¤Ÿà¤® सेटिंगà¥à¤¸" title = "कॉनà¥à¤«à¤¼à¤¿à¤—रेशन" @@ -6332,10 +6868,13 @@ title = "Stirling में साइन इन" [setup.selfhosted] link = "या किसी सà¥à¤µ-होसà¥à¤Ÿà¥‡à¤¡ खाते से कनेकà¥à¤Ÿ करें" subtitle = "अपने सरà¥à¤µà¤° कà¥à¤°à¥‡à¤¡à¥‡à¤‚शियलà¥à¤¸ दरà¥à¤œ करें" +changeServerLocked = "आपके संगठन ने इस à¤à¤ª को à¤à¤• विशिषà¥à¤Ÿ सरà¥à¤µà¤° तक सीमित कर दिया है" switchToLocal = "इसके बजाय लोकल टूलà¥à¤¸ का उपयोग करें" title = "सरà¥à¤µà¤° में साइन इन" [setup.selfhosted.unreachable] +changeServer = "किसी अनà¥à¤¯ सरà¥à¤µà¤° से कनेकà¥à¤Ÿ करें" +changeServerLocked = "आपके संगठन ने इस à¤à¤ª को à¤à¤• विशिषà¥à¤Ÿ सरà¥à¤µà¤° तक सीमित कर दिया है" continueOffline = "इसके बजाय लोकल टूलà¥à¤¸ का उपयोग करें" message = "{{url}} तक पहà¥à¤à¤šà¤¾ नहीं जा सका। जाà¤à¤šà¥‡à¤‚ कि सरà¥à¤µà¤° चल रहा है और सà¥à¤²à¤­ है।" retry = "पà¥à¤¨à¤ƒ पà¥à¤°à¤¯à¤¾à¤¸ करें" @@ -6529,6 +7068,15 @@ saved = "सहेजा गया" text = "टेकà¥à¤¸à¥à¤Ÿ" title = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° पà¥à¤°à¤•ार" +[signRequest] +declined = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° अनà¥à¤°à¥‹à¤§ असà¥à¤µà¥€à¤•ृत" +fetchFailed = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° अनà¥à¤°à¥‹à¤§ लोड करने में विफल" +signed = "दसà¥à¤¤à¤¾à¤µà¥‡à¤œà¤¼ सफलतापूरà¥à¤µà¤• हसà¥à¤¤à¤¾à¤•à¥à¤·à¤°à¤¿à¤¤" + +[signSession] +createFailed = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° अनà¥à¤°à¥‹à¤§ बनाने में विफल" +created = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° अनà¥à¤°à¥‹à¤§ भेजा गया" + [signup] accountCreatedSuccessfully = "खाता सफलतापूरà¥à¤µà¤• बनाया गया! अब आप साइन इन कर सकते हैं।" alreadyHaveAccount = "पहले से खाता है? साइन इन करें" @@ -6807,6 +7355,106 @@ title = "अधà¥à¤¯à¤¾à¤¯à¥‹à¤‚ दà¥à¤µà¤¾à¤°à¤¾ PDF विभाजित [splitPdfByChapters] tags = "विभाजन,अधà¥à¤¯à¤¾à¤¯,बà¥à¤•मारà¥à¤•,वà¥à¤¯à¤µà¤¸à¥à¤¥à¤¿à¤¤ करें" +[storageShare] +accessed = "à¤à¤•à¥à¤¸à¥‡à¤¸ किया गया" +accessDenied = "आपको इस साà¤à¤¾ फ़ाइल की पहà¥à¤à¤š नहीं है। मालिक से इसे आपके साथ साà¤à¤¾ करने के लिठकहें।" +accessFailed = "गतिविधि लोड करने में असमरà¥à¤¥à¥¤" +accessDeniedBody = "आपको इस फ़ाइल की पहà¥à¤à¤š नहीं है। मालिक से इसे आपके साथ साà¤à¤¾ करने के लिठकहें।" +accessDeniedTitle = "पहà¥à¤à¤š नहीं" +accessLimitedCommenter = "टिपà¥à¤ªà¤£à¥€ पहà¥à¤à¤š जलà¥à¤¦ ही आ रही है। यदि आपको डाउनलोड करना है तो मालिक से संपादक पहà¥à¤à¤š माà¤à¤—ें।" +accessLimitedTitle = "सीमित पहà¥à¤à¤š" +accessLimitedViewer = "यह लिंक केवल देखने के लिठहै। यदि आपको डाउनलोड करना है तो मालिक से संपादक पहà¥à¤à¤š माà¤à¤—ें।" +createdAt = "बनाया गया" +download = "डाउनलोड" +downloadFailed = "इस फ़ाइल को डाउनलोड करने में असमरà¥à¤¥à¥¤" +expiredBody = "यह शेयर लिंक अमानà¥à¤¯ है या इसकी मियाद समापà¥à¤¤ हो गई है।" +expiredTitle = "लिंक समापà¥à¤¤" +goToLogin = "लॉगिन पर जाà¤à¤" +loadFailed = "साà¤à¤¾ फ़ाइल खोलने में असमरà¥à¤¥à¥¤" +loading = "शेयर लिंक लोड हो रहा है..." +loginPrompt = "इस साà¤à¤¾ फ़ाइल तक पहà¥à¤à¤šà¤¨à¥‡ के लिठसाइन इन करें।" +loginRequired = "लॉगिन आवशà¥à¤¯à¤•" +openInApp = "Stirling PDF में खोलें" +ownerLabel = "मालिक" +ownerUnknown = "अजà¥à¤žà¤¾à¤¤" +requiresLogin = "इस साà¤à¤¾ फ़ाइल के लिठलॉगिन आवशà¥à¤¯à¤• है।" +roleCommenter = "टिपà¥à¤ªà¤£à¥€à¤•ार" +roleEditor = "संपादक" +roleViewer = "दरà¥à¤¶à¤•" +shareHeading = "साà¤à¤¾ फ़ाइल" +titleDefault = "साà¤à¤¾ फ़ाइल" +tryAgain = "कृपया बाद में पà¥à¤¨à¤ƒ पà¥à¤°à¤¯à¤¾à¤¸ करें।" +addUser = "जोड़ें" +commenterHint = "टिपà¥à¤ªà¤£à¥€ करना जलà¥à¤¦ ही आ रहा है।" +copied = "लिंक कà¥à¤²à¤¿à¤ªà¤¬à¥‹à¤°à¥à¤¡ पर कॉपी किया गया" +copy = "कॉपी" +copyFailed = "कॉपी विफल" +description = "इस फ़ाइल के लिठà¤à¤• शेयर लिंक बनाà¤à¤à¥¤ लिंक के साथ साइन-इन उपयोगकरà¥à¤¤à¤¾ इसे à¤à¤•à¥à¤¸à¥‡à¤¸ कर सकते हैं।" +downloadsCount = "डाउनलोड: {{count}}" +emailWarningBody = "यह ईमेल पता जैसा लगता है। यदि यह वà¥à¤¯à¤•à¥à¤¤à¤¿ पहले से Stirling PDF उपयोगकरà¥à¤¤à¤¾ नहीं है, तो वह फ़ाइल तक पहà¥à¤à¤š नहीं कर पाà¤à¤—ा।" +emailWarningConfirm = "फिर भी साà¤à¤¾ करें" +emailWarningTitle = "ईमेल पता" +errorTitle = "शेयर विफल" +failure = "शेयर लिंक जनरेट करने में असमरà¥à¤¥à¥¤ कृपया पà¥à¤¨à¤ƒ पà¥à¤°à¤¯à¤¾à¤¸ करें।" +fileLabel = "फ़ाइल" +generate = "लिंक जनरेट करें" +generated = "शेयर लिंक जनरेट किया गया" +hideActivity = "गतिविधि छिपाà¤à¤" +invalidUsername = "मानà¥à¤¯ उपयोगकरà¥à¤¤à¤¾ नाम या ईमेल पता दरà¥à¤œ करें।" +lastAccessed = "अंतिम बार à¤à¤•à¥à¤¸à¥‡à¤¸ किया गया" +linkAccessTitle = "शेयर लिंक पहà¥à¤à¤š" +linkLabel = "शेयर लिंक" +linksDisabled = "शेयर लिंक अकà¥à¤·à¤® हैं।" +linksDisabledBody = "आपके सरà¥à¤µà¤° सेटिंगà¥à¤¸ दà¥à¤µà¤¾à¤°à¤¾ शेयर लिंक अकà¥à¤·à¤® हैं।" +manage = "शेयरिंग पà¥à¤°à¤¬à¤‚धित करें" +manageDescription = "इस फ़ाइल को साà¤à¤¾ करने के लिठलिंक बनाà¤à¤ और पà¥à¤°à¤¬à¤‚धित करें।" +manageLoadFailed = "शेयर लिंक लोड करने में असमरà¥à¤¥à¥¤" +manageTitle = "शेयरिंग पà¥à¤°à¤¬à¤‚धन" +noActivity = "अभी तक कोई गतिविधि नहीं।" +noLinks = "अभी तक कोई सकà¥à¤°à¤¿à¤¯ शेयर लिंक नहीं।" +noSharedUsers = "अभी तक किसी उपयोगकरà¥à¤¤à¤¾ को पहà¥à¤à¤š नहीं है।" +removeLink = "लिंक हटाà¤à¤" +removeUser = "हटाà¤à¤" +revokeFailed = "शेयर लिंक हटाने में असमरà¥à¤¥à¥¤" +revoked = "शेयर लिंक हटाया गया" +roleLabel = "भूमिका" +sharingDisabled = "शेयरिंग अकà¥à¤·à¤® है।" +sharingDisabledBody = "आपकी सरà¥à¤µà¤° सेटिंगà¥à¤¸ दà¥à¤µà¤¾à¤°à¤¾ शेयरिंग अकà¥à¤·à¤® कर दी गई है।" +sharedUsersTitle = "शेयर किठगठउपयोगकरà¥à¤¤à¤¾" +title = "फ़ाइल शेयर करें" +unknownUser = "अजà¥à¤žà¤¾à¤¤ उपयोगकरà¥à¤¤à¤¾" +userAddFailed = "उस उपयोगकरà¥à¤¤à¤¾ के साथ शेयर नहीं कर सके।" +userAdded = "उपयोगकरà¥à¤¤à¤¾ को शेयर सूची में जोड़ा गया।" +usernameLabel = "उपयोगकरà¥à¤¤à¤¾ नाम या ईमेल" +usernamePlaceholder = "उपयोगकरà¥à¤¤à¤¾ नाम या ईमेल दरà¥à¤œ करें" +userRemoveFailed = "उस उपयोगकरà¥à¤¤à¤¾ को नहीं हटा सके।" +userRemoved = "उपयोगकरà¥à¤¤à¤¾ को शेयर सूची से हटाया गया।" +viewActivity = "गतिविधि देखें" +viewed = "देखा गया" +viewsCount = "देखे गà¤: {{count}}" +downloaded = "डाउनलोड किया गया" +bulkDescription = "साइन-इन उपयोगकरà¥à¤¤à¤¾à¤“ं के साथ सभी चयनित फ़ाइलें शेयर करने के लिठà¤à¤• लिंक बनाà¤à¤à¥¤" +bulkTitle = "चयनित फ़ाइलें शेयर करें" +copyLink = "शेयर लिंक कॉपी करें" +fileCount = "{{count}} फ़ाइलें चयनित" +ownerOnly = "केवल मालिक ही शेयरिंग पà¥à¤°à¤¬à¤‚धित कर सकता है।" +selectSingleFile = "शेयरिंग पà¥à¤°à¤¬à¤‚धित करने के लिठà¤à¤• ही फ़ाइल चà¥à¤¨à¥‡à¤‚।" + +[storageUpload] +description = "यह वरà¥à¤¤à¤®à¤¾à¤¨ फ़ाइल को आपकी पहà¥à¤à¤š के लिठसरà¥à¤µà¤° सà¥à¤Ÿà¥‹à¤°à¥‡à¤œ पर अपलोड करता है।" +errorTitle = "अपलोड विफल हà¥à¤†" +failure = "अपलोड विफल हà¥à¤†à¥¤ कृपया अपने लॉगिन और सà¥à¤Ÿà¥‹à¤°à¥‡à¤œ सेटिंगà¥à¤¸ की जाà¤à¤š करें।" +fileLabel = "फ़ाइल" +hint = "सारà¥à¤µà¤œà¤¨à¤¿à¤• लिंक और à¤à¤•à¥à¤¸à¥‡à¤¸ मोड आपकी सरà¥à¤µà¤° सेटिंगà¥à¤¸ दà¥à¤µà¤¾à¤°à¤¾ नियंतà¥à¤°à¤¿à¤¤ होते हैं।" +success = "सरà¥à¤µà¤° पर अपलोड किया गया" +title = "सरà¥à¤µà¤° पर अपलोड करें" +updateButton = "सरà¥à¤µà¤° पर अपडेट करें" +uploadButton = "सरà¥à¤µà¤° पर अपलोड करें" +bulkDescription = "यह चयनित फ़ाइलों को आपके सरà¥à¤µà¤° सà¥à¤Ÿà¥‹à¤°à¥‡à¤œ पर अपलोड करता है।" +bulkTitle = "चयनित फ़ाइलें अपलोड करें" +fileCount = "{{count}} फ़ाइलें चयनित" +more = " +{{count}} और" + [storage] approximateSize = "अनà¥à¤®à¤¾à¤¨à¤¿à¤¤ आकार" fileTooLarge = "फ़ाइल बहà¥à¤¤ बड़ी है। पà¥à¤°à¤¤à¤¿ फ़ाइल अधिकतम आकार है" @@ -7153,6 +7801,30 @@ title = "PDF देखें/संपादित करें" [warning] tooltipTitle = "चेतावनी" +[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 = "हसà¥à¤¤à¤¾à¤•à¥à¤·à¤° इमेज अपलोड करें" + [watermark] completed = "वॉटरमारà¥à¤• जोड़ा गया" desc = "PDF फ़ाइलों में टेकà¥à¤¸à¥à¤Ÿ या इमेज वॉटरमारà¥à¤• जोड़ें" @@ -7333,6 +8005,7 @@ activeSession = "सकà¥à¤°à¤¿à¤¯ सतà¥à¤°" addMembers = "सदसà¥à¤¯ जोड़ें" admin = "पà¥à¤°à¤¶à¤¾à¤¸à¤•" confirmDelete = "कà¥à¤¯à¤¾ आप वाकई इस उपयोगकरà¥à¤¤à¤¾ को हटाना चाहते हैं? यह कà¥à¤°à¤¿à¤¯à¤¾ पूरà¥à¤µà¤µà¤¤ नहीं की जा सकती।" +confirmUnlock = "कà¥à¤¯à¤¾ आप सà¥à¤¨à¤¿à¤¶à¥à¤šà¤¿à¤¤ हैं कि आप इस उपयोगकरà¥à¤¤à¤¾ खाते को अनलॉक करना चाहते हैं?" deleteUser = "उपयोगकरà¥à¤¤à¤¾ हटाà¤à¤" deleteUserError = "उपयोगकरà¥à¤¤à¤¾ हटाने में विफल" deleteUserSuccess = "उपयोगकरà¥à¤¤à¤¾ सफलतापूरà¥à¤µà¤• हटाया गया" @@ -7341,6 +8014,8 @@ disable = "अकà¥à¤·à¤® करें" disabled = "अकà¥à¤·à¤®" editRole = "भूमिका संपादित करें" enable = "सकà¥à¤°à¤¿à¤¯ करें" +locked = "लॉकà¥à¤¡" +lockedBadge = "लॉकà¥à¤¡" loading = "लोग लोड हो रहे हैं..." loginRequired = "पहले लॉगिन मोड सकà¥à¤·à¤® करें" member = "सदसà¥à¤¯" @@ -7350,6 +8025,9 @@ searchMembers = "सदसà¥à¤¯à¥‹à¤‚ को खोजें..." status = "सà¥à¤¥à¤¿à¤¤à¤¿" team = "टीम" title = "लोग" +unlockAccount = "खाता अनलॉक करें" +unlockUserError = "उपयोगकरà¥à¤¤à¤¾ खाता अनलॉक करने में विफल" +unlockUserSuccess = "उपयोगकरà¥à¤¤à¤¾ खाता सफलतापूरà¥à¤µà¤• अनलॉक किया गया" user = "उपयोगकरà¥à¤¤à¤¾" [workspace.people.actions] diff --git a/frontend/public/locales/hr-HR/translation.toml b/frontend/public/locales/hr-HR/translation.toml index 4cd16516b7..05d68b832b 100644 --- a/frontend/public/locales/hr-HR/translation.toml +++ b/frontend/public/locales/hr-HR/translation.toml @@ -8,6 +8,7 @@ black = "Crno" blue = "Plavo" bored = "DosaÄ‘ujete se Äekajući?" cancel = "Odustani" +confirm = "Potvrdi" changedCredsMessage = "Podaci za prijavu uspjeÅ¡no promijenjeni!" chooseFile = "Odaberi datoteku" close = "Zatvori" @@ -146,6 +147,7 @@ insufficientCredits = "Nedovoljno kredita. Potrebno: {{requiredCredits}}, Dostup loadingCredits = "Provjera kredita..." loadingProStatus = "Provjera statusa pretplate..." noticeTopUpOrPlan = "Nedovoljno kredita, nadoplatite ili nadogradite na plan" +accessInvite = "Pozovi" [account] accountSettings = "Postavke raÄuna" @@ -1427,6 +1429,34 @@ title = "Obrada" description = "Maksimalno vrijeme Äekanja na zadatak obrade prije prijave pogreÅ¡ke." label = "Vremensko ograniÄenje obrade (sekunde)" +[admin.settings.storage] +description = "Upravljajte pohranom na poslužitelju i opcijama dijeljenja." +title = "Pohrana i dijeljenje datoteka" + +[admin.settings.storage.enabled] +description = "Omogućite korisnicima pohranu datoteka na poslužitelju." +label = "Omogući pohranu datoteka na poslužitelju" + +[admin.settings.storage.sharing.email] +description = "Omogući dijeljenje s adresama e-poÅ¡te." +label = "Omogući dijeljenje e-poÅ¡tom" +mailLink = "Konfiguriraj postavke poÅ¡te" +mailNote = "Zahtijeva konfiguraciju poÅ¡te. " + +[admin.settings.storage.sharing.enabled] +description = "Omogućite korisnicima dijeljenje pohranjenih datoteka." +label = "Omogući dijeljenje" + +[admin.settings.storage.sharing.links] +description = "Omogući dijeljenje putem poveznica uz prijavu." +frontendUrlLink = "Konfiguriraj u Postavkama sustava" +frontendUrlNote = "Zahtijeva Frontend URL. " +label = "Omogući poveznice za dijeljenje" + +[admin.settings.storage.signing.enabled] +description = "Omogućite korisnicima stvaranje sesija potpisivanja s viÅ¡e sudionika. Zahtijeva omogućenu poslužiteljsku pohranu datoteka." +label = "Omogući grupno potpisivanje (alfa)" + [admin.settings.unsavedChanges] cancel = "Nastavi ureÄ‘ivati" discard = "Odbaci promjene" @@ -2059,7 +2089,19 @@ numbers = "Brojevi/rasponi: 5, 10-20" progressions = "Progresije: 3n, 4n+1" [certSign] +allSigned = "Svi sudionici su potpisali. Spremno za dovrÅ¡avanje." +awaitingSignatures = "ÄŒeka potpise" +signatureProgress = "{{signedCount}}/{{totalCount}} potpisa" chooseCertificate = "Odaberite datoteku certifikata" +declined = "Odbijeno" +fetchFailed = "Nije moguće uÄitati podatke o potpisivanju" +finalized = "DovrÅ¡eno" +notified = "Na Äekanju" +partialNote = "Možete ranije dovrÅ¡iti s trenutaÄnim potpisima. Nepotpisani sudionici bit će iskljuÄeni." +pending = "Na Äekanju" +readyToFinalize = "Spremno za dovrÅ¡avanje" +signed = "Potpisano" +viewed = "Pregledano" chooseJksFile = "Odaberite JKS datoteku" chooseP12File = "Odaberite PKCS12 datoteku" choosePfxFile = "Odaberite PFX datoteku" @@ -2082,6 +2124,7 @@ title = "Potpisivanje Certifikatom" invisible = "Nevidljivo" stepTitle = "Izgled potpisa" visible = "Vidljivo" +visibility = "Vidljivost" [certSign.appearance.options] title = "Detalji potpisa" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "DovrÅ¡i i uÄitaj potpisani PDF" +early = "DovrÅ¡i s trenutaÄnim potpisima" + +[certSign.collab.sessionDetail] +addButton = "Dodaj sudionike" +addParticipants = "Dodaj sudionike" +addParticipantsError = "Neuspjelo dodavanje sudionika" +backToList = "Natrag na sesije" +deleteConfirm = "Jeste li sigurni? Ovo se ne može poniÅ¡titi." +deleteError = "Neuspjelo brisanje sesije" +deleted = "Sesija izbrisana" +deleteSession = "IzbriÅ¡i sesiju" +dueDate = "Rok" +finalizeError = "Neuspjelo dovrÅ¡avanje sesije" +loadPdfError = "Neuspjelo uÄitavanje potpisanog PDF-a" +loadSignedPdf = "UÄitaj potpisani PDF u aktivne datoteke" +messageLabel = "Poruka" +noAdditionalInfo = "Nema dodatnih informacija" +owner = "Vlasnik" +participantRemoved = "Sudionik uklonjen" +participants = "Sudionici" +participantsAdded = "Sudionici su uspjeÅ¡no dodani" +removeParticipant = "Ukloni" +removeParticipantError = "Neuspjelo uklanjanje sudionika" +selectUsers = "Odaberite korisnike..." +sessionInfo = "Informacije o sesiji" +workbenchTitle = "Upravljanje sesijom" + +[certSign.collab.signRequest] +addedToFiles = "Dokument dodan u aktivne datoteke" +addSignature = "Dodajte svoj potpis" +addToFiles = "Dodaj u aktivne datoteke" +advancedSettings = "Napredne postavke" +backToList = "Natrag na zahtjeve za potpis" +certificateChoice = "Odaberite certifikat za potpisivanje" +changeSignature = "Promijeni potpis" +clearSignature = "ObriÅ¡i potpis" +completeAndSign = "DovrÅ¡i i potpiÅ¡i" +createNewSignature = "Izradi novi potpis" +declineButton = "Odbij" +decline = "Odbij zahtjev" +deleteSelected = "IzbriÅ¡i odabrani potpis" +drawSignature = "Nacrtajte svoj potpis dolje" +dueDate = "Rok" +fileTooLarge = "VeliÄina datoteke mora biti manja od 5 MB" +fontFamily = "Obitelj fonta" +fontSize = "VeliÄina fonta: {{size}}px" +fontSizePlaceholder = "VeliÄina" +from = "Od" +invalidCertFile = "Odaberite P12 ili PFX datoteku certifikata" +invalidFileType = "Odaberite slikovnu datoteku" +location = "Lokacija (neobavezno)" +locationPlaceholder = "Odakle potpisujete?" +message = "Poruka" +noCertificate = "Odaberite datoteku certifikata" +noSignatures = "Postavite barem jedan potpis na PDF" +p12File = "P12/PFX datoteka certifikata" +password = "Lozinka certifikata" +passwordPlaceholder = "Unesite lozinku..." +penColor = "Boja olovke" +penSize = "VeliÄina olovke: {{size}}px" +placementActive = "Kliknite na PDF za postavljanje" +placeSignatureButton = "Postavi potpis na PDF" +reason = "Razlog (neobavezno)" +reasonPlaceholder = "ZaÅ¡to potpisujete?" +removeImage = "Ukloni sliku" +removeCertFile = "Ukloni datoteku" +savedSignatures = "Spremljeni potpisi" +selectFile = "Odaberi datoteku slike" +selectSignatureTitle = "Odaberite ili izradite potpis" +signButton = "PotpiÅ¡i dokument" +signatureInfo = "Ove postavke konfigurira vlasnik dokumenta" +signaturePlaced = "Potpis postavljen na stranicu" +signatureSettings = "Postavke potpisa" +signatureText = "Tekst potpisa" +signatureTextPlaceholder = "Unesite svoje ime..." +signatureTypeLabel = "Vrsta potpisa" +signingTitle = "Potpisivanje" +textColor = "Boja teksta" +typeSignature = "UpiÅ¡ite svoje ime da biste izradili potpis" +uploadCert = "PrilagoÄ‘eni certifikat" +uploadCertDesc = "Koristite vlastiti P12/PFX certifikat" +uploadSignature = "Prenesite sliku svog potpisa" +usePersonalCert = "Osobni certifikat" +usePersonalCertDesc = "Automatski generiran za vaÅ¡ raÄun" +useServerCert = "Certifikat organizacije" +useServerCertDesc = "ZajedniÄki certifikat organizacije" +workbenchTitle = "Zahtjev za potpis" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Odaberite boju poteza" +continue = "Nastavi" + +[certSign.collab.signRequest.certModal] +description = "Postavili ste {{count}} potpis(a). Odaberite svoj certifikat za dovrÅ¡etak potpisivanja." +sign = "PotpiÅ¡i dokument" +certValidating = "Provjera certifikata..." +certValidUntil = "Certifikat vrijedi do {{date}}" +certInvalid = "Certifikat nevažeći: {{error}}" +certInvalidFallback = "Nevažeći certifikat" +certNetworkError = "Nije moguće potvrditi certifikat" +title = "Konfiguriraj certifikat" + +[certSign.collab.signRequest.image] +hint = "Prenesite PNG ili JPG sliku svog potpisa" + +[certSign.collab.signRequest.mode] +move = "Premjesti potpis" +place = "Postavi potpis" +title = "NaÄin potpisivanja ili premjeÅ¡tanja" + +[certSign.collab.signRequest.modeTabs] +draw = "Crtaj" +image = "Prenesi" +text = "UpiÅ¡i" + +[certSign.collab.signRequest.placeSignature] +message = "Kliknite na PDF kako biste postavili svoj potpis" +title = "Postavi potpis" + +[certSign.collab.signRequest.preview] +imageAlt = "Odabrani potpis" +missing = "Nema pregleda" +textFallback = "Potpis" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Crtani potpis" +defaultImageLabel = "Preneseni potpis" +defaultLabel = "Potpis" +defaultTextLabel = "Upisani potpis" +delete = "IzbriÅ¡i potpis" +none = "Nema spremljenih potpisa" + +[certSign.collab.signRequest.signatureType] +draw = "Crtaj" +type = "UpiÅ¡i" +upload = "Prenesi" + +[certSign.collab.signRequest.steps] +back = "Natrag" +cancelPlacement = "Otkaži postavljanje" +certificate = "Certifikat" +clickMultipleTimes = "Kliknite na PDF viÅ¡e puta kako biste postavili potpise. Povucite bilo koji potpis za premjeÅ¡tanje ili promjenu veliÄine." +clickToPlace = "Kliknite na PDF gdje želite da se vaÅ¡ potpis pojavi." +continue = "Nastavi na odabir certifikata" +continueToPlacement = "Nastavi na postavljanje" +continueToReview = "Nastavi na pregled" +createSignature = "Izradi potpis" +invisible = "Nevidljivo" +location = "Lokacija:" +multipleSignatures = "{{count}} potpisa bit će primijenjeno na PDF" +oneSignature = "1 potpis bit će primijenjen na PDF" +placeOnPdf = "Postavi na PDF" +reason = "Razlog:" +reviewTitle = "Pregled prije potpisivanja" +signaturePlaced = "Potpis postavljen na stranicu {{page}}. Možete prilagoditi položaj ponovnim klikom ili nastaviti na pregled." +visible = "Vidljivo" +visibility = "Vidljivost:" +yourSignatures = "VaÅ¡i potpisi ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Boja" +fontLabel = "Font" +fontSizeLabel = "VeliÄina" +fontSizePlaceholder = "16" +label = "Tekst potpisa" +modalHint = "Unesite svoje ime, zatim kliknite Nastavi za postavljanje na PDF." +placeholder = "Unesite svoje ime..." + +[certSign.collab.participant] +certValidating = "Provjera certifikata..." +certValid = "✓ Certifikat valjan" +certValidUntil = " do {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Nevažeći certifikat" +certNetworkError = "Nije moguće potvrditi certifikat" + +[certSign.collab.addParticipants] +add = "Dodaj {{count}} sudionika" +back = "Natrag" +configureSignatures = "Konfiguriraj postavke potpisa" +continue = "Nastavi na postavke potpisa" +reasonHelp = "Unaprijed postavite razlog potpisivanja za ove sudionike (neobavezno, mogu ga promijeniti pri potpisivanju)" +reasonPlaceholder = "npr. odobrenje, pregled..." +selectUsers = "Odaberi korisnike" + +[certSign.collab.sessionCreation] +includeSummaryPage = "UkljuÄi stranicu sažetka potpisa" +includeSummaryPageHelp = "Na kraju će se dodati stranica sa svim metapodacima potpisa. Okviri digitalnih certifikata na pojedinaÄnim stranicama bit će potisnuti (ruÄni potpisi nisu pogoÄ‘eni)." + +[certSign.collab.sessionList] +active = "Aktivno" +finalized = "DovrÅ¡eno" + +[certSign.collab.signatureSettings] +description = "Konfigurirajte kako će potpisi izgledati za sve sudionike" +title = "Izgled potpisa" + +[certSign.collab.userSelector] +inviteUsers = "Dodaj korisnike" +loadError = "Nije moguće uÄitati korisnike" +noTeam = "Bez tima" +noUsers = "Nisu pronaÄ‘eni drugi korisnici." +placeholder = "Odaberite korisnike..." + +[certSign.mobile] +panelActions = "Akcije" +panelDocument = "Dokument" +panelPeople = "Osobe" + +[certSign.sessions] +deleted = "Sesija izbrisana" +fetchFailed = "Nije moguće uÄitati detalje sesije" +finalized = "Sesija dovrÅ¡ena" +loaded = "Potpisani PDF uÄitan" +pdfNotReady = "PDF nije spreman" +pdfNotReadyDesc = "Potpisani PDF se generira. PokuÅ¡ajte ponovno za trenutak." + +[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" + [changeCreds] changePassword = "Koristite zadanu lozinku za prijavu. Unesite novu lozinku" changeUsername = "Ažurirajte svoje korisniÄko ime. Bit ćete odjavljeni nakon ažuriranja." @@ -3242,6 +3531,46 @@ totalSelected = "Ukupno odabrano" unsupported = "Nepodržano" unzip = "Raspakiraj" uploadError = "Nije uspjelo otpremiti neke datoteke." +copyCreated = "Kopija spremljena na ovaj ureÄ‘aj." +copyFailed = "Nije moguće stvoriti kopiju." +leaveShare = "Ukloni s mog popisa" +leaveShareFailed = "Nije moguće ukloniti dijeljenu datoteku." +leaveShareSuccess = "Uklonjeno s vaÅ¡eg popisa dijeljenih." +removeBoth = "Ukloni s oba" +removeFilePrompt = "Ova je datoteka spremljena na ovom ureÄ‘aju i na vaÅ¡em poslužitelju. Odakle je želite ukloniti?" +removeFileTitle = "Ukloni datoteku" +removeLocalOnly = "Samo s ovog ureÄ‘aja" +removeServerFailed = "Nije moguće ukloniti datoteku s poslužitelja." +removeServerOnly = "Samo s poslužitelja" +removeServerOnlyPrompt = "Ova je datoteka pohranjena samo na vaÅ¡em poslužitelju. Želite li je ukloniti s poslužitelja?" +removeServerSuccess = "Uklonjeno s poslužitelja." +removeSharedPrompt = "Ova je datoteka podijeljena s vama. Možete je ukloniti s ovog ureÄ‘aja ili s vaÅ¡eg popisa dijeljenih." +removeSharedServerOnlyBlockedPrompt = "Ova je datoteka podijeljena s vama i pohranjena samo na poslužitelju." +removeSharedServerOnlyPrompt = "Ova je datoteka podijeljena s vama i pohranjena samo na poslužitelju. Ukloniti je s vaÅ¡eg popisa?" +changesNotUploaded = "Promjene nisu prenesene" +cloudFile = "Datoteka u oblaku" +filterAll = "Sve" +filterLocal = "Lokalno" +filterSharedByMe = "Podijeljeno od mene" +filterSharedWithMe = "Podijeljeno sa mnom" +lastSynced = "Zadnja sinkronizacija" +localOnly = "Samo lokalno" +makeCopy = "Izradi kopiju" +owner = "Vlasnik" +ownerUnknown = "Nepoznato" +share = "Podijeli" +shareSelected = "Podijeli odabrano" +sharedByYou = "Podijeljeno od vas" +sharedEditNoticeBody = "Nemate prava ureÄ‘ivanja poslužiteljske verzije ove datoteke. Sve izmjene koje napravite bit će spremljene kao lokalna kopija." +sharedEditNoticeConfirm = "Razumijem" +sharedEditNoticeTitle = "Kopija na poslužitelju samo za Äitanje" +sharedWithYou = "Podijeljeno s vama" +sharing = "Dijeljenje" +storageState = "Pohrana" +synced = "Sinkronizirano" +updateOnServer = "Ažuriraj na poslužitelju" +uploadSelected = "Prenesi odabrano" +uploadToServer = "Prenesi na poslužitelj" [files] addFiles = "Dodaj datoteke" @@ -3367,6 +3696,77 @@ title = "O spljoÅ¡tavanju PDF-ova" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "O grupnom potpisivanju" + +[groupSigning.tooltip.finalization] +bullet1 = "Svi potpisi primjenjuju se redoslijedom sudionika koji ste odredili" +bullet2 = "Možete dovrÅ¡iti s djelomiÄnim potpisima ako je potrebno" +bullet3 = "Nakon dovrÅ¡avanja sesiju nije moguće mijenjati" +description = "Kada svi sudionici potpiÅ¡u (ili odaberete rano dovrÅ¡iti), možete generirati konaÄni potpisani PDF." +title = "Postupak dovrÅ¡avanja" + +[groupSigning.tooltip.roles] +bullet1 = "Vlasnik (vi): stvara sesiju, konfigurira zadane postavke potpisa, dovrÅ¡ava dokument" +bullet2 = "Sudionici: stvaraju svoj potpis, biraju certifikat, postavljaju na PDF" +bullet3 = "Sudionici ne mogu mijenjati postavke vidljivosti, razloga ili lokacije potpisa" +description = "Vi kontrolirate postavke izgleda potpisa za sve sudionike." +title = "Uloge sudionika" + +[groupSigning.tooltip.sequential] +bullet1 = "Prvi sudionik mora potpisati prije nego drugi dobije pristup dokumentu" +bullet2 = "Osigurava ispravan redoslijed potpisivanja radi pravne usklaÄ‘enosti" +bullet3 = "Možete promijeniti redoslijed sudionika povlaÄenjem na popisu" +description = "Sudionici potpisuju dokumente redoslijedom koji odredite. Svaki potpisnik dobiva obavijest kada doÄ‘e njegov red." +title = "Sekvencijalno potpisivanje" + +[groupSigning.steps] +back = "Natrag" +completed = "DovrÅ¡eno" +current = "Trenutno" +stepLabel = "Korak {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Nastavi na pregled" +invisible = "Potpisi će biti nevidljivi (samo metapodaci)" +locationLabel = "Lokacija:" +preview = "Pretpregled" +reasonLabel = "Razlog:" +title = "Konfiguriraj postavke potpisa" +visible = "Potpisi će biti vidljivi na stranici {{page}}" + +[groupSigning.steps.review] +document = "Dokument" +dueDate = "Rok (neobavezno)" +dueDatePlaceholder = "Odaberite rok..." +invisible = "Nevidljivo (samo metapodaci)" +location = "Lokacija:" +logo = "Logo:" +logoHidden = "Bez logotipa" +logoShown = "Prikazan Stirling PDF logotip" +participants = "Sudionici" +reason = "Razlog:" +send = "PoÅ¡alji zahtjeve za potpisivanje" +signatureSettings = "Postavke potpisa" +title = "Pregled detalja sesije" +titleShort = "Pregled i slanje" +visibility = "Vidljivost:" +visible = "Vidljivo na stranici {{page}}" +participantCount = "{{count}} sudionik(a) potpisivat će redom" + +[groupSigning.steps.selectDocument] +continue = "Nastavi na odabir sudionika" +noFile = "Odaberite jednu PDF datoteku iz svojih aktivnih datoteka za stvaranje sesije potpisivanja." +selectedFile = "Odabrani dokument" +title = "Odaberi dokument" + +[groupSigning.steps.selectParticipants] +continue = "Nastavi na postavke potpisa" +count = "{{count}} sudionik(a) odabrano" +label = "Odaberite sudionike" +placeholder = "Odaberite sudionike za potpisivanje..." +title = "Odaberite sudionike" + [getPdfInfo] downloadJson = "Preuzmite JSON" downloads = "Preuzimanja" @@ -4460,7 +4860,10 @@ zoomOut = "Umanji" [viewer] cannotPreviewFile = "Nije moguće pregledati datoteku" +disableColorFilter = "Onemogući filtar boje" dualPageView = "Prikaz dviju stranica" +enableDarkFilter = "Omogući tamni filtar" +enableSepiaFilter = "Omogući sepija filtar" firstPage = "Prva stranica" lastPage = "Zadnja stranica" nextPage = "Sljedeća stranica" @@ -4470,6 +4873,22 @@ singlePageView = "Prikaz jedne stranice" unknownFile = "Nepoznata datoteka" zoomIn = "Povećaj" zoomOut = "Umanji" +resetZoom = "PoniÅ¡ti zum" + +[viewer.nonPdf] +fileTypeBadge = "{{type}} datoteka" +convertToPdf = "Pretvori u PDF" +loading = "UÄitavanje..." +emptyFile = "Prazna datoteka" +csvStats = "{{rows}} redaka · {{columns}} stupaca · {{size}}" +sortedBy = "Sortirano po: {{column}}" +columnDefault = "Stupac {{index}}" +htmlPreviewWarning = "HTML pregled — vanjski resursi se možda neće uÄitati · {{size}}" +htmlPreview = "HTML pregled" +invalidJson = "Nevažeći JSON — prikaz neobraÄ‘enog sadržaja" +textStats = "{{lines}} redaka · {{size}}" +lineNumbers = "Brojevi redaka" +renderMarkdown = "Prikaži Markdown" [viewer.attachments] title = "Prilozi" @@ -4531,6 +4950,7 @@ toggleAttachments = "Prikaži/sakrij priloge" toggleTheme = "Prebaci temu" language = "Jezik" toggleAnnotations = "Prebaci vidljivost biljeÅ¡ki" +toggleLayers = "Prikaži/sakrij slojeve" search = "Pretraži PDF" panMode = "NaÄin pomicanja" applyRedactionsFirst = "Najprije primijeni zacrnjivanja" @@ -5407,20 +5827,72 @@ title = "Ispis datoteke" 2 = "Unesite naziv pisaÄa" [quickAccess] +access = "Pristup" +accessAddPerson = "Dodaj joÅ¡ jednu osobu" +accessBack = "Natrag" +accessCopyLink = "Kopiraj poveznicu" +accessEmail = "Adresa e-poÅ¡te" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Datoteka" +accessGeneral = "Opći pristup" +accessInviteTitle = "Pozovi osobe" +accessOwner = "Vlasnik" +accessPanel = "Pristup dokumentu" +accessPeople = "Osobe s pristupom" +accessRemove = "Ukloni" +accessRestricted = "OgraniÄeno" +accessRestrictedHint = "Samo osobe s pristupom mogu otvoriti" +accessRole = "Uloga" +accessRoleCommenter = "Komentator" +accessRoleEditor = "Urednik" +accessRoleViewer = "Pregledavatelj" +accessSelectedFile = "Odabrana datoteka" +accessSendInvite = "PoÅ¡alji poziv" +accessTitle = "Pristup dokumentu" +accessYou = "Vi" account = "RaÄun" +activeSessions = "Aktivne sesije" +activeTab = "Aktivno" activity = "Dnevnik" adminSettings = "Admin postavke" +allSessions = "Sve sesije" allTools = "All Tools" automate = "Auto" +back = "Natrag" +certSign = "Potpis certifikatom" +completedSessions = "DovrÅ¡ene sesije" +completedTab = "DovrÅ¡eno" config = "Postavke" +createNew = "Stvori novi zahtjev" +createSession = "Stvori zahtjev za potpisivanje" +dueDate = "Rok (neobavezno)" files = "Datoteke" help = "Pomoć" +noActiveSessions = "Nema zahtjeva za potpisivanje na Äekanju ni aktivnih sesija" +noCompletedSessions = "Nema dovrÅ¡enih sesija" +noFile = "Nije odabrana datoteka" read = "ÄŒitanje" reader = "ÄŒitaÄ" +refresh = "Osvježi" +requestSignatures = "Zatraži potpise" +selectSingleFileToRequest = "Odaberite jednu PDF datoteku za zahtjev potpisa" +selectedFile = "Odabrana datoteka" +selectUsers = "Odaberite korisnike za potpis" +selectUsersPlaceholder = "Odaberite sudionike..." +sendingRequest = "Slanje..." settings = "Postavke" showMeAround = "Provedi me kroz" sign = "PotpiÅ¡i" +signatureRequests = "Zahtjevi za potpis" +signYourself = "PotpiÅ¡ite sami" +newRequest = "Novi zahtjev" tours = "Obilasci" +wetSign = "Dodaj potpis" +filterMine = "Moji" +filterOverdue = "ZakaÅ¡njelo" +filterSigned = "Potpisano" +filterDeclined = "Odbijeno" +searchDocuments = "Pretraži dokumente…" [quickAccess.helpMenu] adminTour = "VodiÄ za administratore" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "VaÅ¡ Stirling-PDF poslužitelj je izvan mreže i \"{{ expired = "VaÅ¡ sesija je istekla. Molim vas da osvježite stranicu i pokuÅ¡ate ponovno." refreshPage = "Osvježi stranicu" +[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" + +[signatureSettings.tooltip] +header = "Postavke izgleda potpisa" + +[signatureSettings.tooltip.location] +bullet1 = "Primjeri: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Nije isto Å¡to i položaj na stranici" +bullet3 = "Može biti obavezno u odreÄ‘enim pravnim jurisdikcijama" +description = "Neobavezna geografska lokacija gdje je primijenjen potpis. Sprema se u metapodacima certifikata." +title = "Lokacija potpisa" + +[signatureSettings.tooltip.logo] +bullet1 = "Prikazuje se uz potpis i tekst" +bullet2 = "Podržava PNG, JPG formate" +bullet3 = "PoboljÅ¡ava profesionalni izgled" +description = "Dodajte logotip tvrtke vidljivim potpisima radi brendiranja i autentiÄnosti." +title = "Logotip tvrtke" + +[signatureSettings.tooltip.reason] +bullet1 = "Primjeri: \"Approval\", \"Contract Agreement\", \"Review Complete\"" +bullet2 = "Vidljivo u svojstvima potpisa PDF-a" +bullet3 = "Korisno za revizijske tragove i usklaÄ‘enost" +description = "Neobavezan tekst koji objaÅ¡njava zaÅ¡to se dokument potpisuje. Sprema se u metapodacima certifikata." +title = "Razlog potpisa" + +[signatureSettings.tooltip.visibility] +bullet1 = "Vidljivo: potpis se pojavljuje na PDF-u s prilagoÄ‘enim izgledom" +bullet2 = "Nevidljivo: certifikat ugraÄ‘en bez vidljive oznake" +bullet3 = "Nevidljivi potpisi i dalje pružaju kriptografsku valjanost" +description = "Upravlja time hoće li potpis biti vidljiv na dokumentu ili ugraÄ‘en nevidljivo." +title = "Vidljivost potpisa" + [settings.configuration] advanced = "Napredno" database = "Baza podataka" endpoints = "Endpointi" features = "ZnaÄajke" +storageSharing = "Pohrana i dijeljenje datoteka" systemSettings = "Postavke sustava" title = "Konfiguracija" @@ -6332,10 +6868,13 @@ title = "Prijavite se u Stirling" [setup.selfhosted] link = "ili se povežite sa samohostiranim raÄunom" subtitle = "Unesite vjerodajnice poslužitelja" +changeServerLocked = "VaÅ¡a organizacija je ograniÄila ovu aplikaciju na odreÄ‘eni poslužitelj" switchToLocal = "Umjesto toga koristi lokalne alate" title = "Prijavite se na poslužitelj" [setup.selfhosted.unreachable] +changeServer = "Povežite se s drugim poslužiteljem" +changeServerLocked = "VaÅ¡a organizacija je ograniÄila ovu aplikaciju na odreÄ‘eni poslužitelj" continueOffline = "Umjesto toga koristi lokalne alate" message = "Nije moguće doći do {{url}}. Provjerite da je poslužitelj pokrenut i dostupan." retry = "PokuÅ¡aj ponovno" @@ -6529,6 +7068,15 @@ saved = "Spremljeno" text = "Tekst" title = "Vrsta potpisa" +[signRequest] +declined = "Zahtjev za potpis odbijen" +fetchFailed = "Nije moguće uÄitati zahtjev za potpis" +signed = "Dokument uspjeÅ¡no potpisan" + +[signSession] +createFailed = "NeuspjeÅ¡no stvaranje zahtjeva za potpisivanje" +created = "Zahtjev za potpisivanje poslan" + [signup] accountCreatedSuccessfully = "RaÄun je uspjeÅ¡no stvoren! Sada se možete prijaviti." alreadyHaveAccount = "Već imate raÄun? Prijavite se" @@ -6807,6 +7355,106 @@ title = "Podijeli PDF naoglazdene glave" [splitPdfByChapters] tags = "podjela, glave, markere, organizacija" +[storageShare] +accessed = "Pristupljeno" +accessDenied = "Nemate pristup ovoj dijeljenoj datoteci. Zatražite od vlasnika da je podijeli s vama." +accessFailed = "Nije moguće uÄitati aktivnost." +accessDeniedBody = "Nemate pristup ovoj datoteci. Zatražite od vlasnika da je podijeli s vama." +accessDeniedTitle = "Nema pristupa" +accessLimitedCommenter = "Pristup komentiranju uskoro dolazi. Zatražite od vlasnika prava urednika ako trebate preuzeti." +accessLimitedTitle = "OgraniÄen pristup" +accessLimitedViewer = "Ova je poveznica samo za pregled. Zatražite od vlasnika prava urednika ako trebate preuzeti." +createdAt = "Stvoreno" +download = "Preuzmi" +downloadFailed = "Nije moguće preuzeti ovu datoteku." +expiredBody = "Ova poveznica za dijeljenje je nevažeća ili je istekla." +expiredTitle = "Poveznica je istekla" +goToLogin = "Idi na prijavu" +loadFailed = "Nije moguće otvoriti dijeljenu datoteku." +loading = "UÄitavanje poveznice za dijeljenje..." +loginPrompt = "Prijavite se za pristup ovoj dijeljenoj datoteci." +loginRequired = "Potrebna je prijava" +openInApp = "Otvori u Stirling PDF" +ownerLabel = "Vlasnik" +ownerUnknown = "Nepoznato" +requiresLogin = "Za ovu dijeljenu datoteku potrebna je prijava." +roleCommenter = "Komentator" +roleEditor = "Urednik" +roleViewer = "Pregledavatelj" +shareHeading = "Dijeljena datoteka" +titleDefault = "Dijeljena datoteka" +tryAgain = "PokuÅ¡ajte ponovno kasnije." +addUser = "Dodaj" +commenterHint = "Komentiranje uskoro dolazi." +copied = "Poveznica kopirana u meÄ‘uspremnik" +copy = "Kopiraj" +copyFailed = "Kopiranje nije uspjelo" +description = "Stvorite poveznicu za dijeljenje za ovu datoteku. Prijavljeni korisnici s poveznicom mogu joj pristupiti." +downloadsCount = "Preuzimanja: {{count}}" +emailWarningBody = "Ovo izgleda kao adresa e-poÅ¡te. Ako ova osoba joÅ¡ nije korisnik Stirling PDF-a, neće moći pristupiti datoteci." +emailWarningConfirm = "Ipak podijeli" +emailWarningTitle = "Adresa e-poÅ¡te" +errorTitle = "Dijeljenje nije uspjelo" +failure = "Nije moguće generirati poveznicu za dijeljenje. PokuÅ¡ajte ponovno." +fileLabel = "Datoteka" +generate = "Generiraj poveznicu" +generated = "Poveznica za dijeljenje generirana" +hideActivity = "Sakrij aktivnost" +invalidUsername = "Unesite valjano korisniÄko ime ili adresu e-poÅ¡te." +lastAccessed = "Zadnji pristup" +linkAccessTitle = "Pristup putem poveznice za dijeljenje" +linkLabel = "Poveznica za dijeljenje" +linksDisabled = "Poveznice za dijeljenje su onemogućene." +linksDisabledBody = "Poveznice za dijeljenje su onemogućene postavkama vaÅ¡eg poslužitelja." +manage = "Upravljaj dijeljenjem" +manageDescription = "Stvarajte i upravljajte poveznicama za dijeljenje ove datoteke." +manageLoadFailed = "Nije moguće uÄitati poveznice za dijeljenje." +manageTitle = "Upravljanje dijeljenjem" +noActivity = "JoÅ¡ nema aktivnosti." +noLinks = "JoÅ¡ nema aktivnih poveznica za dijeljenje." +noSharedUsers = "JoÅ¡ nitko nema pristup." +removeLink = "Ukloni poveznicu" +removeUser = "Ukloni" +revokeFailed = "Nije moguće ukloniti poveznicu za dijeljenje." +revoked = "Poveznica za dijeljenje uklonjena" +roleLabel = "Uloga" +sharingDisabled = "Dijeljenje je onemogućeno." +sharingDisabledBody = "Dijeljenje je onemogućeno postavkama vaÅ¡eg poslužitelja." +sharedUsersTitle = "Korisnici s kojima je podijeljeno" +title = "Dijeli datoteku" +unknownUser = "Nepoznat korisnik" +userAddFailed = "Nije moguće dijeliti s tim korisnikom." +userAdded = "Korisnik je dodan na popis dijeljenja." +usernameLabel = "KorisniÄko ime ili e-poÅ¡ta" +usernamePlaceholder = "Unesite korisniÄko ime ili e-poÅ¡tu" +userRemoveFailed = "Nije moguće ukloniti tog korisnika." +userRemoved = "Korisnik je uklonjen s popisa dijeljenja." +viewActivity = "Prikaži aktivnost" +viewed = "Pregledano" +viewsCount = "Pregledi: {{count}}" +downloaded = "Preuzeto" +bulkDescription = "Stvorite jednu poveznicu za dijeljenje svih odabranih datoteka s prijavljenim korisnicima." +bulkTitle = "Dijeli odabrane datoteke" +copyLink = "Kopiraj poveznicu za dijeljenje" +fileCount = "{{count}} datoteka odabrano" +ownerOnly = "Samo vlasnik može upravljati dijeljenjem." +selectSingleFile = "Odaberite jednu datoteku za upravljanje dijeljenjem." + +[storageUpload] +description = "Ovo prenosi trenutaÄnu datoteku u pohranu na poslužitelju za vaÅ¡ vlastiti pristup." +errorTitle = "Prijenos nije uspio" +failure = "Prijenos nije uspio. Provjerite svoje postavke prijave i pohrane." +fileLabel = "Datoteka" +hint = "Javnim poveznicama i naÄinima pristupa upravljaju postavke vaÅ¡eg poslužitelja." +success = "Preneseno na poslužitelj" +title = "Prenesi na poslužitelj" +updateButton = "Ažuriraj na poslužitelju" +uploadButton = "Prenesi na poslužitelj" +bulkDescription = "Ovo prenosi odabrane datoteke u pohranu na vaÅ¡em poslužitelju." +bulkTitle = "Prenesi odabrane datoteke" +fileCount = "{{count}} datoteka odabrano" +more = " +{{count}} joÅ¡" + [storage] approximateSize = "Približna veliÄina" fileTooLarge = "Datoteka je prevelika. Maksimalna veliÄina po datoteci je" @@ -7153,6 +7801,30 @@ title = "Pregled/Uredi PDF" [warning] tooltipTitle = "Upozorenje" +[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" + [watermark] completed = "Vodeni žig dodan" desc = "Dodajte tekstualne ili slikovne vodene žigove u PDF datoteke" @@ -7333,6 +8005,7 @@ activeSession = "Aktivna sesija" addMembers = "Dodaj Älanove" admin = "Administrator" confirmDelete = "Jeste li sigurni da želite izbrisati ovog korisnika? Ova radnja se ne može poniÅ¡titi." +confirmUnlock = "Jeste li sigurni da želite otkljuÄati ovaj korisniÄki raÄun?" deleteUser = "IzbriÅ¡i korisnika" deleteUserError = "Nije uspjelo brisanje korisnika" deleteUserSuccess = "Korisnik je uspjeÅ¡no izbrisan" @@ -7341,6 +8014,8 @@ disable = "Onemogući" disabled = "Onemogućeno" editRole = "Uredi ulogu" enable = "Omogući" +locked = "zakljuÄan" +lockedBadge = "ZakljuÄan" loading = "UÄitavanje osoba..." loginRequired = "Najprije omogućite naÄin prijave" member = "ÄŒlan" @@ -7350,6 +8025,9 @@ searchMembers = "Pretraži Älanove..." status = "Status" team = "Tim" title = "Osobe" +unlockAccount = "OtkljuÄaj raÄun" +unlockUserError = "OtkljuÄavanje korisniÄkog raÄuna nije uspjelo" +unlockUserSuccess = "KorisniÄki raÄun je uspjeÅ¡no otkljuÄan" user = "Korisnik" [workspace.people.actions] diff --git a/frontend/public/locales/hu-HU/translation.toml b/frontend/public/locales/hu-HU/translation.toml index 0830a287d5..1f239e71f2 100644 --- a/frontend/public/locales/hu-HU/translation.toml +++ b/frontend/public/locales/hu-HU/translation.toml @@ -8,6 +8,7 @@ black = "Fekete" blue = "Kék" bored = "Unatkozik várakozás közben?" cancel = "Mégse" +confirm = "MegerÅ‘sítés" changedCredsMessage = "A hitelesítési adatok megváltoztak!" chooseFile = "Fájl kiválasztása" close = "Bezárás" @@ -146,6 +147,7 @@ insufficientCredits = "Elégtelen kreditek. Szükséges: {{requiredCredits}}, El loadingCredits = "Kreditek ellenÅ‘rzése..." loadingProStatus = "ElÅ‘fizetési állapot ellenÅ‘rzése..." noticeTopUpOrPlan = "Nincs elég kredit, kérjük, töltsön fel vagy váltson csomagra" +accessInvite = "Meghívás" [account] accountSettings = "Fiókbeállítások" @@ -1427,6 +1429,34 @@ title = "Feldolgozás" description = "Maximális várakozási idÅ‘ egy feldolgozási feladatra hiba jelentése elÅ‘tt." label = "Feldolgozási idÅ‘korlát (másodperc)" +[admin.settings.storage] +description = "A szerver tárhely- és megosztási beállításainak kezelése." +title = "Fájltárolás és megosztás" + +[admin.settings.storage.enabled] +description = "Engedélyezi, hogy a felhasználók fájlokat tároljanak a szerveren." +label = "Szerveres fájltárolás engedélyezése" + +[admin.settings.storage.sharing.email] +description = "E-mail-címekkel való megosztás engedélyezése." +label = "E-mailes megosztás engedélyezése" +mailLink = "Levelezési beállítások konfigurálása" +mailNote = "Levelezési beállítás szükséges. " + +[admin.settings.storage.sharing.enabled] +description = "Engedélyezi a tárolt fájlok megosztását." +label = "Megosztás engedélyezése" + +[admin.settings.storage.sharing.links] +description = "Megosztás engedélyezése bejelentkezést igénylÅ‘ linkeken keresztül." +frontendUrlLink = "Beállítás a Rendszerbeállításokban" +frontendUrlNote = "Frontend URL szükséges. " +label = "Megosztási linkek engedélyezése" + +[admin.settings.storage.signing.enabled] +description = "Engedélyezi több résztvevÅ‘s dokumentum-aláírási munkamenetek létrehozását. A szerveres fájltárolás engedélyezése szükséges." +label = "Csoportos aláírás engedélyezése (Alpha)" + [admin.settings.unsavedChanges] cancel = "Szerkesztés folytatása" discard = "Módosítások elvetése" @@ -2059,7 +2089,19 @@ numbers = "Számok/tartományok: 5, 10-20" progressions = "Sorozatok: 3n, 4n+1" [certSign] +allSigned = "Minden résztvevÅ‘ aláírt. Készen áll a véglegesítésre." +awaitingSignatures = "Aláírásokra vár" +signatureProgress = "{{signedCount}}/{{totalCount}} aláírás" chooseCertificate = "Tanúsítványfájl kiválasztása" +declined = "Elutasítva" +fetchFailed = "Az aláírási adatok betöltése sikertelen" +finalized = "Véglegesítve" +notified = "FüggÅ‘ben" +partialNote = "A jelenlegi aláírásokkal korábban is véglegesíthet. Az aláíratlan résztvevÅ‘k ki lesznek hagyva." +pending = "FüggÅ‘ben" +readyToFinalize = "Készen áll a véglegesítésre" +signed = "Aláírva" +viewed = "Megtekintve" chooseJksFile = "JKS fájl kiválasztása" chooseP12File = "PKCS12 fájl kiválasztása" choosePfxFile = "PFX fájl kiválasztása" @@ -2082,6 +2124,7 @@ title = "Tanúsítvánnyal aláírás" invisible = "Láthatatlan" stepTitle = "Aláírás megjelenése" visible = "Látható" +visibility = "Láthatóság" [certSign.appearance.options] title = "Aláírás részletei" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Véglegesítés és az aláírt PDF betöltése" +early = "Véglegesítés a jelenlegi aláírásokkal" + +[certSign.collab.sessionDetail] +addButton = "RésztvevÅ‘k hozzáadása" +addParticipants = "RésztvevÅ‘k hozzáadása" +addParticipantsError = "A résztvevÅ‘k hozzáadása sikertelen" +backToList = "Vissza a munkamenetekhez" +deleteConfirm = "Biztos benne? Ez a művelet nem vonható vissza." +deleteError = "A munkamenet törlése sikertelen" +deleted = "Munkamenet törölve" +deleteSession = "Munkamenet törlése" +dueDate = "HatáridÅ‘" +finalizeError = "A munkamenet véglegesítése sikertelen" +loadPdfError = "Az aláírt PDF betöltése sikertelen" +loadSignedPdf = "Aláírt PDF betöltése az Aktív fájlok közé" +messageLabel = "Üzenet" +noAdditionalInfo = "Nincs további információ" +owner = "Tulajdonos" +participantRemoved = "RésztvevÅ‘ eltávolítva" +participants = "RésztvevÅ‘k" +participantsAdded = "A résztvevÅ‘k hozzáadása sikeres" +removeParticipant = "Eltávolítás" +removeParticipantError = "A résztvevÅ‘ eltávolítása sikertelen" +selectUsers = "Felhasználók kiválasztása..." +sessionInfo = "Munkamenet-információ" +workbenchTitle = "Munkamenet-kezelés" + +[certSign.collab.signRequest] +addedToFiles = "A dokumentum hozzáadva az aktív fájlokhoz" +addSignature = "Adja hozzá az aláírását" +addToFiles = "Hozzáadás az aktív fájlokhoz" +advancedSettings = "Speciális beállítások" +backToList = "Vissza az aláírási kérelmekhez" +certificateChoice = "Válasszon tanúsítványt az aláíráshoz" +changeSignature = "Aláírás megváltoztatása" +clearSignature = "Aláírás törlése" +completeAndSign = "Kitöltés és aláírás" +createNewSignature = "Új aláírás létrehozása" +declineButton = "Elutasítás" +decline = "Kérelem elutasítása" +deleteSelected = "Kiválasztott aláírás törlése" +drawSignature = "Rajzolja le az aláírását lent" +dueDate = "HatáridÅ‘" +fileTooLarge = "A fájlméretnek 5 MB alatt kell lennie" +fontFamily = "Betűcsalád" +fontSize = "Betűméret: {{size}}px" +fontSizePlaceholder = "Méret" +from = "Feladó" +invalidCertFile = "Válasszon P12 vagy PFX tanúsítványfájlt" +invalidFileType = "Válasszon képfájlt" +location = "Hely (opcionális)" +locationPlaceholder = "Honnan ír alá?" +message = "Üzenet" +noCertificate = "Válasszon tanúsítványfájlt" +noSignatures = "Helyezzen el legalább egy aláírást a PDF-en" +p12File = "P12/PFX tanúsítványfájl" +password = "Tanúsítvány jelszava" +passwordPlaceholder = "Adja meg a jelszót..." +penColor = "Toll színe" +penSize = "Toll vastagsága: {{size}}px" +placementActive = "Kattintson a PDF-re az elhelyezéshez" +placeSignatureButton = "Aláírás elhelyezése a PDF-en" +reason = "Indoklás (opcionális)" +reasonPlaceholder = "Miért ír alá?" +removeImage = "Kép eltávolítása" +removeCertFile = "Fájl eltávolítása" +savedSignatures = "Mentett aláírások" +selectFile = "Képfájl kiválasztása" +selectSignatureTitle = "Aláírás kiválasztása vagy létrehozása" +signButton = "Dokumentum aláírása" +signatureInfo = "Ezeket a beállításokat a dokumentum tulajdonosa állította be" +signaturePlaced = "Aláírás elhelyezve az oldalon" +signatureSettings = "Aláírás beállításai" +signatureText = "Aláírás szövege" +signatureTextPlaceholder = "Adja meg a nevét..." +signatureTypeLabel = "Aláírás típusa" +signingTitle = "Aláírás" +textColor = "Szöveg színe" +typeSignature = "Ãrja be a nevét az aláírás létrehozásához" +uploadCert = "Egyéni tanúsítvány" +uploadCertDesc = "Használja saját P12/PFX tanúsítványát" +uploadSignature = "Töltse fel az aláírás képfájlját" +usePersonalCert = "Személyes tanúsítvány" +usePersonalCertDesc = "Automatikusan generálva a fiókjához" +useServerCert = "Szervezeti tanúsítvány" +useServerCertDesc = "Megosztott szervezeti tanúsítvány" +workbenchTitle = "Aláírási kérelem" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Vonal színének kiválasztása" +continue = "Folytatás" + +[certSign.collab.signRequest.certModal] +description = "{{count}} aláírást helyezett el. Válassza ki a tanúsítványt az aláírás befejezéséhez." +sign = "Dokumentum aláírása" +certValidating = "Tanúsítvány ellenÅ‘rzése..." +certValidUntil = "A tanúsítvány érvényes eddig: {{date}}" +certInvalid = "Érvénytelen tanúsítvány: {{error}}" +certInvalidFallback = "Érvénytelen tanúsítvány" +certNetworkError = "A tanúsítvány nem ellenÅ‘rizhetÅ‘" +title = "Tanúsítvány beállítása" + +[certSign.collab.signRequest.image] +hint = "Töltsön fel egy PNG vagy JPG képet az aláírásáról" + +[certSign.collab.signRequest.mode] +move = "Aláírás mozgatása" +place = "Aláírás elhelyezése" +title = "Aláírás vagy mozgatás mód" + +[certSign.collab.signRequest.modeTabs] +draw = "Rajz" +image = "Feltöltés" +text = "Gépelés" + +[certSign.collab.signRequest.placeSignature] +message = "Kattintson a PDF-re az aláírás elhelyezéséhez" +title = "Aláírás elhelyezése" + +[certSign.collab.signRequest.preview] +imageAlt = "Kiválasztott aláírás" +missing = "Nincs elÅ‘nézet" +textFallback = "Aláírás" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Rajzolt aláírás" +defaultImageLabel = "Feltöltött aláírás" +defaultLabel = "Aláírás" +defaultTextLabel = "Gépelt aláírás" +delete = "Aláírás törlése" +none = "Nincsenek mentett aláírások" + +[certSign.collab.signRequest.signatureType] +draw = "Rajz" +type = "Gépelés" +upload = "Feltöltés" + +[certSign.collab.signRequest.steps] +back = "Vissza" +cancelPlacement = "Elhelyezés megszakítása" +certificate = "Tanúsítvány" +clickMultipleTimes = "Kattintson többször a PDF-re az aláírások elhelyezéséhez. Húzással mozgathatja vagy átméretezheti Å‘ket." +clickToPlace = "Kattintson a PDF-en arra a helyre, ahol meg szeretné jeleníteni az aláírását." +continue = "Tovább a tanúsítvány kiválasztásához" +continueToPlacement = "Tovább az elhelyezéshez" +continueToReview = "Tovább az áttekintéshez" +createSignature = "Aláírás létrehozása" +invisible = "Láthatatlan" +location = "Hely:" +multipleSignatures = "{{count}} aláírás kerül alkalmazásra a PDF-re" +oneSignature = "1 aláírás kerül alkalmazásra a PDF-re" +placeOnPdf = "Elhelyezés a PDF-en" +reason = "Indoklás:" +reviewTitle = "Ãttekintés aláírás elÅ‘tt" +signaturePlaced = "Aláírás elhelyezve a(z) {{page}}. oldalon. Újabb kattintással módosíthatja a pozíciót, vagy folytathatja az áttekintéssel." +visible = "Látható" +visibility = "Láthatóság:" +yourSignatures = "Az Ön aláírásai ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Szín" +fontLabel = "Betűtípus" +fontSizeLabel = "Méret" +fontSizePlaceholder = "16" +label = "Aláírás szövege" +modalHint = "Adja meg a nevét, majd kattintson a Folytatás gombra az elhelyezéshez a PDF-en." +placeholder = "Adja meg a nevét..." + +[certSign.collab.participant] +certValidating = "Tanúsítvány ellenÅ‘rzése..." +certValid = "✓ Érvényes tanúsítvány" +certValidUntil = " eddig: {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Érvénytelen tanúsítvány" +certNetworkError = "A tanúsítvány nem ellenÅ‘rizhetÅ‘" + +[certSign.collab.addParticipants] +add = "{{count}} résztvevÅ‘ hozzáadása" +back = "Vissza" +configureSignatures = "Aláírási beállítások konfigurálása" +continue = "Tovább az aláírási beállításokhoz" +reasonHelp = "ElÅ‘re megadhatja az aláírás okát ezeknek a résztvevÅ‘knek (opcionális, aláíráskor felülírhatják)" +reasonPlaceholder = "pl. Jóváhagyás, Ãttekintés..." +selectUsers = "Felhasználók kiválasztása" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Aláírási összefoglaló oldal hozzáadása" +includeSummaryPageHelp = "A dokumentum végére egy összefoglaló oldal kerül az összes aláírási metaadattal. Az egyes oldalakon lévÅ‘ digitális tanúsítvány aláírási keretek el lesznek rejtve (a kézírásos aláírásokat ez nem érinti)." + +[certSign.collab.sessionList] +active = "Aktív" +finalized = "Véglegesítve" + +[certSign.collab.signatureSettings] +description = "Beállíthatja, hogyan jelenjenek meg az aláírások minden résztvevÅ‘nél" +title = "Aláírás megjelenése" + +[certSign.collab.userSelector] +inviteUsers = "Felhasználók hozzáadása" +loadError = "A felhasználók betöltése sikertelen" +noTeam = "Nincs csapat" +noUsers = "Nem található más felhasználó." +placeholder = "Felhasználók kiválasztása..." + +[certSign.mobile] +panelActions = "Műveletek" +panelDocument = "Dokumentum" +panelPeople = "Személyek" + +[certSign.sessions] +deleted = "Munkamenet törölve" +fetchFailed = "A munkamenet részleteinek betöltése sikertelen" +finalized = "Munkamenet véglegesítve" +loaded = "Aláírt PDF betöltve" +pdfNotReady = "A PDF még nem kész" +pdfNotReadyDesc = "Az aláírt PDF előállítása folyamatban van. Próbálja meg késÅ‘bb." + +[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" + [changeCreds] changePassword = "Az alapértelmezett bejelentkezési adatokat használja. Kérjük, adjon meg új jelszót" changeUsername = "Felhasználónév frissítése. A frissítés után ki lesz jelentkeztetve." @@ -3242,6 +3531,46 @@ totalSelected = "Összesen kiválasztva" unsupported = "Nem támogatott" unzip = "Kicsomagolás" uploadError = "Néhány fájl feltöltése nem sikerült." +copyCreated = "Másolat elmentve erre az eszközre." +copyFailed = "Nem sikerült másolatot létrehozni." +leaveShare = "Eltávolítás a listámról" +leaveShareFailed = "Nem sikerült eltávolítani a megosztott fájlt." +leaveShareSuccess = "Eltávolítva a megosztott listájáról." +removeBoth = "Eltávolítás mindkettÅ‘rÅ‘l" +removeFilePrompt = "Ez a fájl ezen az eszközön és a szerveren is mentve van. Honnan szeretné eltávolítani?" +removeFileTitle = "Fájl eltávolítása" +removeLocalOnly = "Csak errÅ‘l az eszközrÅ‘l" +removeServerFailed = "Nem sikerült eltávolítani a fájlt a szerverrÅ‘l." +removeServerOnly = "Csak a szerverrÅ‘l" +removeServerOnlyPrompt = "Ez a fájl csak a szerveren van tárolva. Eltávolítja a szerverrÅ‘l?" +removeServerSuccess = "Eltávolítva a szerverrÅ‘l." +removeSharedPrompt = "Ezt a fájlt megosztották Önnel. Eltávolíthatja errÅ‘l az eszközrÅ‘l vagy a megosztott listájáról." +removeSharedServerOnlyBlockedPrompt = "Ezt a fájlt megosztották Önnel, és csak a szerveren van tárolva." +removeSharedServerOnlyPrompt = "Ezt a fájlt megosztották Önnel, és csak a szerveren van tárolva. Eltávolítja a listájáról?" +changesNotUploaded = "A módosítások nincsenek feltöltve" +cloudFile = "FelhÅ‘fájl" +filterAll = "Összes" +filterLocal = "Helyi" +filterSharedByMe = "Ãltalam megosztva" +filterSharedWithMe = "Velem megosztva" +lastSynced = "Utoljára szinkronizálva" +localOnly = "Csak helyi" +makeCopy = "Másolat készítése" +owner = "Tulajdonos" +ownerUnknown = "Ismeretlen" +share = "Megosztás" +shareSelected = "Kijelöltek megosztása" +sharedByYou = "Ön által megosztva" +sharedEditNoticeBody = "Nincs szerkesztési joga a fájl szerveres verziójához. A módosítások helyi másolatként lesznek mentve." +sharedEditNoticeConfirm = "Értettem" +sharedEditNoticeTitle = "Csak olvasható szerverpéldány" +sharedWithYou = "Önnel megosztva" +sharing = "Megosztás" +storageState = "Tárhely" +synced = "Szinkronizálva" +updateOnServer = "Frissítés a szerveren" +uploadSelected = "Kijelöltek feltöltése" +uploadToServer = "Feltöltés a szerverre" [files] addFiles = "Fájlok hozzáadása" @@ -3367,6 +3696,77 @@ title = "PDF-ek lapításáról" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "A csoportos aláírásról" + +[groupSigning.tooltip.finalization] +bullet1 = "Minden aláírás az Ön által meghatározott résztvevÅ‘i sorrendben kerül alkalmazásra" +bullet2 = "Szükség esetén részleges aláírásokkal is véglegesíthet" +bullet3 = "A véglegesítés után a munkamenet nem módosítható" +description = "Miután minden résztvevÅ‘ aláírt (vagy korábban véglegesít), elkészítheti a végleges, aláírt PDF-et." +title = "Véglegesítési folyamat" + +[groupSigning.tooltip.roles] +bullet1 = "Tulajdonos (Ön): munkamenetet hoz létre, beállítja az aláírási alapértelmezéseket, véglegesíti a dokumentumot" +bullet2 = "RésztvevÅ‘k: elkészítik az aláírásukat, tanúsítványt választanak, elhelyezik a PDF-en" +bullet3 = "A résztvevÅ‘k nem módosíthatják az aláírás láthatóságát, okát vagy helyét" +description = "Ön szabályozza az aláírás megjelenésének beállításait minden résztvevÅ‘ számára." +title = "Szerepkörök" + +[groupSigning.tooltip.sequential] +bullet1 = "Az elsÅ‘ résztvevÅ‘nek alá kell írnia, mielÅ‘tt a második hozzáférhet a dokumentumhoz" +bullet2 = "Biztosítja a megfelelÅ‘ aláírási sorrendet a jogszabályok betartásához" +bullet3 = "A résztvevÅ‘k sorrendje a listában húzással módosítható" +description = "A résztvevÅ‘k az Ön által meghatározott sorrendben írják alá a dokumentumot. Minden aláíró értesítést kap, amikor rá kerül a sor." +title = "Szekvenciális aláírás" + +[groupSigning.steps] +back = "Vissza" +completed = "Kész" +current = "Aktuális" +stepLabel = "{{number}}. lépés" + +[groupSigning.steps.configureDefaults] +continue = "Tovább az áttekintéshez" +invisible = "Az aláírások láthatatlanok lesznek (csak metaadat)" +locationLabel = "Hely:" +preview = "ElÅ‘nézet" +reasonLabel = "Indoklás:" +title = "Aláírási beállítások konfigurálása" +visible = "Az aláírások a(z) {{page}}. oldalon lesznek láthatók" + +[groupSigning.steps.review] +document = "Dokumentum" +dueDate = "HatáridÅ‘ (opcionális)" +dueDatePlaceholder = "Válasszon határidÅ‘t..." +invisible = "Láthatatlan (csak metaadat)" +location = "Hely:" +logo = "Logó:" +logoHidden = "Nincs logó" +logoShown = "Stirling PDF logó megjelenítve" +participants = "RésztvevÅ‘k" +reason = "Indoklás:" +send = "Aláírási kérelmek küldése" +signatureSettings = "Aláírás beállításai" +title = "Munkamenet részleteinek áttekintése" +titleShort = "Ãttekintés és küldés" +visibility = "Láthatóság:" +visible = "Látható a(z) {{page}}. oldalon" +participantCount = "{{count}} résztvevÅ‘ ír alá sorrendben" + +[groupSigning.steps.selectDocument] +continue = "Tovább a résztvevÅ‘k kiválasztásához" +noFile = "Válasszon ki egyetlen PDF fájlt az aktív fájlok közül az aláírási munkamenet létrehozásához." +selectedFile = "Kiválasztott dokumentum" +title = "Dokumentum kiválasztása" + +[groupSigning.steps.selectParticipants] +continue = "Tovább az aláírási beállításokhoz" +count = "{{count}} résztvevÅ‘ kiválasztva" +label = "RésztvevÅ‘k kiválasztása" +placeholder = "Válassza ki az aláíró résztvevÅ‘ket..." +title = "RésztvevÅ‘k kiválasztása" + [getPdfInfo] downloadJson = "JSON letöltése" downloads = "Letöltések" @@ -4460,7 +4860,10 @@ zoomOut = "Kicsinyítés" [viewer] cannotPreviewFile = "A fájl elÅ‘nézete nem lehetséges" +disableColorFilter = "SzínszűrÅ‘ kikapcsolása" dualPageView = "Kétoldalas nézet" +enableDarkFilter = "Sötét szűrÅ‘ engedélyezése" +enableSepiaFilter = "Szépia szűrÅ‘ engedélyezése" firstPage = "ElsÅ‘ oldal" lastPage = "Utolsó oldal" nextPage = "KövetkezÅ‘ oldal" @@ -4470,6 +4873,22 @@ singlePageView = "Egyoldalas nézet" unknownFile = "Ismeretlen fájl" zoomIn = "Nagyítás" zoomOut = "Kicsinyítés" +resetZoom = "Nagyítás visszaállítása" + +[viewer.nonPdf] +fileTypeBadge = "{{type}} fájl" +convertToPdf = "Konvertálás PDF-be" +loading = "Betöltés..." +emptyFile = "Üres fájl" +csvStats = "{{rows}} sor · {{columns}} oszlop · {{size}}" +sortedBy = "Rendezve: {{column}}" +columnDefault = "{{index}}. oszlop" +htmlPreviewWarning = "HTML elÅ‘nézet — a külsÅ‘ erÅ‘források nem biztos, hogy betöltÅ‘dnek · {{size}}" +htmlPreview = "HTML elÅ‘nézet" +invalidJson = "Érvénytelen JSON — nyers tartalom megjelenítése" +textStats = "{{lines}} sor · {{size}}" +lineNumbers = "Sorszámok" +renderMarkdown = "Markdown megjelenítése" [viewer.attachments] title = "Mellékletek" @@ -4531,6 +4950,7 @@ toggleAttachments = "Mellékletek megjelenítése/elrejtése" toggleTheme = "Téma váltása" language = "Nyelv" toggleAnnotations = "Jegyzetek láthatóságának váltása" +toggleLayers = "Rétegek váltása" search = "PDF keresése" panMode = "Pásztázó mód" applyRedactionsFirst = "ElÅ‘bb alkalmazza a kitakarásokat" @@ -5407,20 +5827,72 @@ title = "Fájl nyomtatása" 2 = "Adja meg a nyomtató nevét" [quickAccess] +access = "Hozzáférés" +accessAddPerson = "További személy hozzáadása" +accessBack = "Vissza" +accessCopyLink = "Hivatkozás másolása" +accessEmail = "E-mail-cím" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Fájl" +accessGeneral = "Ãltalános hozzáférés" +accessInviteTitle = "Személyek meghívása" +accessOwner = "Tulajdonos" +accessPanel = "Dokumentum-hozzáférés" +accessPeople = "Hozzáféréssel rendelkezÅ‘k" +accessRemove = "Eltávolítás" +accessRestricted = "Korlátozott" +accessRestrictedHint = "Csak a hozzáféréssel rendelkezÅ‘k nyithatják meg" +accessRole = "Szerepkör" +accessRoleCommenter = "Hozzászóló" +accessRoleEditor = "SzerkesztÅ‘" +accessRoleViewer = "MegtekintÅ‘" +accessSelectedFile = "Kiválasztott fájl" +accessSendInvite = "Meghívó küldése" +accessTitle = "Dokumentum-hozzáférés" +accessYou = "Ön" account = "Fiók" +activeSessions = "Aktív munkamenetek" +activeTab = "Aktív" activity = "Napló" adminSettings = "Admin beáll." +allSessions = "Összes munkamenet" allTools = "All Tools" automate = "Autom." +back = "Vissza" +certSign = "Tanúsítványos aláírás" +completedSessions = "Befejezett munkamenetek" +completedTab = "Befejezett" config = "Konfig" +createNew = "Új kérelem létrehozása" +createSession = "Aláírási kérelem létrehozása" +dueDate = "HatáridÅ‘ (opcionális)" files = "Fájlok" help = "Súgó" +noActiveSessions = "Nincsenek függÅ‘ben lévÅ‘ aláírási kérelmek vagy aktív munkamenetek" +noCompletedSessions = "Nincs befejezett munkamenet" +noFile = "Nincs fájl kiválasztva" read = "Olvasás" reader = "Olvasó" +refresh = "Frissítés" +requestSignatures = "Aláírások kérése" +selectSingleFileToRequest = "Válasszon ki egyetlen PDF fájlt az aláírások kéréséhez" +selectedFile = "Kiválasztott fájl" +selectUsers = "Válassza ki az aláíró felhasználókat" +selectUsersPlaceholder = "Válasszon résztvevÅ‘ket..." +sendingRequest = "Küldés..." settings = "Beáll." showMeAround = "Vezessen körbe" sign = "Aláírás" +signatureRequests = "Aláírási kérelmek" +signYourself = "Saját aláírás" +newRequest = "Új kérelem" tours = "Bemutatók" +wetSign = "Aláírás hozzáadása" +filterMine = "Saját" +filterOverdue = "Lejárt" +filterSigned = "Aláírva" +filterDeclined = "Elutasítva" +searchDocuments = "Dokumentumok keresése…" [quickAccess.helpMenu] adminTour = "Admin túra" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "A Stirling-PDF szervere offline, és a(z) \"{{endpoin expired = "A munkamenet lejárt. Kérjük, frissítse az oldalt és próbálja újra." refreshPage = "Oldal frissítése" +[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" + +[signatureSettings.tooltip] +header = "Aláírás megjelenésének beállításai" + +[signatureSettings.tooltip.location] +bullet1 = "Példák: „New York, USAâ€, „London irodaâ€, „Távoliâ€" +bullet2 = "Nem azonos az oldalon elfoglalt helyzettel" +bullet3 = "Bizonyos joghatóságokban kötelezÅ‘ lehet" +description = "Opcionális földrajzi hely, ahol az aláírás történt. A tanúsítvány metaadataiban tárolódik." +title = "Aláírás helye" + +[signatureSettings.tooltip.logo] +bullet1 = "Az aláírás és a szöveg mellett jelenik meg" +bullet2 = "Támogatott formátumok: PNG, JPG" +bullet3 = "Professzionális megjelenést kölcsönöz" +description = "Adjon vállalati logót a látható aláírásokhoz a márkaépítés és a hitelesség érdekében." +title = "Vállalati logó" + +[signatureSettings.tooltip.reason] +bullet1 = "Példák: „Jóváhagyásâ€, „SzerzÅ‘déskötésâ€, „Ãttekintés készâ€" +bullet2 = "Látható a PDF aláírási tulajdonságaiban" +bullet3 = "Hasznos ellenÅ‘rzési nyomvonalhoz és megfeleléshez" +description = "Opcionális szöveg, amely megmagyarázza az aláírás okát. A tanúsítvány metaadataiban tárolódik." +title = "Aláírás oka" + +[signatureSettings.tooltip.visibility] +bullet1 = "Látható: az aláírás egyéni megjelenéssel látható a PDF-en" +bullet2 = "Láthatatlan: a tanúsítvány vizuális jelölés nélkül beágyazva" +bullet3 = "A láthatatlan aláírások is kriptográfiai hitelesítést nyújtanak" +description = "Szabályozza, hogy az aláírás látható-e a dokumentumban vagy láthatatlanul legyen beágyazva." +title = "Aláírás láthatósága" + [settings.configuration] advanced = "Speciális" database = "Adatbázis" endpoints = "Végpontok" features = "Funkciók" +storageSharing = "Fájltárolás és megosztás" systemSettings = "Rendszerbeállítások" title = "Konfiguráció" @@ -6332,10 +6868,13 @@ title = "Bejelentkezés a Stirlingbe" [setup.selfhosted] link = "vagy csatlakozzon egy saját üzemeltetésű fiókhoz" subtitle = "Adja meg a szerver hitelesítÅ‘ adatait" +changeServerLocked = "A szervezete egy konkrét szerverre korlátozta az alkalmazást" switchToLocal = "Használja inkább a helyi eszközöket" title = "Bejelentkezés a szerverre" [setup.selfhosted.unreachable] +changeServer = "Kapcsolódás másik szerverhez" +changeServerLocked = "A szervezete egy konkrét szerverre korlátozta az alkalmazást" continueOffline = "Használja inkább a helyi eszközöket" message = "Nem sikerült elérni: {{url}}. EllenÅ‘rizze, hogy a szerver fut-e és elérhetÅ‘-e." retry = "Újrapróbálás" @@ -6529,6 +7068,15 @@ saved = "Mentett" text = "Szöveg" title = "Aláírás típusa" +[signRequest] +declined = "Aláírási kérelem elutasítva" +fetchFailed = "Az aláírási kérelem betöltése sikertelen" +signed = "A dokumentum sikeresen aláírva" + +[signSession] +createFailed = "Az aláírási kérelem létrehozása sikertelen" +created = "Aláírási kérelem elküldve" + [signup] accountCreatedSuccessfully = "A fiók sikeresen létrejött! Most már bejelentkezhet." alreadyHaveAccount = "Már van fiókja? Jelentkezzen be" @@ -6807,6 +7355,106 @@ title = "PDF felosztása fejezetek szerint" [splitPdfByChapters] tags = "felosztás,fejezetek,könyvjelzÅ‘k,rendszerezés" +[storageShare] +accessed = "Megnyitva" +accessDenied = "Nincs hozzáférése ehhez a megosztott fájlhoz. Kérje meg a tulajdonost, hogy ossza meg Önnel." +accessFailed = "Az aktivitás nem tölthetÅ‘ be." +accessDeniedBody = "Nincs hozzáférése ehhez a fájlhoz. Kérje meg a tulajdonost, hogy ossza meg Önnel." +accessDeniedTitle = "Nincs hozzáférés" +accessLimitedCommenter = "A hozzászólói hozzáférés hamarosan elérhetÅ‘. Letöltéshez kérjen szerkesztÅ‘i jogosultságot a tulajdonostól." +accessLimitedTitle = "Korlátozott hozzáférés" +accessLimitedViewer = "Ez a hivatkozás csak megtekintésre szolgál. Letöltéshez kérjen szerkesztÅ‘i jogosultságot a tulajdonostól." +createdAt = "Létrehozva" +download = "Letöltés" +downloadFailed = "A fájl nem tölthetÅ‘ le." +expiredBody = "Ez a megosztási hivatkozás érvénytelen vagy lejárt." +expiredTitle = "A hivatkozás lejárt" +goToLogin = "Ugrás a bejelentkezéshez" +loadFailed = "A megosztott fájl nem nyitható meg." +loading = "Megosztási hivatkozás betöltése..." +loginPrompt = "Jelentkezzen be a megosztott fájl eléréséhez." +loginRequired = "Bejelentkezés szükséges" +openInApp = "Megnyitás a Stirling PDF-ben" +ownerLabel = "Tulajdonos" +ownerUnknown = "Ismeretlen" +requiresLogin = "Ehhez a megosztott fájlhoz bejelentkezés szükséges." +roleCommenter = "Hozzászóló" +roleEditor = "SzerkesztÅ‘" +roleViewer = "MegtekintÅ‘" +shareHeading = "Megosztott fájl" +titleDefault = "Megosztott fájl" +tryAgain = "Kérjük, próbálja meg késÅ‘bb." +addUser = "Hozzáadás" +commenterHint = "A hozzászólás hamarosan elérhetÅ‘." +copied = "Hivatkozás a vágólapra másolva" +copy = "Másolás" +copyFailed = "A másolás sikertelen" +description = "Hozzon létre megosztási hivatkozást ehhez a fájlhoz. A bejelentkezett felhasználók a link birtokában elérhetik." +downloadsCount = "Letöltések: {{count}}" +emailWarningBody = "Ez e-mail-címnek tűnik. Ha ez a személy nem Stirling PDF felhasználó, nem fogja tudni elérni a fájlt." +emailWarningConfirm = "Megosztás mindenképp" +emailWarningTitle = "E-mail-cím" +errorTitle = "A megosztás sikertelen" +failure = "Nem sikerült megosztási hivatkozást létrehozni. Kérjük, próbálja újra." +fileLabel = "Fájl" +generate = "Hivatkozás létrehozása" +generated = "Megosztási hivatkozás létrehozva" +hideActivity = "Aktivitás elrejtése" +invalidUsername = "Adjon meg érvényes felhasználónevet vagy e-mail-címet." +lastAccessed = "Utoljára megnyitva" +linkAccessTitle = "Megosztási hivatkozás hozzáférése" +linkLabel = "Megosztási hivatkozás" +linksDisabled = "A megosztási hivatkozások le vannak tiltva." +linksDisabledBody = "A megosztási hivatkozások a szerver beállításai miatt le vannak tiltva." +manage = "Megosztás kezelése" +manageDescription = "Hozzon létre és kezeljen linkeket a fájl megosztásához." +manageLoadFailed = "A megosztási hivatkozások nem tölthetÅ‘k be." +manageTitle = "Megosztás kezelése" +noActivity = "Még nincs aktivitás." +noLinks = "Még nincsenek aktív megosztási hivatkozások." +noSharedUsers = "Még nincs felhasználó, aki hozzáfér." +removeLink = "Hivatkozás eltávolítása" +removeUser = "Eltávolítás" +revokeFailed = "A megosztási hivatkozás nem távolítható el." +revoked = "Megosztási hivatkozás eltávolítva" +roleLabel = "Szerepkör" +sharingDisabled = "A megosztás le van tiltva." +sharingDisabledBody = "A megosztást a szerver beállításai letiltották." +sharedUsersTitle = "Megosztott felhasználók" +title = "Fájl megosztása" +unknownUser = "Ismeretlen felhasználó" +userAddFailed = "Nem sikerült megosztani ezzel a felhasználóval." +userAdded = "Felhasználó hozzáadva a megosztási listához." +usernameLabel = "Felhasználónév vagy e‑mail-cím" +usernamePlaceholder = "Adjon meg egy felhasználónevet vagy e‑mail-címet" +userRemoveFailed = "Nem sikerült eltávolítani ezt a felhasználót." +userRemoved = "Felhasználó eltávolítva a megosztási listáról." +viewActivity = "Tevékenység megtekintése" +viewed = "Megtekintve" +viewsCount = "Megtekintések: {{count}}" +downloaded = "Letöltve" +bulkDescription = "Hozzon létre egy hivatkozást az összes kijelölt fájl megosztásához a bejelentkezett felhasználókkal." +bulkTitle = "Kijelölt fájlok megosztása" +copyLink = "Megosztási hivatkozás másolása" +fileCount = "{{count}} fájl kijelölve" +ownerOnly = "Csak a tulajdonos kezelheti a megosztást." +selectSingleFile = "Válasszon ki egyetlen fájlt a megosztás kezeléséhez." + +[storageUpload] +description = "Ez feltölti az aktuális fájlt a szerver tárhelyére, hogy Ön hozzáférhessen." +errorTitle = "A feltöltés sikertelen" +failure = "A feltöltés sikertelen. Kérjük, ellenÅ‘rizze a bejelentkezési és tárhelybeállításokat." +fileLabel = "Fájl" +hint = "A nyilvános hivatkozásokat és a hozzáférési módokat a szerver beállításai szabályozzák." +success = "Feltöltve a szerverre" +title = "Feltöltés a szerverre" +updateButton = "Frissítés a szerveren" +uploadButton = "Feltöltés a szerverre" +bulkDescription = "Ez a kijelölt fájlokat a szerver tárhelyére tölti fel." +bulkTitle = "Kijelölt fájlok feltöltése" +fileCount = "{{count}} fájl kijelölve" +more = " +{{count}} további" + [storage] approximateSize = "MegközelítÅ‘ méret" fileTooLarge = "A fájl túl nagy. A fájl maximális mérete" @@ -7153,6 +7801,30 @@ title = "PDF megtekintése/szerkesztése" [warning] tooltipTitle = "Figyelmeztetés" +[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" + [watermark] completed = "Vízjel hozzáadva" desc = "Szöveg- vagy képvízjelek hozzáadása PDF fájlokhoz" @@ -7333,6 +8005,7 @@ activeSession = "Aktív munkamenet" addMembers = "Tagok hozzáadása" admin = "Admin" confirmDelete = "Biztosan törli ezt a felhasználót? Ez a művelet nem vonható vissza." +confirmUnlock = "Biztosan feloldja ezt a felhasználói fiókot?" deleteUser = "Felhasználó törlése" deleteUserError = "A felhasználó törlése sikertelen" deleteUserSuccess = "Felhasználó sikeresen törölve" @@ -7341,6 +8014,8 @@ disable = "Letiltás" disabled = "Letiltva" editRole = "Szerepkör szerkesztése" enable = "Engedélyezés" +locked = "zárolva" +lockedBadge = "Zárolva" loading = "Tagok betöltése..." loginRequired = "ElÅ‘bb engedélyezze a bejelentkezési módot" member = "Tag" @@ -7350,6 +8025,9 @@ searchMembers = "Tagok keresése..." status = "Ãllapot" team = "Csapat" title = "Tagok" +unlockAccount = "Fiók feloldása" +unlockUserError = "Nem sikerült feloldani a felhasználói fiókot" +unlockUserSuccess = "A felhasználói fiók sikeresen feloldva" user = "Felhasználó" [workspace.people.actions] diff --git a/frontend/public/locales/id-ID/translation.toml b/frontend/public/locales/id-ID/translation.toml index 98a9c6a2cc..3ca00ceb3c 100644 --- a/frontend/public/locales/id-ID/translation.toml +++ b/frontend/public/locales/id-ID/translation.toml @@ -8,6 +8,7 @@ black = "Hitam" blue = "Biru" bored = "Bosan Menunggu?" cancel = "Batal" +confirm = "Konfirmasi" changedCredsMessage = "Kredensial berubah!!" chooseFile = "Pilih File" close = "Tutup" @@ -146,6 +147,7 @@ insufficientCredits = "Kredit tidak mencukupi. Diperlukan: {{requiredCredits}}, loadingCredits = "Memeriksa kredit..." loadingProStatus = "Memeriksa status langganan..." noticeTopUpOrPlan = "Kredit tidak mencukupi, silakan isi ulang atau upgrade ke paket" +accessInvite = "Undang" [account] accountSettings = "Pengaturan Akun" @@ -1427,6 +1429,34 @@ title = "Pemrosesan" description = "Waktu maksimum menunggu pekerjaan pemrosesan sebelum melaporkan kesalahan." label = "Batas Waktu Pemrosesan (detik)" +[admin.settings.storage] +description = "Kendalikan opsi penyimpanan dan berbagi server." +title = "Penyimpanan File & Berbagi" + +[admin.settings.storage.enabled] +description = "Izinkan pengguna menyimpan file di server." +label = "Aktifkan Penyimpanan File di Server" + +[admin.settings.storage.sharing.email] +description = "Izinkan berbagi dengan alamat email." +label = "Aktifkan Berbagi via Email" +mailLink = "Konfigurasikan Pengaturan Email" +mailNote = "Memerlukan konfigurasi email. " + +[admin.settings.storage.sharing.enabled] +description = "Izinkan pengguna untuk membagikan file yang tersimpan." +label = "Aktifkan Berbagi" + +[admin.settings.storage.sharing.links] +description = "Izinkan berbagi melalui tautan yang memerlukan masuk." +frontendUrlLink = "Konfigurasikan di Pengaturan Sistem" +frontendUrlNote = "Memerlukan URL Frontend. " +label = "Aktifkan Tautan Berbagi" + +[admin.settings.storage.signing.enabled] +description = "Izinkan pengguna membuat sesi penandatanganan dokumen multi-peserta. Memerlukan penyimpanan file di server diaktifkan." +label = "Aktifkan Penandatanganan Grup (Alpha)" + [admin.settings.unsavedChanges] cancel = "Lanjutkan Mengedit" discard = "Buang Perubahan" @@ -2059,7 +2089,19 @@ numbers = "Angka/rentang: 5, 10-20" progressions = "Progresi: 3n, 4n+1" [certSign] +allSigned = "Semua peserta telah menandatangani. Siap difinalisasi." +awaitingSignatures = "Menunggu tanda tangan" +signatureProgress = "{{signedCount}}/{{totalCount}} tanda tangan" chooseCertificate = "Pilih File Sertifikat" +declined = "Ditolak" +fetchFailed = "Gagal memuat data penandatanganan" +finalized = "Difinalisasi" +notified = "Tertunda" +partialNote = "Anda dapat memfinalisasi lebih awal dengan tanda tangan saat ini. Peserta yang belum menandatangani akan dikecualikan." +pending = "Tertunda" +readyToFinalize = "Siap difinalisasi" +signed = "Ditandatangani" +viewed = "Dilihat" chooseJksFile = "Pilih File JKS" chooseP12File = "Pilih File PKCS12" choosePfxFile = "Pilih File PFX" @@ -2082,6 +2124,7 @@ title = "Penandatanganan Sertifikat" invisible = "Tidak terlihat" stepTitle = "Tampilan Tanda Tangan" visible = "Terlihat" +visibility = "Visibilitas" [certSign.appearance.options] title = "Detail Tanda Tangan" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Finalisasi dan Muat PDF Bertanda Tangan" +early = "Finalisasi dengan Tanda Tangan Saat Ini" + +[certSign.collab.sessionDetail] +addButton = "Tambah Peserta" +addParticipants = "Tambah Peserta" +addParticipantsError = "Gagal menambahkan peserta" +backToList = "Kembali ke Sesi" +deleteConfirm = "Anda yakin? Ini tidak dapat dibatalkan." +deleteError = "Gagal menghapus sesi" +deleted = "Sesi dihapus" +deleteSession = "Hapus Sesi" +dueDate = "Tanggal Jatuh Tempo" +finalizeError = "Gagal memfinalisasi sesi" +loadPdfError = "Gagal memuat PDF bertanda tangan" +loadSignedPdf = "Muat PDF Bertanda Tangan ke File Aktif" +messageLabel = "Pesan" +noAdditionalInfo = "Tidak ada informasi tambahan" +owner = "Pemilik" +participantRemoved = "Peserta dihapus" +participants = "Peserta" +participantsAdded = "Peserta berhasil ditambahkan" +removeParticipant = "Hapus" +removeParticipantError = "Gagal menghapus peserta" +selectUsers = "Pilih pengguna..." +sessionInfo = "Info Sesi" +workbenchTitle = "Manajemen Sesi" + +[certSign.collab.signRequest] +addedToFiles = "Dokumen ditambahkan ke file aktif" +addSignature = "Tambahkan Tanda Tangan Anda" +addToFiles = "Tambahkan ke File Aktif" +advancedSettings = "Pengaturan Lanjutan" +backToList = "Kembali ke Permintaan Tanda Tangan" +certificateChoice = "Pilih sertifikat untuk menandatangani" +changeSignature = "Ubah tanda tangan" +clearSignature = "Hapus Tanda Tangan" +completeAndSign = "Lengkapi & Tanda Tangani" +createNewSignature = "Buat Tanda Tangan Baru" +declineButton = "Tolak" +decline = "Tolak Permintaan" +deleteSelected = "Hapus tanda tangan yang dipilih" +drawSignature = "Gambar tanda tangan Anda di bawah" +dueDate = "Tanggal Jatuh Tempo" +fileTooLarge = "Ukuran file harus kurang dari 5MB" +fontFamily = "Keluarga Font" +fontSize = "Ukuran Font: {{size}}px" +fontSizePlaceholder = "Ukuran" +from = "Dari" +invalidCertFile = "Pilih file sertifikat P12 atau PFX" +invalidFileType = "Pilih file gambar" +location = "Lokasi (Opsional)" +locationPlaceholder = "Dari mana Anda menandatangani?" +message = "Pesan" +noCertificate = "Pilih file sertifikat" +noSignatures = "Tempatkan setidaknya satu tanda tangan pada PDF" +p12File = "File Sertifikat P12/PFX" +password = "Kata Sandi Sertifikat" +passwordPlaceholder = "Masukkan kata sandi..." +penColor = "Warna Pena" +penSize = "Ukuran Pena: {{size}}px" +placementActive = "Klik PDF untuk menempatkan" +placeSignatureButton = "Tempatkan Tanda Tangan pada PDF" +reason = "Alasan (Opsional)" +reasonPlaceholder = "Mengapa Anda menandatangani?" +removeImage = "Hapus Gambar" +removeCertFile = "Hapus File" +savedSignatures = "Tanda Tangan Tersimpan" +selectFile = "Pilih File Gambar" +selectSignatureTitle = "Pilih atau Buat Tanda Tangan" +signButton = "Tanda Tangani Dokumen" +signatureInfo = "Pengaturan ini dikonfigurasi oleh pemilik dokumen" +signaturePlaced = "Tanda tangan ditempatkan pada halaman" +signatureSettings = "Pengaturan Tanda Tangan" +signatureText = "Teks Tanda Tangan" +signatureTextPlaceholder = "Masukkan nama Anda..." +signatureTypeLabel = "Jenis Tanda Tangan" +signingTitle = "Penandatanganan" +textColor = "Warna Teks" +typeSignature = "Ketik nama Anda untuk membuat tanda tangan" +uploadCert = "Sertifikat Kustom" +uploadCertDesc = "Gunakan sertifikat P12/PFX Anda sendiri" +uploadSignature = "Unggah gambar tanda tangan Anda" +usePersonalCert = "Sertifikat Pribadi" +usePersonalCertDesc = "Dibuat otomatis untuk akun Anda" +useServerCert = "Sertifikat Organisasi" +useServerCertDesc = "Sertifikat organisasi bersama" +workbenchTitle = "Permintaan Tanda Tangan" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Pilih warna goresan" +continue = "Lanjutkan" + +[certSign.collab.signRequest.certModal] +description = "Anda telah menempatkan {{count}} tanda tangan. Pilih sertifikat Anda untuk menyelesaikan penandatanganan." +sign = "Tanda Tangani Dokumen" +certValidating = "Memvalidasi sertifikat..." +certValidUntil = "Sertifikat berlaku hingga {{date}}" +certInvalid = "Sertifikat tidak valid: {{error}}" +certInvalidFallback = "Sertifikat tidak valid" +certNetworkError = "Tidak dapat memvalidasi sertifikat" +title = "Konfigurasikan Sertifikat" + +[certSign.collab.signRequest.image] +hint = "Unggah gambar PNG atau JPG dari tanda tangan Anda" + +[certSign.collab.signRequest.mode] +move = "Pindahkan Tanda Tangan" +place = "Tempatkan Tanda Tangan" +title = "Mode tanda tangan atau pindah" + +[certSign.collab.signRequest.modeTabs] +draw = "Gambar" +image = "Unggah" +text = "Ketik" + +[certSign.collab.signRequest.placeSignature] +message = "Klik pada PDF untuk menempatkan tanda tangan Anda" +title = "Tempatkan Tanda Tangan" + +[certSign.collab.signRequest.preview] +imageAlt = "Tanda tangan terpilih" +missing = "Tidak ada pratinjau" +textFallback = "Tanda tangan" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Tanda tangan hasil gambar" +defaultImageLabel = "Tanda tangan terunggah" +defaultLabel = "Tanda tangan" +defaultTextLabel = "Tanda tangan ketik" +delete = "Hapus tanda tangan" +none = "Tidak ada tanda tangan tersimpan" + +[certSign.collab.signRequest.signatureType] +draw = "Gambar" +type = "Ketik" +upload = "Unggah" + +[certSign.collab.signRequest.steps] +back = "Kembali" +cancelPlacement = "Batalkan Penempatan" +certificate = "Sertifikat" +clickMultipleTimes = "Klik pada PDF beberapa kali untuk menempatkan tanda tangan. Seret tanda tangan apa pun untuk memindahkan atau mengubah ukurannya." +clickToPlace = "Klik pada PDF di tempat Anda ingin tanda tangan muncul." +continue = "Lanjutkan ke Pemilihan Sertifikat" +continueToPlacement = "Lanjutkan ke Penempatan" +continueToReview = "Lanjutkan ke Tinjauan" +createSignature = "Buat Tanda Tangan" +invisible = "Tidak terlihat" +location = "Lokasi:" +multipleSignatures = "{{count}} tanda tangan akan diterapkan ke PDF" +oneSignature = "1 tanda tangan akan diterapkan ke PDF" +placeOnPdf = "Tempatkan pada PDF" +reason = "Alasan:" +reviewTitle = "Tinjau Sebelum Menandatangani" +signaturePlaced = "Tanda tangan ditempatkan pada halaman {{page}}. Anda dapat menyesuaikan posisinya dengan mengeklik lagi atau lanjut ke tinjauan." +visible = "Terlihat" +visibility = "Visibilitas:" +yourSignatures = "Tanda Tangan Anda ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Warna" +fontLabel = "Font" +fontSizeLabel = "Ukuran" +fontSizePlaceholder = "16" +label = "Teks Tanda Tangan" +modalHint = "Masukkan nama Anda, lalu klik Lanjutkan untuk menempatkannya pada PDF." +placeholder = "Masukkan nama Anda..." + +[certSign.collab.participant] +certValidating = "Memvalidasi sertifikat..." +certValid = "✓ Sertifikat valid" +certValidUntil = " hingga {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Sertifikat tidak valid" +certNetworkError = "Tidak dapat memvalidasi sertifikat" + +[certSign.collab.addParticipants] +add = "Tambahkan {{count}} Peserta" +back = "Kembali" +configureSignatures = "Konfigurasikan Pengaturan Tanda Tangan" +continue = "Lanjutkan ke Pengaturan Tanda Tangan" +reasonHelp = "Tetapkan alasan penandatanganan untuk peserta ini (opsional; mereka dapat menggantinya saat menandatangani)" +reasonPlaceholder = "mis. Persetujuan, Tinjauan..." +selectUsers = "Pilih Pengguna" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Sertakan Halaman Ringkasan Tanda Tangan" +includeSummaryPageHelp = "Sebuah halaman ringkasan akan ditambahkan di akhir dengan semua metadata tanda tangan. Kotak tanda tangan sertifikat digital pada halaman individual akan disembunyikan (tanda tangan basah tidak terpengaruh)." + +[certSign.collab.sessionList] +active = "Aktif" +finalized = "Difinalisasi" + +[certSign.collab.signatureSettings] +description = "Konfigurasikan bagaimana tanda tangan akan muncul untuk semua peserta" +title = "Tampilan Tanda Tangan" + +[certSign.collab.userSelector] +inviteUsers = "Tambah Pengguna" +loadError = "Gagal memuat pengguna" +noTeam = "Tidak Ada Tim" +noUsers = "Tidak ada pengguna lain yang ditemukan." +placeholder = "Pilih pengguna..." + +[certSign.mobile] +panelActions = "Tindakan" +panelDocument = "Dokumen" +panelPeople = "Orang" + +[certSign.sessions] +deleted = "Sesi dihapus" +fetchFailed = "Gagal memuat detail sesi" +finalized = "Sesi difinalisasi" +loaded = "PDF bertanda tangan dimuat" +pdfNotReady = "PDF Belum Siap" +pdfNotReadyDesc = "PDF bertanda tangan sedang dibuat. Silakan coba lagi sebentar." + +[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" + [changeCreds] changePassword = "Anda menggunakan kredensial login default. Silakan masukkan kata sandi baru" changeUsername = "Perbarui nama pengguna Anda. Anda akan keluar setelah memperbarui." @@ -3242,6 +3531,46 @@ totalSelected = "Total Dipilih" unsupported = "Tidak didukung" unzip = "Ekstrak" uploadError = "Gagal mengunggah beberapa file." +copyCreated = "Salinan disimpan ke perangkat ini." +copyFailed = "Tidak dapat membuat salinan." +leaveShare = "Hapus dari daftar saya" +leaveShareFailed = "Tidak dapat menghapus file bersama." +leaveShareSuccess = "Dihapus dari daftar berbagi Anda." +removeBoth = "Hapus dari keduanya" +removeFilePrompt = "File ini disimpan di perangkat ini dan di server Anda. Anda ingin menghapusnya dari mana?" +removeFileTitle = "Hapus file" +removeLocalOnly = "Hanya perangkat ini" +removeServerFailed = "Tidak dapat menghapus file dari server." +removeServerOnly = "Hanya server" +removeServerOnlyPrompt = "File ini hanya disimpan di server Anda. Ingin menghapusnya dari server?" +removeServerSuccess = "Dihapus dari server." +removeSharedPrompt = "File ini dibagikan kepada Anda. Anda dapat menghapusnya dari perangkat ini atau dari daftar berbagi Anda." +removeSharedServerOnlyBlockedPrompt = "File ini dibagikan kepada Anda dan hanya disimpan di server." +removeSharedServerOnlyPrompt = "File ini dibagikan kepada Anda dan hanya disimpan di server. Hapus dari daftar Anda?" +changesNotUploaded = "Perubahan tidak diunggah" +cloudFile = "File cloud" +filterAll = "Semua" +filterLocal = "Lokal" +filterSharedByMe = "Dibagikan oleh saya" +filterSharedWithMe = "Dibagikan kepada saya" +lastSynced = "Sinkronisasi terakhir" +localOnly = "Hanya lokal" +makeCopy = "Buat salinan" +owner = "Pemilik" +ownerUnknown = "Tidak diketahui" +share = "Bagikan" +shareSelected = "Bagikan yang Dipilih" +sharedByYou = "Dibagikan oleh Anda" +sharedEditNoticeBody = "Anda tidak memiliki hak edit untuk versi server dari file ini. Perubahan yang Anda lakukan akan disimpan sebagai salinan lokal." +sharedEditNoticeConfirm = "Mengerti" +sharedEditNoticeTitle = "Salinan server hanya-baca" +sharedWithYou = "Dibagikan kepada Anda" +sharing = "Berbagi" +storageState = "Penyimpanan" +synced = "Tersinkron" +updateOnServer = "Perbarui di Server" +uploadSelected = "Unggah yang Dipilih" +uploadToServer = "Unggah ke Server" [files] addFiles = "Tambahkan file" @@ -3367,6 +3696,77 @@ title = "Tentang Meratakan PDF" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Tentang Penandatanganan Grup" + +[groupSigning.tooltip.finalization] +bullet1 = "Semua tanda tangan diterapkan sesuai urutan peserta yang Anda tentukan" +bullet2 = "Anda dapat memfinalisasi dengan tanda tangan sebagian bila diperlukan" +bullet3 = "Setelah difinalisasi, sesi tidak dapat diubah" +description = "Setelah semua peserta menandatangani (atau Anda memilih memfinalisasi lebih awal), Anda dapat membuat PDF final yang ditandatangani." +title = "Proses Finalisasi" + +[groupSigning.tooltip.roles] +bullet1 = "Pemilik (Anda): Membuat sesi, mengonfigurasi default tanda tangan, memfinalisasi dokumen" +bullet2 = "Peserta: Membuat tanda tangan, memilih sertifikat, menempatkannya pada PDF" +bullet3 = "Peserta tidak dapat mengubah pengaturan visibilitas, alasan, atau lokasi tanda tangan" +description = "Anda mengontrol pengaturan tampilan tanda tangan untuk semua peserta." +title = "Peran Peserta" + +[groupSigning.tooltip.sequential] +bullet1 = "Peserta pertama harus menandatangani sebelum peserta kedua dapat mengakses dokumen" +bullet2 = "Memastikan urutan penandatanganan yang tepat untuk kepatuhan hukum" +bullet3 = "Anda dapat mengubah urutan peserta dengan menyeret mereka dalam daftar" +description = "Peserta menandatangani dokumen sesuai urutan yang Anda tentukan. Setiap penanda tangan menerima pemberitahuan saat gilirannya." +title = "Penandatanganan Berurutan" + +[groupSigning.steps] +back = "Kembali" +completed = "Selesai" +current = "Saat ini" +stepLabel = "Langkah {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Lanjutkan ke Tinjauan" +invisible = "Tanda tangan akan tidak terlihat (hanya metadata)" +locationLabel = "Lokasi:" +preview = "Pratinjau" +reasonLabel = "Alasan:" +title = "Konfigurasikan Pengaturan Tanda Tangan" +visible = "Tanda tangan akan terlihat pada halaman {{page}}" + +[groupSigning.steps.review] +document = "Dokumen" +dueDate = "Tanggal Jatuh Tempo (Opsional)" +dueDatePlaceholder = "Pilih tanggal jatuh tempo..." +invisible = "Tidak terlihat (hanya metadata)" +location = "Lokasi:" +logo = "Logo:" +logoHidden = "Tanpa logo" +logoShown = "Logo Stirling PDF ditampilkan" +participants = "Peserta" +reason = "Alasan:" +send = "Kirim Permintaan Penandatanganan" +signatureSettings = "Pengaturan Tanda Tangan" +title = "Tinjau Detail Sesi" +titleShort = "Tinjau & Kirim" +visibility = "Visibilitas:" +visible = "Terlihat pada halaman {{page}}" +participantCount = "{{count}} peserta akan menandatangani secara berurutan" + +[groupSigning.steps.selectDocument] +continue = "Lanjutkan ke Pemilihan Peserta" +noFile = "Pilih satu file PDF dari file aktif Anda untuk membuat sesi penandatanganan." +selectedFile = "Dokumen terpilih" +title = "Pilih Dokumen" + +[groupSigning.steps.selectParticipants] +continue = "Lanjutkan ke Pengaturan Tanda Tangan" +count = "{{count}} peserta dipilih" +label = "Pilih peserta" +placeholder = "Pilih peserta untuk menandatangani..." +title = "Pilih Peserta" + [getPdfInfo] downloadJson = "Unduh JSON" downloads = "Unduhan" @@ -4460,7 +4860,10 @@ zoomOut = "Perkecil" [viewer] cannotPreviewFile = "Tidak dapat menampilkan pratinjau file" +disableColorFilter = "Nonaktifkan Filter Warna" dualPageView = "Tampilan Dua Halaman" +enableDarkFilter = "Aktifkan Filter Gelap" +enableSepiaFilter = "Aktifkan Filter Sepia" firstPage = "Halaman Pertama" lastPage = "Halaman Terakhir" nextPage = "Halaman Berikutnya" @@ -4470,6 +4873,22 @@ singlePageView = "Tampilan Satu Halaman" unknownFile = "File tidak dikenal" zoomIn = "Perbesar" zoomOut = "Perkecil" +resetZoom = "Setel ulang zoom" + +[viewer.nonPdf] +fileTypeBadge = "File {{type}}" +convertToPdf = "Konversi ke PDF" +loading = "Memuat..." +emptyFile = "File kosong" +csvStats = "{{rows}} baris · {{columns}} kolom · {{size}}" +sortedBy = "Diurutkan menurut: {{column}}" +columnDefault = "Kolom {{index}}" +htmlPreviewWarning = "Pratinjau HTML — sumber eksternal mungkin tidak dimuat · {{size}}" +htmlPreview = "Pratinjau HTML" +invalidJson = "JSON tidak valid — menampilkan konten mentah" +textStats = "{{lines}} baris · {{size}}" +lineNumbers = "Nomor baris" +renderMarkdown = "Render markdown" [viewer.attachments] title = "Lampiran" @@ -4531,6 +4950,7 @@ toggleAttachments = "Tampilkan/Sembunyikan Lampiran" toggleTheme = "Alihkan Tema" language = "Bahasa" toggleAnnotations = "Alihkan Visibilitas Anotasi" +toggleLayers = "Alihkan Lapisan" search = "Cari PDF" panMode = "Mode Geser" applyRedactionsFirst = "Terapkan redaksi terlebih dahulu" @@ -5407,20 +5827,72 @@ title = "Cetak File" 2 = "Masukkan Nama Printer" [quickAccess] +access = "Akses" +accessAddPerson = "Tambah orang lain" +accessBack = "Kembali" +accessCopyLink = "Salin tautan" +accessEmail = "Alamat Email" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "File" +accessGeneral = "Akses Umum" +accessInviteTitle = "Undang Orang" +accessOwner = "Pemilik" +accessPanel = "Akses dokumen" +accessPeople = "Orang yang memiliki akses" +accessRemove = "Hapus" +accessRestricted = "Dibatasi" +accessRestrictedHint = "Hanya orang yang memiliki akses yang dapat membuka" +accessRole = "Peran" +accessRoleCommenter = "Komentator" +accessRoleEditor = "Editor" +accessRoleViewer = "Penampil" +accessSelectedFile = "File terpilih" +accessSendInvite = "Kirim Undangan" +accessTitle = "Akses Dokumen" +accessYou = "Anda" account = "Akun" +activeSessions = "Sesi Aktif" +activeTab = "Aktif" activity = "Kegiatan" adminSettings = "Setelan Admin" +allSessions = "Semua Sesi" allTools = "All Tools" automate = "Otomasi" +back = "Kembali" +certSign = "Tanda Tangan Sertifikat" +completedSessions = "Sesi Selesai" +completedTab = "Selesai" config = "Konfig" +createNew = "Buat Permintaan Baru" +createSession = "Buat Permintaan Penandatanganan" +dueDate = "Tanggal jatuh tempo (opsional)" files = "File" help = "Bantuan" +noActiveSessions = "Tidak ada permintaan tanda tangan tertunda atau sesi aktif" +noCompletedSessions = "Tidak ada sesi selesai" +noFile = "Tidak ada file yang dipilih" read = "Baca" reader = "Pembaca" +refresh = "Segarkan" +requestSignatures = "Minta Tanda Tangan" +selectSingleFileToRequest = "Pilih satu file PDF untuk meminta tanda tangan" +selectedFile = "File terpilih" +selectUsers = "Pilih pengguna untuk menandatangani" +selectUsersPlaceholder = "Pilih peserta..." +sendingRequest = "Mengirim..." settings = "Setelan" showMeAround = "Ajak saya berkeliling" sign = "Tanda Tangan" +signatureRequests = "Permintaan Tanda Tangan" +signYourself = "Tanda Tangani Sendiri" +newRequest = "Permintaan Baru" tours = "Tur" +wetSign = "Tambah Tanda Tangan" +filterMine = "Milik saya" +filterOverdue = "Terlambat" +filterSigned = "Ditandatangani" +filterDeclined = "Ditolak" +searchDocuments = "Cari dokumen…" [quickAccess.helpMenu] adminTour = "Tur Admin" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Server Stirling-PDF Anda sedang offline dan \"{{endpo expired = "Sesi Anda telah kedaluwarsa. Silakan muat ulang halaman dan coba lagi." refreshPage = "Muat Ulang Halaman" +[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" + +[signatureSettings.tooltip] +header = "Pengaturan Tampilan Tanda Tangan" + +[signatureSettings.tooltip.location] +bullet1 = "Contoh: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Bukan sama dengan posisi pada halaman" +bullet3 = "Mungkin diperlukan untuk yurisdiksi hukum tertentu" +description = "Lokasi geografis opsional tempat tanda tangan diterapkan. Disimpan dalam metadata sertifikat." +title = "Lokasi Tanda Tangan" + +[signatureSettings.tooltip.logo] +bullet1 = "Ditampilkan bersama tanda tangan dan teks" +bullet2 = "Mendukung format PNG, JPG" +bullet3 = "Meningkatkan tampilan profesional" +description = "Tambahkan logo perusahaan ke tanda tangan yang terlihat untuk branding dan keaslian." +title = "Logo Perusahaan" + +[signatureSettings.tooltip.reason] +bullet1 = "Contoh: \"Persetujuan\", \"Perjanjian Kontrak\", \"Tinjauan Selesai\"" +bullet2 = "Terlihat di properti tanda tangan PDF" +bullet3 = "Berguna untuk jejak audit dan kepatuhan" +description = "Teks opsional yang menjelaskan mengapa dokumen ditandatangani. Disimpan dalam metadata sertifikat." +title = "Alasan Tanda Tangan" + +[signatureSettings.tooltip.visibility] +bullet1 = "Terlihat: Tanda tangan muncul pada PDF dengan tampilan kustom" +bullet2 = "Tidak terlihat: Sertifikat disematkan tanpa tanda visual" +bullet3 = "Tanda tangan tidak terlihat tetap memberikan validasi kriptografis" +description = "Mengontrol apakah tanda tangan terlihat pada dokumen atau disematkan secara tidak terlihat." +title = "Visibilitas Tanda Tangan" + [settings.configuration] advanced = "Lanjutan" database = "Database" endpoints = "Endpoint" features = "Fitur" +storageSharing = "Penyimpanan File & Berbagi" systemSettings = "Pengaturan Sistem" title = "Konfigurasi" @@ -6332,10 +6868,13 @@ title = "Masuk ke Stirling" [setup.selfhosted] link = "atau hubungkan ke akun self-hosted" subtitle = "Masukkan kredensial server Anda" +changeServerLocked = "Organisasi Anda membatasi aplikasi ini ke server tertentu" switchToLocal = "Gunakan alat lokal saja" title = "Masuk ke Server" [setup.selfhosted.unreachable] +changeServer = "Hubungkan ke server lain" +changeServerLocked = "Organisasi Anda membatasi aplikasi ini ke server tertentu" continueOffline = "Gunakan alat lokal saja" message = "Tidak dapat menjangkau {{url}}. Periksa apakah server sedang berjalan dan dapat diakses." retry = "Coba lagi" @@ -6529,6 +7068,15 @@ saved = "Tersimpan" text = "Teks" title = "Jenis Tanda Tangan" +[signRequest] +declined = "Permintaan tanda tangan ditolak" +fetchFailed = "Gagal memuat permintaan tanda tangan" +signed = "Dokumen berhasil ditandatangani" + +[signSession] +createFailed = "Gagal membuat permintaan penandatanganan" +created = "Permintaan penandatanganan dikirim" + [signup] accountCreatedSuccessfully = "Akun berhasil dibuat! Anda sekarang dapat masuk." alreadyHaveAccount = "Sudah punya akun? Masuk" @@ -6807,6 +7355,106 @@ title = "Pecah PDF berdasarkan Bab" [splitPdfByChapters] tags = "pemisahan,bab,bookmark,atur" +[storageShare] +accessed = "Diakses" +accessDenied = "Anda tidak memiliki akses ke file bersama ini. Minta pemilik untuk membagikannya kepada Anda." +accessFailed = "Tidak dapat memuat aktivitas." +accessDeniedBody = "Anda tidak memiliki akses ke file ini. Minta pemilik untuk membagikannya kepada Anda." +accessDeniedTitle = "Tidak ada akses" +accessLimitedCommenter = "Akses komentar segera hadir. Minta akses editor kepada pemilik jika Anda perlu mengunduh." +accessLimitedTitle = "Akses terbatas" +accessLimitedViewer = "Tautan ini hanya untuk melihat. Minta akses editor kepada pemilik jika Anda perlu mengunduh." +createdAt = "Dibuat" +download = "Unduh" +downloadFailed = "Tidak dapat mengunduh file ini." +expiredBody = "Tautan berbagi ini tidak valid atau telah kedaluwarsa." +expiredTitle = "Tautan kedaluwarsa" +goToLogin = "Buka login" +loadFailed = "Tidak dapat membuka file bersama." +loading = "Memuat tautan berbagi..." +loginPrompt = "Masuk untuk mengakses file bersama ini." +loginRequired = "Login diperlukan" +openInApp = "Buka di Stirling PDF" +ownerLabel = "Pemilik" +ownerUnknown = "Tidak diketahui" +requiresLogin = "File bersama ini memerlukan login." +roleCommenter = "Komentator" +roleEditor = "Editor" +roleViewer = "Penampil" +shareHeading = "File bersama" +titleDefault = "File bersama" +tryAgain = "Silakan coba lagi nanti." +addUser = "Tambah" +commenterHint = "Fitur komentar segera hadir." +copied = "Tautan disalin ke papan klip" +copy = "Salin" +copyFailed = "Gagal menyalin" +description = "Buat tautan berbagi untuk file ini. Pengguna yang masuk dengan tautan tersebut dapat mengaksesnya." +downloadsCount = "Unduhan: {{count}}" +emailWarningBody = "Ini terlihat seperti alamat email. Jika orang ini belum menjadi pengguna Stirling PDF, mereka tidak akan dapat mengakses file." +emailWarningConfirm = "Tetap bagikan" +emailWarningTitle = "Alamat email" +errorTitle = "Gagal berbagi" +failure = "Tidak dapat membuat tautan berbagi. Silakan coba lagi." +fileLabel = "File" +generate = "Buat Tautan" +generated = "Tautan berbagi dibuat" +hideActivity = "Sembunyikan aktivitas" +invalidUsername = "Masukkan nama pengguna atau alamat email yang valid." +lastAccessed = "Terakhir diakses" +linkAccessTitle = "Akses tautan berbagi" +linkLabel = "Tautan berbagi" +linksDisabled = "Tautan berbagi dinonaktifkan." +linksDisabledBody = "Tautan berbagi dinonaktifkan oleh pengaturan server Anda." +manage = "Kelola berbagi" +manageDescription = "Buat dan kelola tautan untuk berbagi file ini." +manageLoadFailed = "Tidak dapat memuat tautan berbagi." +manageTitle = "Kelola Berbagi" +noActivity = "Belum ada aktivitas." +noLinks = "Belum ada tautan berbagi aktif." +noSharedUsers = "Belum ada pengguna yang memiliki akses." +removeLink = "Hapus tautan" +removeUser = "Hapus" +revokeFailed = "Tidak dapat menghapus tautan berbagi." +revoked = "Tautan berbagi dihapus" +roleLabel = "Peran" +sharingDisabled = "Berbagi dinonaktifkan." +sharingDisabledBody = "Berbagi telah dinonaktifkan oleh pengaturan server Anda." +sharedUsersTitle = "Pengguna dengan akses" +title = "Bagikan File" +unknownUser = "Pengguna tidak dikenal" +userAddFailed = "Tidak dapat berbagi dengan pengguna tersebut." +userAdded = "Pengguna ditambahkan ke daftar berbagi." +usernameLabel = "Nama pengguna atau email" +usernamePlaceholder = "Masukkan nama pengguna atau email" +userRemoveFailed = "Tidak dapat menghapus pengguna tersebut." +userRemoved = "Pengguna dihapus dari daftar berbagi." +viewActivity = "Lihat aktivitas" +viewed = "Dilihat" +viewsCount = "Tayangan: {{count}}" +downloaded = "Diunduh" +bulkDescription = "Buat satu tautan untuk membagikan semua file yang dipilih kepada pengguna yang telah masuk." +bulkTitle = "Bagikan file yang dipilih" +copyLink = "Salin tautan berbagi" +fileCount = "{{count}} file dipilih" +ownerOnly = "Hanya pemilik yang dapat mengelola berbagi." +selectSingleFile = "Pilih satu file untuk mengelola berbagi." + +[storageUpload] +description = "Ini mengunggah file saat ini ke penyimpanan server untuk akses Anda sendiri." +errorTitle = "Gagal mengunggah" +failure = "Gagal mengunggah. Harap periksa login dan pengaturan penyimpanan Anda." +fileLabel = "File" +hint = "Tautan publik dan mode akses dikendalikan oleh pengaturan server Anda." +success = "Diunggah ke server" +title = "Unggah ke Server" +updateButton = "Perbarui di Server" +uploadButton = "Unggah ke Server" +bulkDescription = "Ini mengunggah file yang dipilih ke penyimpanan server Anda." +bulkTitle = "Unggah file yang dipilih" +fileCount = "{{count}} file dipilih" +more = " +{{count}} lainnya" + [storage] approximateSize = "Perkiraan ukuran" fileTooLarge = "File terlalu besar. Ukuran maksimum per file adalah" @@ -7153,6 +7801,30 @@ title = "Lihat/Edit PDF" [warning] tooltipTitle = "Peringatan" +[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" + [watermark] completed = "Tanda air ditambahkan" desc = "Tambahkan tanda air teks atau gambar ke file PDF" @@ -7333,6 +8005,7 @@ activeSession = "Sesi aktif" addMembers = "Tambah Anggota" admin = "Admin" confirmDelete = "Anda yakin ingin menghapus pengguna ini? Tindakan ini tidak dapat dibatalkan." +confirmUnlock = "Anda yakin ingin membuka kunci akun pengguna ini?" deleteUser = "Hapus Pengguna" deleteUserError = "Gagal menghapus pengguna" deleteUserSuccess = "Pengguna berhasil dihapus" @@ -7341,6 +8014,8 @@ disable = "Nonaktifkan" disabled = "Dinonaktifkan" editRole = "Edit Peran" enable = "Aktifkan" +locked = "terkunci" +lockedBadge = "Terkunci" loading = "Memuat orang..." loginRequired = "Aktifkan mode login terlebih dahulu" member = "Anggota" @@ -7350,6 +8025,9 @@ searchMembers = "Cari anggota..." status = "Status" team = "Tim" title = "Orang" +unlockAccount = "Buka Kunci Akun" +unlockUserError = "Gagal membuka kunci akun pengguna" +unlockUserSuccess = "Akun pengguna berhasil dibuka kunci" user = "Pengguna" [workspace.people.actions] diff --git a/frontend/public/locales/it-IT/translation.toml b/frontend/public/locales/it-IT/translation.toml index 562e8597ca..b9b2d61f03 100644 --- a/frontend/public/locales/it-IT/translation.toml +++ b/frontend/public/locales/it-IT/translation.toml @@ -8,6 +8,7 @@ black = "Nero" blue = "Blu" bored = "Stanco di aspettare?" cancel = "Annulla" +confirm = "Conferma" changedCredsMessage = "Credenziali modificate!" chooseFile = "Scegli file" close = "Chiudi" @@ -146,6 +147,7 @@ insufficientCredits = "Crediti insufficienti. Richiesti: {{requiredCredits}}, Di loadingCredits = "Verifica dei crediti..." loadingProStatus = "Verifica dello stato dell'abbonamento..." noticeTopUpOrPlan = "Crediti insufficienti, ricarica o passa a un piano" +accessInvite = "Invita" [account] accountSettings = "Impostazioni Account" @@ -1427,6 +1429,34 @@ title = "Elaborazione" description = "Tempo massimo di attesa per un processo prima di segnalare un errore." label = "Timeout elaborazione (secondi)" +[admin.settings.storage] +description = "Controlla le opzioni di archiviazione e condivisione del server." +title = "Archiviazione file e condivisione" + +[admin.settings.storage.enabled] +description = "Consenti agli utenti di archiviare file sul server." +label = "Abilita archiviazione file sul server" + +[admin.settings.storage.sharing.email] +description = "Consenti la condivisione con indirizzi email." +label = "Abilita condivisione via email" +mailLink = "Configura impostazioni email" +mailNote = "Richiede la configurazione dell'email. " + +[admin.settings.storage.sharing.enabled] +description = "Consenti agli utenti di condividere i file archiviati." +label = "Abilita condivisione" + +[admin.settings.storage.sharing.links] +description = "Consenti la condivisione tramite link con accesso autenticato." +frontendUrlLink = "Configura nelle impostazioni di sistema" +frontendUrlNote = "Richiede un Frontend URL. " +label = "Abilita link di condivisione" + +[admin.settings.storage.signing.enabled] +description = "Consenti agli utenti di creare sessioni di firma del documento con più partecipanti. Richiede che l'archiviazione file sul server sia abilitata." +label = "Abilita firma di gruppo (Alpha)" + [admin.settings.unsavedChanges] cancel = "Continua a modificare" discard = "Scarta modifiche" @@ -2059,7 +2089,19 @@ numbers = "Numeri/intervalli: 5, 10-20" progressions = "Progressioni: 3n, 4n+1" [certSign] +allSigned = "Tutti i partecipanti hanno firmato. Pronto per la finalizzazione." +awaitingSignatures = "In attesa di firme" +signatureProgress = "{{signedCount}}/{{totalCount}} firme" chooseCertificate = "Scegli il file del certificato" +declined = "Rifiutato" +fetchFailed = "Impossibile caricare i dati di firma" +finalized = "Finalizzato" +notified = "In sospeso" +partialNote = "Puoi finalizzare in anticipo con le firme attuali. I partecipanti senza firma saranno esclusi." +pending = "In sospeso" +readyToFinalize = "Pronto per la finalizzazione" +signed = "Firmato" +viewed = "Visualizzato" chooseJksFile = "Scegli il file JKS" chooseP12File = "Scegli il file PKCS12" choosePfxFile = "Scegli il file PFX" @@ -2082,6 +2124,7 @@ title = "Firma del certificato" invisible = "Invisibile" stepTitle = "Aspetto firma" visible = "Visibile" +visibility = "Visibilità" [certSign.appearance.options] title = "Dettagli firma" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Finalizza e carica PDF firmato" +early = "Finalizza con le firme attuali" + +[certSign.collab.sessionDetail] +addButton = "Aggiungi partecipanti" +addParticipants = "Aggiungi partecipanti" +addParticipantsError = "Impossibile aggiungere i partecipanti" +backToList = "Torna alle sessioni" +deleteConfirm = "Sei sicuro? Questa operazione non può essere annullata." +deleteError = "Impossibile eliminare la sessione" +deleted = "Sessione eliminata" +deleteSession = "Elimina sessione" +dueDate = "Scadenza" +finalizeError = "Impossibile finalizzare la sessione" +loadPdfError = "Impossibile caricare il PDF firmato" +loadSignedPdf = "Carica PDF firmato nei file attivi" +messageLabel = "Messaggio" +noAdditionalInfo = "Nessuna informazione aggiuntiva" +owner = "Proprietario" +participantRemoved = "Partecipante rimosso" +participants = "Partecipanti" +participantsAdded = "Partecipanti aggiunti con successo" +removeParticipant = "Rimuovi" +removeParticipantError = "Impossibile rimuovere il partecipante" +selectUsers = "Seleziona utenti..." +sessionInfo = "Informazioni sulla sessione" +workbenchTitle = "Gestione sessione" + +[certSign.collab.signRequest] +addedToFiles = "Documento aggiunto ai file attivi" +addSignature = "Aggiungi la tua firma" +addToFiles = "Aggiungi ai file attivi" +advancedSettings = "Impostazioni avanzate" +backToList = "Torna alle richieste di firma" +certificateChoice = "Seleziona un certificato con cui firmare" +changeSignature = "Cambia firma" +clearSignature = "Cancella firma" +completeAndSign = "Completa e firma" +createNewSignature = "Crea nuova firma" +declineButton = "Rifiuta" +decline = "Rifiuta richiesta" +deleteSelected = "Elimina firma selezionata" +drawSignature = "Disegna la tua firma qui sotto" +dueDate = "Scadenza" +fileTooLarge = "La dimensione del file deve essere inferiore a 5 MB" +fontFamily = "Famiglia di caratteri" +fontSize = "Dimensione carattere: {{size}}px" +fontSizePlaceholder = "Dimensione" +from = "Da" +invalidCertFile = "Seleziona un file di certificato P12 o PFX" +invalidFileType = "Seleziona un file immagine" +location = "Località (opzionale)" +locationPlaceholder = "Da dove stai firmando?" +message = "Messaggio" +noCertificate = "Seleziona un file di certificato" +noSignatures = "Posiziona almeno una firma sul PDF" +p12File = "File di certificato P12/PFX" +password = "Password del certificato" +passwordPlaceholder = "Inserisci password..." +penColor = "Colore penna" +penSize = "Dimensione penna: {{size}}px" +placementActive = "Clicca sul PDF per posizionare" +placeSignatureButton = "Posiziona firma sul PDF" +reason = "Motivo (opzionale)" +reasonPlaceholder = "Perché stai firmando?" +removeImage = "Rimuovi immagine" +removeCertFile = "Rimuovi file" +savedSignatures = "Firme salvate" +selectFile = "Seleziona file immagine" +selectSignatureTitle = "Seleziona o crea firma" +signButton = "Firma documento" +signatureInfo = "Queste impostazioni sono configurate dal proprietario del documento" +signaturePlaced = "Firma posizionata sulla pagina" +signatureSettings = "Impostazioni firma" +signatureText = "Testo firma" +signatureTextPlaceholder = "Inserisci il tuo nome..." +signatureTypeLabel = "Tipo di firma" +signingTitle = "Firma" +textColor = "Colore testo" +typeSignature = "Digita il tuo nome per creare una firma" +uploadCert = "Certificato personalizzato" +uploadCertDesc = "Usa il tuo certificato P12/PFX" +uploadSignature = "Carica l'immagine della tua firma" +usePersonalCert = "Certificato personale" +usePersonalCertDesc = "Generato automaticamente per il tuo account" +useServerCert = "Certificato dell'organizzazione" +useServerCertDesc = "Certificato condiviso dell'organizzazione" +workbenchTitle = "Richiesta di firma" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Scegli il colore del tratto" +continue = "Continua" + +[certSign.collab.signRequest.certModal] +description = "Hai posizionato {{count}} firma/e. Scegli il tuo certificato per completare la firma." +sign = "Firma documento" +certValidating = "Verifica del certificato..." +certValidUntil = "Certificato valido fino al {{date}}" +certInvalid = "Certificato non valido: {{error}}" +certInvalidFallback = "Certificato non valido" +certNetworkError = "Impossibile verificare il certificato" +title = "Configura certificato" + +[certSign.collab.signRequest.image] +hint = "Carica un'immagine PNG o JPG della tua firma" + +[certSign.collab.signRequest.mode] +move = "Sposta firma" +place = "Posiziona firma" +title = "Modalità firma o spostamento" + +[certSign.collab.signRequest.modeTabs] +draw = "Disegna" +image = "Carica" +text = "Digita" + +[certSign.collab.signRequest.placeSignature] +message = "Clicca sul PDF per posizionare la tua firma" +title = "Posiziona firma" + +[certSign.collab.signRequest.preview] +imageAlt = "Firma selezionata" +missing = "Nessuna anteprima" +textFallback = "Firma" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Firma disegnata" +defaultImageLabel = "Firma caricata" +defaultLabel = "Firma" +defaultTextLabel = "Firma digitata" +delete = "Elimina firma" +none = "Nessuna firma salvata" + +[certSign.collab.signRequest.signatureType] +draw = "Disegna" +type = "Digita" +upload = "Carica" + +[certSign.collab.signRequest.steps] +back = "Indietro" +cancelPlacement = "Annulla posizionamento" +certificate = "Certificato" +clickMultipleTimes = "Clicca più volte sul PDF per posizionare le firme. Trascina qualsiasi firma per spostarla o ridimensionarla." +clickToPlace = "Clicca sul PDF dove desideri che appaia la tua firma." +continue = "Continua alla selezione del certificato" +continueToPlacement = "Continua al posizionamento" +continueToReview = "Continua alla revisione" +createSignature = "Crea firma" +invisible = "Invisibile" +location = "Località:" +multipleSignatures = "{{count}} firme verranno applicate al PDF" +oneSignature = "1 firma verrà applicata al PDF" +placeOnPdf = "Posiziona sul PDF" +reason = "Motivo:" +reviewTitle = "Rivedi prima di firmare" +signaturePlaced = "Firma posizionata sulla pagina {{page}}. Puoi regolare la posizione cliccando di nuovo oppure continuare alla revisione." +visible = "Visibile" +visibility = "Visibilità:" +yourSignatures = "Le tue firme ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Colore" +fontLabel = "Carattere" +fontSizeLabel = "Dimensione" +fontSizePlaceholder = "16" +label = "Testo firma" +modalHint = "Inserisci il tuo nome, quindi fai clic su Continua per posizionarlo sul PDF." +placeholder = "Inserisci il tuo nome..." + +[certSign.collab.participant] +certValidating = "Verifica del certificato..." +certValid = "✓ Certificato valido" +certValidUntil = " fino al {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Certificato non valido" +certNetworkError = "Impossibile verificare il certificato" + +[certSign.collab.addParticipants] +add = "Aggiungi {{count}} partecipante/i" +back = "Indietro" +configureSignatures = "Configura impostazioni firma" +continue = "Continua alle impostazioni firma" +reasonHelp = "Preimposta un motivo di firma per questi partecipanti (opzionale, possono modificarlo al momento della firma)" +reasonPlaceholder = "es. Approvazione, Revisione..." +selectUsers = "Seleziona utenti" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Includi pagina di riepilogo firme" +includeSummaryPageHelp = "Alla fine verrà aggiunta una pagina di riepilogo con tutti i metadati delle firme. Le caselle di firma del certificato digitale sulle singole pagine saranno nascoste (le firme autografe non sono interessate)." + +[certSign.collab.sessionList] +active = "Attiva" +finalized = "Finalizzata" + +[certSign.collab.signatureSettings] +description = "Configura l'aspetto delle firme per tutti i partecipanti" +title = "Aspetto firma" + +[certSign.collab.userSelector] +inviteUsers = "Aggiungi utenti" +loadError = "Impossibile caricare gli utenti" +noTeam = "Nessun team" +noUsers = "Nessun altro utente trovato." +placeholder = "Seleziona utenti..." + +[certSign.mobile] +panelActions = "Azioni" +panelDocument = "Documento" +panelPeople = "Persone" + +[certSign.sessions] +deleted = "Sessione eliminata" +fetchFailed = "Impossibile caricare i dettagli della sessione" +finalized = "Sessione finalizzata" +loaded = "PDF firmato caricato" +pdfNotReady = "PDF non pronto" +pdfNotReadyDesc = "Il PDF firmato è in fase di generazione. Riprova tra un momento." + +[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" + [changeCreds] changePassword = "Stai utilizzando le credenziali di accesso predefinite. Inserisci una nuova password" changeUsername = "Aggiorna il nome utente. Verrai disconnesso dopo l'aggiornamento." @@ -3242,6 +3531,46 @@ totalSelected = "Totale selezionati" unsupported = "Non supportato" unzip = "Estrai" uploadError = "Caricamento di alcuni file non riuscito." +copyCreated = "Copia salvata su questo dispositivo." +copyFailed = "Impossibile creare una copia." +leaveShare = "Rimuovi dal mio elenco" +leaveShareFailed = "Impossibile rimuovere il file condiviso." +leaveShareSuccess = "Rimosso dal tuo elenco Condivisi." +removeBoth = "Rimuovi da entrambi" +removeFilePrompt = "Questo file è salvato su questo dispositivo e sul tuo server. Da dove desideri rimuoverlo?" +removeFileTitle = "Rimuovi file" +removeLocalOnly = "Solo da questo dispositivo" +removeServerFailed = "Impossibile rimuovere il file dal server." +removeServerOnly = "Solo dal server" +removeServerOnlyPrompt = "Questo file è archiviato solo sul tuo server. Vuoi rimuoverlo dal server?" +removeServerSuccess = "Rimosso dal server." +removeSharedPrompt = "Questo file è condiviso con te. Puoi rimuoverlo da questo dispositivo o dal tuo elenco Condivisi." +removeSharedServerOnlyBlockedPrompt = "Questo file è condiviso con te ed è archiviato solo sul server." +removeSharedServerOnlyPrompt = "Questo file è condiviso con te ed è archiviato solo sul server. Rimuoverlo dal tuo elenco?" +changesNotUploaded = "Modifiche non caricate" +cloudFile = "File cloud" +filterAll = "Tutti" +filterLocal = "Locale" +filterSharedByMe = "Condivisi da me" +filterSharedWithMe = "Condivisi con me" +lastSynced = "Ultima sincronizzazione" +localOnly = "Solo locale" +makeCopy = "Crea una copia" +owner = "Proprietario" +ownerUnknown = "Sconosciuto" +share = "Condividi" +shareSelected = "Condividi selezionati" +sharedByYou = "Condivisi da te" +sharedEditNoticeBody = "Non hai diritti di modifica sulla versione sul server di questo file. Qualsiasi modifica verrà salvata come copia locale." +sharedEditNoticeConfirm = "Ho capito" +sharedEditNoticeTitle = "Copia sul server in sola lettura" +sharedWithYou = "Condivisi con te" +sharing = "Condivisione" +storageState = "Archiviazione" +synced = "Sincronizzato" +updateOnServer = "Aggiorna sul server" +uploadSelected = "Carica selezionati" +uploadToServer = "Carica sul server" [files] addFiles = "Aggiungi file" @@ -3367,6 +3696,77 @@ title = "Informazioni sull'appiattimento dei PDF" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Informazioni sulla firma di gruppo" + +[groupSigning.tooltip.finalization] +bullet1 = "Tutte le firme vengono applicate nell'ordine dei partecipanti che hai specificato" +bullet2 = "Puoi finalizzare con firme parziali se necessario" +bullet3 = "Una volta finalizzata, la sessione non può essere modificata" +description = "Una volta che tutti i partecipanti hanno firmato (o scegli di finalizzare prima), puoi generare il PDF finale firmato." +title = "Processo di finalizzazione" + +[groupSigning.tooltip.roles] +bullet1 = "Proprietario (tu): crea la sessione, configura i valori predefiniti della firma, finalizza il documento" +bullet2 = "Partecipanti: creano la propria firma, scelgono il certificato, la posizionano sul PDF" +bullet3 = "I partecipanti non possono modificare le impostazioni di visibilità, motivo o località della firma" +description = "Controlli le impostazioni dell'aspetto della firma per tutti i partecipanti." +title = "Ruoli dei partecipanti" + +[groupSigning.tooltip.sequential] +bullet1 = "Il primo partecipante deve firmare prima che il secondo possa accedere al documento" +bullet2 = "Garantisce il corretto ordine di firma per la conformità legale" +bullet3 = "Puoi riordinare i partecipanti trascinandoli nell'elenco" +description = "I partecipanti firmano i documenti nell'ordine che specifichi. Ogni firmatario riceve una notifica quando è il suo turno." +title = "Firma sequenziale" + +[groupSigning.steps] +back = "Indietro" +completed = "Completato" +current = "Corrente" +stepLabel = "Passaggio {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Continua alla revisione" +invisible = "Le firme saranno invisibili (solo metadati)" +locationLabel = "Località:" +preview = "Anteprima" +reasonLabel = "Motivo:" +title = "Configura impostazioni firma" +visible = "Le firme saranno visibili a pagina {{page}}" + +[groupSigning.steps.review] +document = "Documento" +dueDate = "Scadenza (opzionale)" +dueDatePlaceholder = "Seleziona una scadenza..." +invisible = "Invisibile (solo metadati)" +location = "Località:" +logo = "Logo:" +logoHidden = "Nessun logo" +logoShown = "Logo Stirling PDF visualizzato" +participants = "Partecipanti" +reason = "Motivo:" +send = "Invia richieste di firma" +signatureSettings = "Impostazioni firma" +title = "Rivedi i dettagli della sessione" +titleShort = "Rivedi e invia" +visibility = "Visibilità:" +visible = "Visibile a pagina {{page}}" +participantCount = "{{count}} partecipanti firmeranno in ordine" + +[groupSigning.steps.selectDocument] +continue = "Continua alla selezione dei partecipanti" +noFile = "Seleziona un singolo file PDF dai tuoi file attivi per creare una sessione di firma." +selectedFile = "Documento selezionato" +title = "Seleziona documento" + +[groupSigning.steps.selectParticipants] +continue = "Continua alle impostazioni firma" +count = "{{count}} partecipante/i selezionato/i" +label = "Seleziona partecipanti" +placeholder = "Scegli i partecipanti che devono firmare..." +title = "Scegli partecipanti" + [getPdfInfo] downloadJson = "Scarica JSON" downloads = "Download" @@ -4460,7 +4860,10 @@ zoomOut = "Riduci" [viewer] cannotPreviewFile = "Impossibile visualizzare l'anteprima del file" +disableColorFilter = "Disabilita filtro colore" dualPageView = "Vista doppia pagina" +enableDarkFilter = "Abilita filtro scuro" +enableSepiaFilter = "Abilita filtro seppia" firstPage = "Prima pagina" lastPage = "Ultima pagina" nextPage = "Pagina successiva" @@ -4470,6 +4873,22 @@ singlePageView = "Vista pagina singola" unknownFile = "File sconosciuto" zoomIn = "Ingrandisci" zoomOut = "Riduci" +resetZoom = "Reimposta zoom" + +[viewer.nonPdf] +fileTypeBadge = "File {{type}}" +convertToPdf = "Converti in PDF" +loading = "Caricamento..." +emptyFile = "File vuoto" +csvStats = "{{rows}} righe · {{columns}} colonne · {{size}}" +sortedBy = "Ordinato per: {{column}}" +columnDefault = "Colonna {{index}}" +htmlPreviewWarning = "Anteprima HTML — le risorse esterne potrebbero non essere caricate · {{size}}" +htmlPreview = "Anteprima HTML" +invalidJson = "JSON non valido — mostra contenuto grezzo" +textStats = "{{lines}} righe · {{size}}" +lineNumbers = "Numeri di riga" +renderMarkdown = "Renderizza markdown" [viewer.attachments] title = "Allegati" @@ -4531,6 +4950,7 @@ toggleAttachments = "Mostra/Nascondi allegati" toggleTheme = "Cambia tema" language = "Lingua" toggleAnnotations = "Attiva/disattiva visibilità annotazioni" +toggleLayers = "Attiva/disattiva livelli" search = "Cerca nel PDF" panMode = "Modalità mano" applyRedactionsFirst = "Applica prima gli oscuramenti" @@ -5407,20 +5827,72 @@ title = "Stampa file" 2 = "Inserire il nome della stampante" [quickAccess] +access = "Accesso" +accessAddPerson = "Aggiungi un'altra persona" +accessBack = "Indietro" +accessCopyLink = "Copia link" +accessEmail = "Indirizzo email" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "File" +accessGeneral = "Accesso generale" +accessInviteTitle = "Invita persone" +accessOwner = "Proprietario" +accessPanel = "Accesso al documento" +accessPeople = "Persone con accesso" +accessRemove = "Rimuovi" +accessRestricted = "Limitato" +accessRestrictedHint = "Solo le persone con accesso possono aprirlo" +accessRole = "Ruolo" +accessRoleCommenter = "Commentatore" +accessRoleEditor = "Editor" +accessRoleViewer = "Visualizzatore" +accessSelectedFile = "File selezionato" +accessSendInvite = "Invia invito" +accessTitle = "Accesso al documento" +accessYou = "Tu" account = "Account" +activeSessions = "Sessioni attive" +activeTab = "Attive" activity = "Attività" adminSettings = "Opzioni Admin" +allSessions = "Tutte le sessioni" allTools = "Funzioni" automate = "Automaz." +back = "Indietro" +certSign = "Firma con certificato" +completedSessions = "Sessioni completate" +completedTab = "Completate" config = "Configurazione" +createNew = "Crea nuova richiesta" +createSession = "Crea richiesta di firma" +dueDate = "Scadenza (opzionale)" files = "File" help = "Guida" +noActiveSessions = "Nessuna richiesta di firma in sospeso o sessione attiva" +noCompletedSessions = "Nessuna sessione completata" +noFile = "Nessun file selezionato" read = "Leggi" reader = "Lettore" +refresh = "Aggiorna" +requestSignatures = "Richiedi firme" +selectSingleFileToRequest = "Seleziona un singolo file PDF per richiedere le firme" +selectedFile = "File selezionato" +selectUsers = "Seleziona gli utenti che devono firmare" +selectUsersPlaceholder = "Scegli i partecipanti..." +sendingRequest = "Invio..." settings = "Opzioni" showMeAround = "Fammi fare un giro" sign = "Firma" +signatureRequests = "Richieste di firma" +signYourself = "Firma tu stesso" +newRequest = "Nuova richiesta" tours = "Tour" +wetSign = "Aggiungi firma" +filterMine = "Miei" +filterOverdue = "In ritardo" +filterSigned = "Firmato" +filterDeclined = "Rifiutato" +searchDocuments = "Cerca documenti…" [quickAccess.helpMenu] adminTour = "Tour amministratore" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Il tuo server Stirling-PDF è offline e \"{{endpoint} expired = "La tua sessione è scaduta. Aggiorna la pagina e riprova." refreshPage = "Aggiorna pagina" +[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" + +[signatureSettings.tooltip] +header = "Impostazioni aspetto firma" + +[signatureSettings.tooltip.location] +bullet1 = "Esempi: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Non coincide con la posizione sulla pagina" +bullet3 = "Potrebbe essere richiesta in alcune giurisdizioni legali" +description = "Località geografica opzionale in cui è stata apposta la firma. Archiviata nei metadati del certificato." +title = "Località della firma" + +[signatureSettings.tooltip.logo] +bullet1 = "Mostrato accanto alla firma e al testo" +bullet2 = "Supporta i formati PNG, JPG" +bullet3 = "Migliora l'aspetto professionale" +description = "Aggiungi un logo aziendale alle firme visibili per branding e autenticità." +title = "Logo aziendale" + +[signatureSettings.tooltip.reason] +bullet1 = "Esempi: \"Approvazione\", \"Accordo contrattuale\", \"Revisione completata\"" +bullet2 = "Visibile nelle proprietà della firma del PDF" +bullet3 = "Utile per tracciabilità e conformità" +description = "Testo opzionale che spiega perché il documento viene firmato. Archiviato nei metadati del certificato." +title = "Motivo della firma" + +[signatureSettings.tooltip.visibility] +bullet1 = "Visibile: la firma appare sul PDF con aspetto personalizzato" +bullet2 = "Invisibile: certificato incorporato senza segno visivo" +bullet3 = "Le firme invisibili forniscono comunque una validazione crittografica" +description = "Controlla se la firma è visibile sul documento o incorporata in modo invisibile." +title = "Visibilità della firma" + [settings.configuration] advanced = "Avanzate" database = "Database" endpoints = "Endpoint" features = "Funzionalità" +storageSharing = "Archiviazione file e condivisione" systemSettings = "Impostazioni di sistema" title = "Configurazione" @@ -6332,10 +6868,13 @@ title = "Accedi a Stirling" [setup.selfhosted] link = "oppure connettiti a un account self-hosted" subtitle = "Inserisci le credenziali del server" +changeServerLocked = "La tua organizzazione ha limitato questa app a un server specifico" switchToLocal = "Usa invece gli strumenti locali" title = "Accedi al server" [setup.selfhosted.unreachable] +changeServer = "Connetti a un server diverso" +changeServerLocked = "La tua organizzazione ha limitato questa app a un server specifico" continueOffline = "Usa invece gli strumenti locali" message = "Impossibile raggiungere {{url}}. Verifica che il server sia in esecuzione e accessibile." retry = "Riprova" @@ -6529,6 +7068,15 @@ saved = "Salvate" text = "Testo" title = "Tipo di firma" +[signRequest] +declined = "Richiesta di firma rifiutata" +fetchFailed = "Impossibile caricare la richiesta di firma" +signed = "Documento firmato con successo" + +[signSession] +createFailed = "Impossibile creare la richiesta di firma" +created = "Richiesta di firma inviata" + [signup] accountCreatedSuccessfully = "Account creato con successo! Ora puoi accedere." alreadyHaveAccount = "Hai già un account? Accedi" @@ -6807,6 +7355,106 @@ title = "Dividere PDF per capitoli" [splitPdfByChapters] tags = "dividi,capitoli,segnalibri,organizza" +[storageShare] +accessed = "Accessato" +accessDenied = "Non hai accesso a questo file condiviso. Chiedi al proprietario di condividerlo con te." +accessFailed = "Impossibile caricare l'attività." +accessDeniedBody = "Non hai accesso a questo file. Chiedi al proprietario di condividerlo con te." +accessDeniedTitle = "Nessun accesso" +accessLimitedCommenter = "L'accesso come commentatore arriverà presto. Chiedi al proprietario l'accesso come editor se devi scaricare." +accessLimitedTitle = "Accesso limitato" +accessLimitedViewer = "Questo link è solo per la visualizzazione. Chiedi al proprietario l'accesso come editor se devi scaricare." +createdAt = "Creato" +download = "Scarica" +downloadFailed = "Impossibile scaricare questo file." +expiredBody = "Questo link di condivisione non è valido o è scaduto." +expiredTitle = "Link scaduto" +goToLogin = "Vai al login" +loadFailed = "Impossibile aprire il file condiviso." +loading = "Caricamento link di condivisione..." +loginPrompt = "Accedi per accedere a questo file condiviso." +loginRequired = "Accesso richiesto" +openInApp = "Apri in Stirling PDF" +ownerLabel = "Proprietario" +ownerUnknown = "Sconosciuto" +requiresLogin = "Questo file condiviso richiede l'accesso." +roleCommenter = "Commentatore" +roleEditor = "Editor" +roleViewer = "Visualizzatore" +shareHeading = "File condiviso" +titleDefault = "File condiviso" +tryAgain = "Riprova più tardi." +addUser = "Aggiungi" +commenterHint = "I commenti arriveranno presto." +copied = "Link copiato negli appunti" +copy = "Copia" +copyFailed = "Copia non riuscita" +description = "Crea un link di condivisione per questo file. Gli utenti autenticati con il link possono accedervi." +downloadsCount = "Download: {{count}}" +emailWarningBody = "Sembra un indirizzo email. Se questa persona non è già un utente di Stirling PDF, non potrà accedere al file." +emailWarningConfirm = "Condividi comunque" +emailWarningTitle = "Indirizzo email" +errorTitle = "Condivisione non riuscita" +failure = "Impossibile generare un link di condivisione. Riprova." +fileLabel = "File" +generate = "Genera link" +generated = "Link di condivisione generato" +hideActivity = "Nascondi attività" +invalidUsername = "Inserisci un nome utente o un indirizzo email valido." +lastAccessed = "Ultimo accesso" +linkAccessTitle = "Accesso tramite link di condivisione" +linkLabel = "Link di condivisione" +linksDisabled = "I link di condivisione sono disabilitati." +linksDisabledBody = "I link di condivisione sono disabilitati dalle impostazioni del tuo server." +manage = "Gestisci condivisione" +manageDescription = "Crea e gestisci link per condividere questo file." +manageLoadFailed = "Impossibile caricare i link di condivisione." +manageTitle = "Gestisci condivisione" +noActivity = "Ancora nessuna attività." +noLinks = "Ancora nessun link di condivisione attivo." +noSharedUsers = "Ancora nessun utente ha accesso." +removeLink = "Rimuovi link" +removeUser = "Rimuovi" +revokeFailed = "Impossibile rimuovere il link di condivisione." +revoked = "Link di condivisione rimosso" +roleLabel = "Ruolo" +sharingDisabled = "La condivisione è disabilitata." +sharingDisabledBody = "La condivisione è stata disabilitata dalle impostazioni del tuo server." +sharedUsersTitle = "Utenti con cui è condiviso" +title = "Condividi file" +unknownUser = "Utente sconosciuto" +userAddFailed = "Impossibile condividere con quell'utente." +userAdded = "Utente aggiunto all'elenco di condivisione." +usernameLabel = "Nome utente o email" +usernamePlaceholder = "Inserisci un nome utente o un'email" +userRemoveFailed = "Impossibile rimuovere quell'utente." +userRemoved = "Utente rimosso dall'elenco di condivisione." +viewActivity = "Visualizza attività" +viewed = "Visualizzato" +viewsCount = "Visualizzazioni: {{count}}" +downloaded = "Scaricato" +bulkDescription = "Crea un unico link per condividere tutti i file selezionati con gli utenti autenticati." +bulkTitle = "Condividi i file selezionati" +copyLink = "Copia link di condivisione" +fileCount = "{{count}} file selezionati" +ownerOnly = "Solo il proprietario può gestire la condivisione." +selectSingleFile = "Seleziona un singolo file per gestire la condivisione." + +[storageUpload] +description = "Carica il file corrente nell'archiviazione del server per il tuo accesso." +errorTitle = "Caricamento non riuscito" +failure = "Caricamento non riuscito. Verifica le impostazioni di accesso e archiviazione." +fileLabel = "File" +hint = "I link pubblici e le modalità di accesso sono controllati dalle impostazioni del tuo server." +success = "Caricato sul server" +title = "Carica sul server" +updateButton = "Aggiorna sul server" +uploadButton = "Carica sul server" +bulkDescription = "Carica i file selezionati nell'archiviazione del tuo server." +bulkTitle = "Carica i file selezionati" +fileCount = "{{count}} file selezionati" +more = " +{{count}} altri" + [storage] approximateSize = "Dimensione approssimativa" fileTooLarge = "File troppo grande. Dimensione massima per file" @@ -7153,6 +7801,30 @@ title = "Visualizza/Modifica PDF" [warning] tooltipTitle = "Avviso" +[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" + [watermark] completed = "Filigrana aggiunta" desc = "Aggiungi filigrane di testo o immagine ai PDF" @@ -7333,6 +8005,7 @@ activeSession = "Sessione attiva" addMembers = "Aggiungi membri" admin = "Amministratore" confirmDelete = "Sei sicuro di voler eliminare questo utente? Questa azione non può essere annullata." +confirmUnlock = "Sei sicuro di voler sbloccare questo account utente?" deleteUser = "Elimina utente" deleteUserError = "Impossibile eliminare l'utente" deleteUserSuccess = "Utente eliminato con successo" @@ -7341,6 +8014,8 @@ disable = "Disabilita" disabled = "Disabilitato" editRole = "Modifica ruolo" enable = "Abilita" +locked = "bloccato" +lockedBadge = "Bloccato" loading = "Caricamento persone..." loginRequired = "Abilita prima la modalità login" member = "Membro" @@ -7350,6 +8025,9 @@ searchMembers = "Cerca membri..." status = "Stato" team = "Team" title = "Persone" +unlockAccount = "Sblocca account" +unlockUserError = "Impossibile sbloccare l'account utente" +unlockUserSuccess = "Account utente sbloccato con successo" user = "Utente" [workspace.people.actions] diff --git a/frontend/public/locales/ja-JP/translation.toml b/frontend/public/locales/ja-JP/translation.toml index 7325b724a1..b58e009014 100644 --- a/frontend/public/locales/ja-JP/translation.toml +++ b/frontend/public/locales/ja-JP/translation.toml @@ -8,6 +8,7 @@ black = "é»’" blue = "é’" bored = "å¾…ã¡æ™‚é–“ãŒé€€å±ˆ" cancel = "キャンセル" +confirm = "確èª" changedCredsMessage = "資格情報ãŒå¤‰æ›´ã•れã¾ã—ãŸï¼" chooseFile = "ãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠž" close = "é–‰ã˜ã‚‹" @@ -146,6 +147,7 @@ insufficientCredits = "クレジットãŒä¸è¶³ã—ã¦ã„ã¾ã™ã€‚å¿…è¦: {{requi loadingCredits = "クレジットを確èªã—ã¦ã„ã¾ã™..." loadingProStatus = "サブスクリプションã®çŠ¶æ…‹ã‚’ç¢ºèªã—ã¦ã„ã¾ã™..." noticeTopUpOrPlan = "クレジットãŒä¸è¶³ã—ã¦ã„ã¾ã™ã€‚ãƒãƒ£ãƒ¼ã‚¸ã™ã‚‹ã‹ãƒ—ランをアップグレードã—ã¦ãã ã•ã„" +accessInvite = "招待" [account] accountSettings = "アカウント設定" @@ -1427,6 +1429,34 @@ title = "処ç†" description = "エラーを報告ã™ã‚‹å‰ã«å‡¦ç†ã‚¸ãƒ§ãƒ–を待機ã™ã‚‹æœ€å¤§æ™‚間。" label = "処ç†ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆï¼ˆç§’)" +[admin.settings.storage] +description = "サーãƒãƒ¼ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸ã¨å…±æœ‰ã‚ªãƒ—ションを管ç†ã—ã¾ã™ã€‚" +title = "ファイルストレージã¨å…±æœ‰" + +[admin.settings.storage.enabled] +description = "ユーザーãŒã‚µãƒ¼ãƒãƒ¼ã«ãƒ•ァイルをä¿å­˜ã§ãるよã†ã«ã—ã¾ã™ã€‚" +label = "サーãƒãƒ¼ãƒ•ァイルストレージを有効化" + +[admin.settings.storage.sharing.email] +description = "メールアドレスã«ã‚ˆã‚‹å…±æœ‰ã‚’許å¯ã—ã¾ã™ã€‚" +label = "メール共有を有効化" +mailLink = "メール設定を設定" +mailNote = "メール設定ãŒå¿…è¦ã§ã™ã€‚ " + +[admin.settings.storage.sharing.enabled] +description = "ä¿å­˜æ¸ˆã¿ãƒ•ァイルã®å…±æœ‰ã‚’許å¯ã—ã¾ã™ã€‚" +label = "共有を有効化" + +[admin.settings.storage.sharing.links] +description = "サインインãŒå¿…è¦ãªãƒªãƒ³ã‚¯ã«ã‚ˆã‚‹å…±æœ‰ã‚’許å¯ã—ã¾ã™ã€‚" +frontendUrlLink = "システム設定ã§è¨­å®š" +frontendUrlNote = "フロントエンドURLãŒå¿…è¦ã§ã™ã€‚ " +label = "共有リンクを有効化" + +[admin.settings.storage.signing.enabled] +description = "複数å‚加者ã®ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆç½²åセッションã®ä½œæˆã‚’許å¯ã—ã¾ã™ã€‚サーãƒãƒ¼ãƒ•ã‚¡ã‚¤ãƒ«ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸ã®æœ‰åŠ¹åŒ–ãŒå¿…è¦ã§ã™ã€‚" +label = "グループ署å(アルファ)を有効化" + [admin.settings.unsavedChanges] cancel = "編集を続ã‘ã‚‹" discard = "変更を破棄" @@ -2059,7 +2089,19 @@ numbers = "数値/範囲:5ã€10-20" progressions = "等差列:3nã€4n+1" [certSign] +allSigned = "ã™ã¹ã¦ã®å‚加者ãŒç½²åã—ã¾ã—ãŸã€‚æœ€çµ‚åŒ–ã®æº–å‚™ãŒã§ãã¾ã—ãŸã€‚" +awaitingSignatures = "ç½²åå¾…ã¡" +signatureProgress = "{{signedCount}}/{{totalCount}} ä»¶ã®ç½²å" chooseCertificate = "è¨¼æ˜Žæ›¸ãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠž" +declined = "辞退" +fetchFailed = "ç½²åデータã®èª­ã¿è¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸ" +finalized = "最終化済ã¿" +notified = "ä¿ç•™ä¸­" +partialNote = "ç¾åœ¨ã®ç½²åã§æ—©æœŸã«æœ€çµ‚化ã§ãã¾ã™ã€‚未署åã®å‚加者ã¯é™¤å¤–ã•れã¾ã™ã€‚" +pending = "ä¿ç•™ä¸­" +readyToFinalize = "æœ€çµ‚åŒ–ã®æº–備完了" +signed = "ç½²åæ¸ˆã¿" +viewed = "閲覧済ã¿" chooseJksFile = "JKS ãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠž" chooseP12File = "PKCS12 ãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠž" choosePfxFile = "PFX ãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠž" @@ -2082,6 +2124,7 @@ title = "証明書ã«ã‚ˆã‚‹ç½²å" invisible = "éžè¡¨ç¤º" stepTitle = "ç½²åã®è¡¨ç¤º" visible = "表示" +visibility = "表示状態" [certSign.appearance.options] title = "ç½²å詳細" @@ -2188,6 +2231,252 @@ bullet4 = "検証ã«ã‚«ã‚¹ã‚¿ãƒ è¨¼æ˜Žæ›¸ã‚’使用å¯èƒ½" text = "ç½²åã®ç¢ºèªã§ã¯ã€ç½²åãŒæœ‰åйã‹ã€èª°ãŒã„ã¤ç½²åã—ãŸã‹ã€ç½²åå¾Œã«æ–‡æ›¸ãŒå¤‰æ›´ã•れãŸã‹ã‚’知らã›ã¾ã™ã€‚" title = "ç½²åã®æ¤œè¨¼" +[certSign.collab.finalize] +button = "最終化ã—ã¦ç½²å済ã¿PDFを読ã¿è¾¼ã‚€" +early = "ç¾åœ¨ã®ç½²åã§æœ€çµ‚化" + +[certSign.collab.sessionDetail] +addButton = "å‚加者を追加" +addParticipants = "å‚加者を追加" +addParticipantsError = "å‚加者ã®è¿½åŠ ã«å¤±æ•—ã—ã¾ã—ãŸ" +backToList = "ã‚»ãƒƒã‚·ãƒ§ãƒ³ä¸€è¦§ã«æˆ»ã‚‹" +deleteConfirm = "実行ã—ã¾ã™ã‹ï¼Ÿå…ƒã«æˆ»ã›ã¾ã›ã‚“。" +deleteError = "セッションã®å‰Šé™¤ã«å¤±æ•—ã—ã¾ã—ãŸ" +deleted = "セッションを削除ã—ã¾ã—ãŸ" +deleteSession = "セッションを削除" +dueDate = "期é™" +finalizeError = "ã‚»ãƒƒã‚·ãƒ§ãƒ³ã®æœ€çµ‚化ã«å¤±æ•—ã—ã¾ã—ãŸ" +loadPdfError = "ç½²åæ¸ˆã¿PDFã®èª­ã¿è¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸ" +loadSignedPdf = "ç½²åæ¸ˆã¿PDFをアクティブファイルã«èª­ã¿è¾¼ã‚€" +messageLabel = "メッセージ" +noAdditionalInfo = "追加情報ã¯ã‚りã¾ã›ã‚“" +owner = "所有者" +participantRemoved = "å‚加者を削除ã—ã¾ã—ãŸ" +participants = "å‚加者" +participantsAdded = "å‚加者を追加ã—ã¾ã—ãŸ" +removeParticipant = "削除" +removeParticipantError = "å‚加者ã®å‰Šé™¤ã«å¤±æ•—ã—ã¾ã—ãŸ" +selectUsers = "ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’é¸æŠž..." +sessionInfo = "セッション情報" +workbenchTitle = "セッション管ç†" + +[certSign.collab.signRequest] +addedToFiles = "ドキュメントをアクティブファイルã«è¿½åŠ ã—ã¾ã—ãŸ" +addSignature = "ã‚ãªãŸã®ç½²åを追加" +addToFiles = "アクティブファイルã«è¿½åŠ " +advancedSettings = "詳細設定" +backToList = "ç½²åãƒªã‚¯ã‚¨ã‚¹ãƒˆã«æˆ»ã‚‹" +certificateChoice = "ç½²åã«ä½¿ç”¨ã™ã‚‹è¨¼æ˜Žæ›¸ã‚’é¸æŠž" +changeSignature = "ç½²åを変更" +clearSignature = "ç½²åをクリア" +completeAndSign = "完了ã—ã¦ç½²å" +createNewSignature = "æ–°ã—ã„ç½²åを作æˆ" +declineButton = "辞退" +decline = "リクエストを辞退" +deleteSelected = "é¸æŠžã—ãŸç½²åを削除" +drawSignature = "下ã«ç½²åを手書ãã—ã¦ãã ã•ã„" +dueDate = "期é™" +fileTooLarge = "ファイルサイズã¯5MB未満ã§ã‚ã‚‹å¿…è¦ãŒã‚りã¾ã™" +fontFamily = "フォントファミリー" +fontSize = "フォントサイズ: {{size}}px" +fontSizePlaceholder = "サイズ" +from = "é€ä¿¡è€…" +invalidCertFile = "P12 ã¾ãŸã¯ PFX ã®è¨¼æ˜Žæ›¸ãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠžã—ã¦ãã ã•ã„" +invalidFileType = "ç”»åƒãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠžã—ã¦ãã ã•ã„" +location = "場所(任æ„)" +locationPlaceholder = "ã©ã“ã§ç½²åã—ã¦ã„ã¾ã™ã‹ï¼Ÿ" +message = "メッセージ" +noCertificate = "è¨¼æ˜Žæ›¸ãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠžã—ã¦ãã ã•ã„" +noSignatures = "å°‘ãªãã¨ã‚‚1ã¤ã®ç½²åã‚’PDFã«é…ç½®ã—ã¦ãã ã•ã„" +p12File = "P12/PFX 証明書ファイル" +password = "証明書ã®ãƒ‘スワード" +passwordPlaceholder = "パスワードを入力..." +penColor = "ペンã®è‰²" +penSize = "ペンサイズ: {{size}}px" +placementActive = "PDFをクリックã—ã¦é…ç½®" +placeSignatureButton = "PDFã«ç½²åã‚’é…ç½®" +reason = "ç†ç”±ï¼ˆä»»æ„)" +reasonPlaceholder = "ãªãœç½²åã—ã¾ã™ã‹ï¼Ÿ" +removeImage = "ç”»åƒã‚’削除" +removeCertFile = "ファイルを削除" +savedSignatures = "ä¿å­˜æ¸ˆã¿ã®ç½²å" +selectFile = "ç”»åƒãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠž" +selectSignatureTitle = "ç½²åã‚’é¸æŠžã¾ãŸã¯ä½œæˆ" +signButton = "ドキュメントã«ç½²å" +signatureInfo = "ã“れらã®è¨­å®šã¯ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆã®æ‰€æœ‰è€…ã«ã‚ˆã£ã¦è¨­å®šã•れã¦ã„ã¾ã™" +signaturePlaced = "ページã«ç½²åã‚’é…ç½®ã—ã¾ã—ãŸ" +signatureSettings = "ç½²å設定" +signatureText = "ç½²åテキスト" +signatureTextPlaceholder = "åå‰ã‚’入力..." +signatureTypeLabel = "ç½²åタイプ" +signingTitle = "ç½²å" +textColor = "テキストã®è‰²" +typeSignature = "åå‰ã‚’入力ã—ã¦ç½²åを作æˆ" +uploadCert = "カスタム証明書" +uploadCertDesc = "自分㮠P12/PFX 証明書を使用" +uploadSignature = "ç½²åç”»åƒã‚’アップロード" +usePersonalCert = "個人証明書" +usePersonalCertDesc = "ã‚ãªãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆç”¨ã«è‡ªå‹•生æˆã•れã¾ã™" +useServerCert = "組織証明書" +useServerCertDesc = "共有ã®çµ„織証明書" +workbenchTitle = "ç½²åリクエスト" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "ç·šã®è‰²ã‚’é¸æŠž" +continue = "続行" + +[certSign.collab.signRequest.certModal] +description = "{{count}} 個ã®ç½²åã‚’é…ç½®ã—ã¾ã—ãŸã€‚ç½²åを完了ã™ã‚‹è¨¼æ˜Žæ›¸ã‚’é¸æŠžã—ã¦ãã ã•ã„。" +sign = "ドキュメントã«ç½²å" +certValidating = "証明書を検証ã—ã¦ã„ã¾ã™..." +certValidUntil = "証明書ã¯{{date}}ã¾ã§æœ‰åй" +certInvalid = "証明書ãŒç„¡åйã§ã™: {{error}}" +certInvalidFallback = "無効ãªè¨¼æ˜Žæ›¸" +certNetworkError = "証明書を検証ã§ãã¾ã›ã‚“ã§ã—ãŸ" +title = "証明書を設定" + +[certSign.collab.signRequest.image] +hint = "ç½²åã® PNG ã¾ãŸã¯ JPG ç”»åƒã‚’アップロード" + +[certSign.collab.signRequest.mode] +move = "ç½²åを移動" +place = "ç½²åã‚’é…ç½®" +title = "ç½²å/移動モード" + +[certSign.collab.signRequest.modeTabs] +draw = "手書ã" +image = "アップロード" +text = "入力" + +[certSign.collab.signRequest.placeSignature] +message = "PDFをクリックã—ã¦ç½²åã‚’é…ç½®ã—ã¦ãã ã•ã„" +title = "ç½²åã‚’é…ç½®" + +[certSign.collab.signRequest.preview] +imageAlt = "é¸æŠžã—ãŸç½²å" +missing = "プレビューãªã—" +textFallback = "ç½²å" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "手書ãã®ç½²å" +defaultImageLabel = "アップロードã—ãŸç½²å" +defaultLabel = "ç½²å" +defaultTextLabel = "入力ã—ãŸç½²å" +delete = "ç½²åを削除" +none = "ä¿å­˜æ¸ˆã¿ã®ç½²åã¯ã‚りã¾ã›ã‚“" + +[certSign.collab.signRequest.signatureType] +draw = "手書ã" +type = "入力" +upload = "アップロード" + +[certSign.collab.signRequest.steps] +back = "戻る" +cancelPlacement = "é…置をキャンセル" +certificate = "証明書" +clickMultipleTimes = "PDFを複数回クリックã—ã¦ç½²åã‚’é…ç½®ã—ã¾ã™ã€‚ç½²åをドラッグã—ã¦ç§»å‹•やサイズ変更ãŒã§ãã¾ã™ã€‚" +clickToPlace = "ç½²åを表示ã—ãŸã„場所をPDF上ã§ã‚¯ãƒªãƒƒã‚¯ã—ã¦ãã ã•ã„。" +continue = "証明書ã®é¸æŠžã«é€²ã‚€" +continueToPlacement = "é…ç½®ã«é€²ã‚€" +continueToReview = "確èªã«é€²ã‚€" +createSignature = "ç½²åを作æˆ" +invisible = "éžè¡¨ç¤º" +location = "場所:" +multipleSignatures = "{{count}} 個ã®ç½²åãŒPDFã«é©ç”¨ã•れã¾ã™" +oneSignature = "1 個ã®ç½²åãŒPDFã«é©ç”¨ã•れã¾ã™" +placeOnPdf = "PDFã«é…ç½®" +reason = "ç†ç”±:" +reviewTitle = "ç½²åå‰ã®ç¢ºèª" +signaturePlaced = "ページ {{page}} ã«ç½²åã‚’é…ç½®ã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ã‚¯ãƒªãƒƒã‚¯ã—ã¦ä½ç½®ã‚’調整ã™ã‚‹ã‹ã€ç¢ºèªã«é€²ã‚“ã§ãã ã•ã„。" +visible = "表示" +visibility = "表示状態:" +yourSignatures = "ã‚ãªãŸã®ç½²å({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "色" +fontLabel = "フォント" +fontSizeLabel = "サイズ" +fontSizePlaceholder = "16" +label = "ç½²åテキスト" +modalHint = "åå‰ã‚’入力ã—ã€ç¶šè¡Œã‚’クリックã—ã¦PDFã«é…ç½®ã—ã¾ã™ã€‚" +placeholder = "åå‰ã‚’入力..." + +[certSign.collab.participant] +certValidating = "証明書を検証ã—ã¦ã„ã¾ã™..." +certValid = "✓ è¨¼æ˜Žæ›¸ã¯æœ‰åйã§ã™" +certValidUntil = " ({{date}}ã¾ã§ï¼‰" +certInvalid = "✗ {{error}}" +certInvalidFallback = "無効ãªè¨¼æ˜Žæ›¸" +certNetworkError = "証明書を検証ã§ãã¾ã›ã‚“ã§ã—ãŸ" + +[certSign.collab.addParticipants] +add = "å‚加者を{{count}}人追加" +back = "戻る" +configureSignatures = "ç½²å設定を設定" +continue = "ç½²å設定ã«é€²ã‚€" +reasonHelp = "ã“れらã®å‚加者ã®ç½²åç†ç”±ã‚’事å‰è¨­å®šã—ã¾ã™ï¼ˆä»»æ„ã€ç½²å時ã«ä¸Šæ›¸ãå¯èƒ½ï¼‰" +reasonPlaceholder = "例)承èªã€ãƒ¬ãƒ“ュー..." +selectUsers = "ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’é¸æŠž" + +[certSign.collab.sessionCreation] +includeSummaryPage = "ç½²åサマリーページをå«ã‚ã‚‹" +includeSummaryPageHelp = "末尾ã«ã™ã¹ã¦ã®ç½²åメタデータをå«ã‚€è¦ç´„ページãŒè¿½åŠ ã•れã¾ã™ã€‚å„ページã®ãƒ‡ã‚¸ã‚¿ãƒ«è¨¼æ˜Žæ›¸ã®ç½²åボックスã¯è¡¨ç¤ºã•れã¾ã›ã‚“(手書ãç½²åã«ã¯å½±éŸ¿ã—ã¾ã›ã‚“)。" + +[certSign.collab.sessionList] +active = "アクティブ" +finalized = "最終化済ã¿" + +[certSign.collab.signatureSettings] +description = "ã™ã¹ã¦ã®å‚加者ã®ç½²åã®è¡¨ç¤ºæ–¹æ³•を設定ã—ã¾ã™" +title = "ç½²åã®å¤–観" + +[certSign.collab.userSelector] +inviteUsers = "ユーザーを追加" +loadError = "ユーザーã®èª­ã¿è¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸ" +noTeam = "ãƒãƒ¼ãƒ ãªã—" +noUsers = "ä»–ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。" +placeholder = "ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’é¸æŠž..." + +[certSign.mobile] +panelActions = "アクション" +panelDocument = "ドキュメント" +panelPeople = "ユーザー" + +[certSign.sessions] +deleted = "セッションを削除ã—ã¾ã—ãŸ" +fetchFailed = "セッション詳細ã®èª­ã¿è¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸ" +finalized = "セッションを最終化ã—ã¾ã—ãŸ" +loaded = "ç½²åæ¸ˆã¿PDFを読ã¿è¾¼ã¿ã¾ã—ãŸ" +pdfNotReady = "PDFã¯æœªæº–å‚™ã§ã™" +pdfNotReadyDesc = "ç½²åæ¸ˆã¿PDFを生æˆä¸­ã§ã™ã€‚å°‘ã—å¾…ã£ã¦ã‹ã‚‰å†è©¦è¡Œã—ã¦ãã ã•ã„。" + +[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 をアップロード" + [changeCreds] changePassword = "デフォルトã®ãƒ­ã‚°ã‚¤ãƒ³èªè¨¼æƒ…報を使用ã—ã¦ã„ã¾ã™ã€‚æ–°ã—ã„パスワードを入力ã—ã¦ãã ã•ã„" changeUsername = "ユーザーåã‚’æ›´æ–°ã—ã¾ã™ã€‚更新後ã¯ãƒ­ã‚°ã‚¢ã‚¦ãƒˆã•れã¾ã™ã€‚" @@ -3242,6 +3531,46 @@ totalSelected = "åˆè¨ˆé¸æŠžæ•°" unsupported = "未対応" unzip = "è§£å‡" uploadError = "一部ã®ãƒ•ァイルã®ã‚¢ãƒƒãƒ—ロードã«å¤±æ•—ã—ã¾ã—ãŸã€‚" +copyCreated = "ã“ã®ãƒ‡ãƒã‚¤ã‚¹ã«ã‚³ãƒ”ーをä¿å­˜ã—ã¾ã—ãŸã€‚" +copyFailed = "コピーを作æˆã§ãã¾ã›ã‚“ã§ã—ãŸã€‚" +leaveShare = "自分ã®ä¸€è¦§ã‹ã‚‰å‰Šé™¤" +leaveShareFailed = "共有ファイルを削除ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚" +leaveShareSuccess = "共有リストã‹ã‚‰å‰Šé™¤ã—ã¾ã—ãŸã€‚" +removeBoth = "両方ã‹ã‚‰å‰Šé™¤" +removeFilePrompt = "ã“ã®ãƒ•ァイルã¯ã“ã®ãƒ‡ãƒã‚¤ã‚¹ã¨ã‚µãƒ¼ãƒãƒ¼ã®ä¸¡æ–¹ã«ä¿å­˜ã•れã¦ã„ã¾ã™ã€‚ã©ã¡ã‚‰ã‹ã‚‰å‰Šé™¤ã—ã¾ã™ã‹ï¼Ÿ" +removeFileTitle = "ファイルを削除" +removeLocalOnly = "ã“ã®ãƒ‡ãƒã‚¤ã‚¹ã®ã¿" +removeServerFailed = "サーãƒãƒ¼ã‹ã‚‰ãƒ•ァイルを削除ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚" +removeServerOnly = "サーãƒãƒ¼ã®ã¿" +removeServerOnlyPrompt = "ã“ã®ãƒ•ァイルã¯ã‚µãƒ¼ãƒãƒ¼ã®ã¿ã«ä¿å­˜ã•れã¦ã„ã¾ã™ã€‚サーãƒãƒ¼ã‹ã‚‰å‰Šé™¤ã—ã¾ã™ã‹ï¼Ÿ" +removeServerSuccess = "サーãƒãƒ¼ã‹ã‚‰å‰Šé™¤ã—ã¾ã—ãŸã€‚" +removeSharedPrompt = "ã“ã®ãƒ•ァイルã¯ã‚ãªãŸã¨å…±æœ‰ã•れã¦ã„ã¾ã™ã€‚ã“ã®ãƒ‡ãƒã‚¤ã‚¹ã‹ã‚‰å‰Šé™¤ã™ã‚‹ã‹ã€å…±æœ‰ãƒªã‚¹ãƒˆã‹ã‚‰å‰Šé™¤ã§ãã¾ã™ã€‚" +removeSharedServerOnlyBlockedPrompt = "ã“ã®ãƒ•ァイルã¯ã‚ãªãŸã¨å…±æœ‰ã•れã¦ãŠã‚Šã€ã‚µãƒ¼ãƒãƒ¼ã®ã¿ã«ä¿å­˜ã•れã¦ã„ã¾ã™ã€‚" +removeSharedServerOnlyPrompt = "ã“ã®ãƒ•ァイルã¯ã‚ãªãŸã¨å…±æœ‰ã•れã€ã‚µãƒ¼ãƒãƒ¼ã®ã¿ã«ä¿å­˜ã•れã¦ã„ã¾ã™ã€‚リストã‹ã‚‰å‰Šé™¤ã—ã¾ã™ã‹ï¼Ÿ" +changesNotUploaded = "変更ã¯ã‚¢ãƒƒãƒ—ロードã•れã¦ã„ã¾ã›ã‚“" +cloudFile = "クラウドファイル" +filterAll = "ã™ã¹ã¦" +filterLocal = "ローカル" +filterSharedByMe = "自分ãŒå…±æœ‰" +filterSharedWithMe = "自分ã¨å…±æœ‰" +lastSynced = "æœ€çµ‚åŒæœŸ" +localOnly = "ローカルã®ã¿" +makeCopy = "コピーを作æˆ" +owner = "所有者" +ownerUnknown = "䏿˜Ž" +share = "共有" +shareSelected = "é¸æŠžé …ç›®ã‚’å…±æœ‰" +sharedByYou = "ã‚ãªãŸãŒå…±æœ‰" +sharedEditNoticeBody = "ã“ã®ãƒ•ァイルã®ã‚µãƒ¼ãƒãƒ¼ä¸Šã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã«ç·¨é›†æ¨©é™ã¯ã‚りã¾ã›ã‚“。行ã£ãŸç·¨é›†ã¯ãƒ­ãƒ¼ã‚«ãƒ«ã‚³ãƒ”ーã¨ã—ã¦ä¿å­˜ã•れã¾ã™ã€‚" +sharedEditNoticeConfirm = "了解" +sharedEditNoticeTitle = "サーãƒãƒ¼ä¸Šã®é–²è¦§å°‚用コピー" +sharedWithYou = "ã‚ãªãŸã¨å…±æœ‰" +sharing = "共有" +storageState = "ストレージ" +synced = "åŒæœŸæ¸ˆã¿" +updateOnServer = "サーãƒãƒ¼ä¸Šã§æ›´æ–°" +uploadSelected = "é¸æŠžé …ç›®ã‚’ã‚¢ãƒƒãƒ—ãƒ­ãƒ¼ãƒ‰" +uploadToServer = "サーãƒãƒ¼ã«ã‚¢ãƒƒãƒ—ロード" [files] addFiles = "ファイルを追加" @@ -3367,6 +3696,77 @@ title = "PDF ã®ãƒ•ラット化ã«ã¤ã„ã¦" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "グループ署åã«ã¤ã„ã¦" + +[groupSigning.tooltip.finalization] +bullet1 = "ã™ã¹ã¦ã®ç½²åã¯æŒ‡å®šã—ãŸå‚加者ã®é †åºã§é©ç”¨ã•れã¾ã™" +bullet2 = "å¿…è¦ã«å¿œã˜ã¦ä¸€éƒ¨ã®ç½²åã ã‘ã§æœ€çµ‚化ã§ãã¾ã™" +bullet3 = "最終化後ã¯ã‚»ãƒƒã‚·ãƒ§ãƒ³ã‚’変更ã§ãã¾ã›ã‚“" +description = "å…¨å‚加者ã®ç½²å完了後(ã¾ãŸã¯æ—©æœŸæœ€çµ‚åŒ–ã‚’é¸æŠžã—ãŸå ´åˆï¼‰ã€æœ€çµ‚çš„ãªç½²å済ã¿PDFを生æˆã§ãã¾ã™ã€‚" +title = "最終化プロセス" + +[groupSigning.tooltip.roles] +bullet1 = "所有者(ã‚ãªãŸï¼‰: セッションを作æˆã—ã€ç½²åã®æ—¢å®šå€¤ã‚’設定ã—ã€ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆã‚’最終化" +bullet2 = "å‚加者: ç½²åを作æˆã—ã€è¨¼æ˜Žæ›¸ã‚’é¸æŠžã—ã¦PDFã«é…ç½®" +bullet3 = "å‚加者ã¯ç½²åã®è¡¨ç¤ºã€ç†ç”±ã€å ´æ‰€ã®è¨­å®šã‚’変更ã§ãã¾ã›ã‚“" +description = "ã™ã¹ã¦ã®å‚加者ã®ç½²åã®å¤–観設定をã‚ãªãŸãŒç®¡ç†ã—ã¾ã™ã€‚" +title = "å‚加者ã®å½¹å‰²" + +[groupSigning.tooltip.sequential] +bullet1 = "2人目ãŒãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹å‰ã«ã€æœ€åˆã®å‚加者ãŒç½²åã™ã‚‹å¿…è¦ãŒã‚りã¾ã™" +bullet2 = "法令順守ã®ãŸã‚é©åˆ‡ãªç½²åé †åºã‚’確ä¿" +bullet3 = "リスト内ã§ãƒ‰ãƒ©ãƒƒã‚°ã—ã¦å‚加者ã®é †åºã‚’変更ã§ãã¾ã™" +description = "å‚åŠ è€…ã¯æŒ‡å®šã—ãŸé †åºã§ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆã«ç½²åã—ã¾ã™ã€‚å„ç½²å者ã¯è‡ªåˆ†ã®é †ç•ªã«ãªã‚‹ã¨é€šçŸ¥ã‚’å—ã‘å–りã¾ã™ã€‚" +title = "順次署å" + +[groupSigning.steps] +back = "戻る" +completed = "完了" +current = "ç¾åœ¨" +stepLabel = "ステップ {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "確èªã«é€²ã‚€" +invisible = "ç½²åã¯éžè¡¨ç¤ºã«ãªã‚Šã¾ã™ï¼ˆãƒ¡ã‚¿ãƒ‡ãƒ¼ã‚¿ã®ã¿ï¼‰" +locationLabel = "場所:" +preview = "プレビュー" +reasonLabel = "ç†ç”±:" +title = "ç½²å設定を設定" +visible = "ç½²åã¯ãƒšãƒ¼ã‚¸ {{page}} ã«è¡¨ç¤ºã•れã¾ã™" + +[groupSigning.steps.review] +document = "ドキュメント" +dueDate = "期é™ï¼ˆä»»æ„)" +dueDatePlaceholder = "期é™ã‚’é¸æŠž..." +invisible = "éžè¡¨ç¤ºï¼ˆãƒ¡ã‚¿ãƒ‡ãƒ¼ã‚¿ã®ã¿ï¼‰" +location = "場所:" +logo = "ロゴ:" +logoHidden = "ロゴãªã—" +logoShown = "Stirling PDF ロゴを表示" +participants = "å‚加者" +reason = "ç†ç”±:" +send = "ç½²åリクエストをé€ä¿¡" +signatureSettings = "ç½²å設定" +title = "セッション詳細ã®ç¢ºèª" +titleShort = "確èªã—ã¦é€ä¿¡" +visibility = "表示状態:" +visible = "ページ {{page}} ã«è¡¨ç¤º" +participantCount = "{{count}} 人ãŒé †ç•ªã«ç½²åã—ã¾ã™" + +[groupSigning.steps.selectDocument] +continue = "å‚加者ã®é¸æŠžã«é€²ã‚€" +noFile = "ç½²åセッションを作æˆã™ã‚‹ã«ã¯ã€ã‚¢ã‚¯ãƒ†ã‚£ãƒ–ファイルã‹ã‚‰1ã¤ã®PDFãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠžã—ã¦ãã ã•ã„。" +selectedFile = "é¸æŠžã—ãŸãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆ" +title = "ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆã‚’é¸æŠž" + +[groupSigning.steps.selectParticipants] +continue = "ç½²å設定ã«é€²ã‚€" +count = "{{count}} äººã‚’é¸æŠž" +label = "å‚åŠ è€…ã‚’é¸æŠž" +placeholder = "å‚åŠ è€…ã‚’é¸æŠž..." +title = "å‚åŠ è€…ã‚’é¸æŠž" + [getPdfInfo] downloadJson = "JSONã§ãƒ€ã‚¦ãƒ³ãƒ­ãƒ¼ãƒ‰" downloads = "ダウンロード" @@ -4460,7 +4860,10 @@ zoomOut = "縮å°" [viewer] cannotPreviewFile = "ファイルをプレビューã§ãã¾ã›ã‚“" +disableColorFilter = "カラーフィルターを無効化" dualPageView = "見開ã表示" +enableDarkFilter = "ダークフィルターを有効化" +enableSepiaFilter = "セピアフィルターを有効化" firstPage = "最åˆã®ãƒšãƒ¼ã‚¸" lastPage = "最後ã®ãƒšãƒ¼ã‚¸" nextPage = "次ã®ãƒšãƒ¼ã‚¸" @@ -4470,6 +4873,22 @@ singlePageView = "å˜ä¸€ãƒšãƒ¼ã‚¸è¡¨ç¤º" unknownFile = "䏿˜Žãªãƒ•ァイル" zoomIn = "拡大" zoomOut = "縮å°" +resetZoom = "ズームをリセット" + +[viewer.nonPdf] +fileTypeBadge = "{{type}} ファイル" +convertToPdf = "PDF ã«å¤‰æ›" +loading = "読ã¿è¾¼ã¿ä¸­..." +emptyFile = "空ã®ãƒ•ァイル" +csvStats = "{{rows}} 行 · {{columns}} 列 · {{size}}" +sortedBy = "ä¸¦ã³æ›¿ãˆåŸºæº–: {{column}}" +columnDefault = "列 {{index}}" +htmlPreviewWarning = "HTMLプレビュー — 外部リソースã¯èª­ã¿è¾¼ã¾ã‚Œãªã„å ´åˆãŒã‚りã¾ã™ · {{size}}" +htmlPreview = "HTMLプレビュー" +invalidJson = "無効㪠JSON — 生ã®å†…容を表示ã—ã¦ã„ã¾ã™" +textStats = "{{lines}} 行 · {{size}}" +lineNumbers = "行番å·" +renderMarkdown = "Markdown をレンダリング" [viewer.attachments] title = "添付ファイル" @@ -4531,6 +4950,7 @@ toggleAttachments = "添付ファイルã®è¡¨ç¤ºåˆ‡æ›¿" toggleTheme = "テーマを切り替ãˆ" language = "言語" toggleAnnotations = "注釈ã®è¡¨ç¤ºã‚’切り替ãˆ" +toggleLayers = "レイヤーã®åˆ‡ã‚Šæ›¿ãˆ" search = "PDF を検索" panMode = "パンモード" applyRedactionsFirst = "å…ˆã«å¢¨æ¶ˆã—ã‚’é©ç”¨" @@ -5407,20 +5827,72 @@ title = "ファイルã®å°åˆ·" 2 = "プリンタåを入力" [quickAccess] +access = "アクセス" +accessAddPerson = "別ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’追加" +accessBack = "戻る" +accessCopyLink = "リンクをコピー" +accessEmail = "メールアドレス" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "ファイル" +accessGeneral = "一般アクセス" +accessInviteTitle = "ユーザーを招待" +accessOwner = "所有者" +accessPanel = "ドキュメントã®ã‚¢ã‚¯ã‚»ã‚¹" +accessPeople = "アクセス権ã®ã‚るユーザー" +accessRemove = "削除" +accessRestricted = "制é™ä»˜ã" +accessRestrictedHint = "アクセス権ã®ã‚るユーザーã®ã¿ãŒé–‹ã‘ã¾ã™" +accessRole = "権é™" +accessRoleCommenter = "コメント投稿者" +accessRoleEditor = "編集者" +accessRoleViewer = "閲覧者" +accessSelectedFile = "é¸æŠžã—ãŸãƒ•ァイル" +accessSendInvite = "招待をé€ä¿¡" +accessTitle = "ドキュメントã®ã‚¢ã‚¯ã‚»ã‚¹" +accessYou = "ã‚ãªãŸ" account = "アカウント" +activeSessions = "アクティブãªã‚»ãƒƒã‚·ãƒ§ãƒ³" +activeTab = "アクティブ" activity = "アクティビティ" adminSettings = "管ç†è€…設定" +allSessions = "ã™ã¹ã¦ã®ã‚»ãƒƒã‚·ãƒ§ãƒ³" allTools = "All Tools" automate = "自動化" +back = "戻る" +certSign = "証明書署å" +completedSessions = "完了ã—ãŸã‚»ãƒƒã‚·ãƒ§ãƒ³" +completedTab = "完了" config = "æ§‹æˆ" +createNew = "æ–°ã—ã„リクエストを作æˆ" +createSession = "ç½²åリクエストを作æˆ" +dueDate = "期é™ï¼ˆä»»æ„)" files = "ファイル" help = "ヘルプ" +noActiveSessions = "ä¿ç•™ä¸­ã®ç½²åリクエストやアクティブãªã‚»ãƒƒã‚·ãƒ§ãƒ³ã¯ã‚りã¾ã›ã‚“" +noCompletedSessions = "完了ã—ãŸã‚»ãƒƒã‚·ãƒ§ãƒ³ã¯ã‚りã¾ã›ã‚“" +noFile = "ファイルãŒé¸æŠžã•れã¦ã„ã¾ã›ã‚“" read = "読む" reader = "リーダー" +refresh = "æ›´æ–°" +requestSignatures = "ç½²åã‚’ä¾é ¼" +selectSingleFileToRequest = "ç½²åã‚’ä¾é ¼ã™ã‚‹PDFファイルを1ã¤é¸æŠžã—ã¦ãã ã•ã„" +selectedFile = "é¸æŠžã—ãŸãƒ•ァイル" +selectUsers = "ç½²åã™ã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’é¸æŠž" +selectUsersPlaceholder = "å‚åŠ è€…ã‚’é¸æŠž..." +sendingRequest = "é€ä¿¡ä¸­..." settings = "設定" showMeAround = "案内ã—ã¦" sign = "ç½²å" +signatureRequests = "ç½²åリクエスト" +signYourself = "自分ã§ç½²å" +newRequest = "æ–°è¦ãƒªã‚¯ã‚¨ã‚¹ãƒˆ" tours = "ツアー" +wetSign = "ç½²åを追加" +filterMine = "自分" +filterOverdue = "期é™è¶…éŽ" +filterSigned = "ç½²åæ¸ˆã¿" +filterDeclined = "辞退" +searchDocuments = "ドキュメントを検索…" [quickAccess.helpMenu] adminTour = "管ç†ãƒ„アー" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Stirling-PDF サーãƒãƒ¼ãŒã‚ªãƒ•ラインã§ã€ãƒ­ expired = "ã‚»ãƒƒã‚·ãƒ§ãƒ³ãŒæœŸé™åˆ‡ã‚Œã§ã™ã€‚ページを更新ã—ã¦ã‚‚ã†ä¸€åº¦ãŠè©¦ã—ãã ã•ã„。" refreshPage = "ページを更新" +[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 = "ç½²åã®é †åº" + +[signatureSettings.tooltip] +header = "ç½²åã®å¤–観設定" + +[signatureSettings.tooltip.location] +bullet1 = "例: \"New York, USA\"ã€\"London Office\"ã€\"Remote\"" +bullet2 = "ページ上ã®ä½ç½®ã¨ã¯ç•°ãªã‚Šã¾ã™" +bullet3 = "法域ã«ã‚ˆã£ã¦ã¯å¿…é ˆã¨ãªã‚‹å ´åˆãŒã‚りã¾ã™" +description = "ç½²åãŒè¡Œã‚れãŸåœ°ç†çš„ãªå ´æ‰€ï¼ˆä»»æ„)。証明書ã®ãƒ¡ã‚¿ãƒ‡ãƒ¼ã‚¿ã«ä¿å­˜ã•れã¾ã™ã€‚" +title = "ç½²åã®å ´æ‰€" + +[signatureSettings.tooltip.logo] +bullet1 = "ç½²åã¨ãƒ†ã‚­ã‚¹ãƒˆã®æ¨ªã«è¡¨ç¤ºã•れã¾ã™" +bullet2 = "PNGã€JPG å½¢å¼ã«å¯¾å¿œ" +bullet3 = "プロフェッショナルãªå°è±¡ã‚’高ã‚ã¾ã™" +description = "ブランドã¨çœŸæ­£æ€§ã®ãŸã‚ã«ã€è¡¨ç¤ºã•れる署åã«ä¼šç¤¾ã®ãƒ­ã‚´ã‚’追加ã—ã¾ã™ã€‚" +title = "会社ロゴ" + +[signatureSettings.tooltip.reason] +bullet1 = "例: \"Approval\"ã€\"Contract Agreement\"ã€\"Review Complete\"" +bullet2 = "PDFã®ç½²åプロパティã«è¡¨ç¤ºã•れã¾ã™" +bullet3 = "ç›£æŸ»è¨¼è·¡ã‚„ã‚³ãƒ³ãƒ—ãƒ©ã‚¤ã‚¢ãƒ³ã‚¹ã«æœ‰ç”¨ã§ã™" +description = "ドキュメントã«ç½²åã™ã‚‹ç†ç”±ï¼ˆä»»æ„)。証明書ã®ãƒ¡ã‚¿ãƒ‡ãƒ¼ã‚¿ã«ä¿å­˜ã•れã¾ã™ã€‚" +title = "ç½²åç†ç”±" + +[signatureSettings.tooltip.visibility] +bullet1 = "表示: カスタム外観ã§PDFã«ç½²åãŒè¡¨ç¤ºã•れã¾ã™" +bullet2 = "éžè¡¨ç¤º: 視覚的ãªãƒžãƒ¼ã‚¯ãªã—ã§è¨¼æ˜Žæ›¸ã®ã¿ã‚’埋ã‚è¾¼ã¿ã¾ã™" +bullet3 = "éžè¡¨ç¤ºã®ç½²åã§ã‚‚æš—å·å­¦çš„ãªæ¤œè¨¼ãŒæä¾›ã•れã¾ã™" +description = "ç½²åをドキュメント上ã«è¡¨ç¤ºã™ã‚‹ã‹ã€éžè¡¨ç¤ºã§åŸ‹ã‚込むã‹ã‚’制御ã—ã¾ã™ã€‚" +title = "ç½²åã®è¡¨ç¤º" + [settings.configuration] advanced = "詳細設定" database = "データベース" endpoints = "エンドãƒã‚¤ãƒ³ãƒˆ" features = "機能" +storageSharing = "ファイルストレージã¨å…±æœ‰" systemSettings = "システム設定" title = "æ§‹æˆ" @@ -6332,10 +6868,13 @@ title = "Stirling ã«ã‚µã‚¤ãƒ³ã‚¤ãƒ³" [setup.selfhosted] link = "ã¾ãŸã¯ã‚»ãƒ«ãƒ•ãƒ›ã‚¹ãƒˆåž‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«æŽ¥ç¶š" subtitle = "サーãƒãƒ¼ã®èªè¨¼æƒ…報を入力" +changeServerLocked = "ã‚ãªãŸã®çµ„ç¹”ã¯ã“ã®ã‚¢ãƒ—リを特定ã®ã‚µãƒ¼ãƒãƒ¼ã«åˆ¶é™ã—ã¦ã„ã¾ã™" switchToLocal = "代ã‚りã«ãƒ­ãƒ¼ã‚«ãƒ«ãƒ„ールを使用" title = "サーãƒãƒ¼ã«ã‚µã‚¤ãƒ³ã‚¤ãƒ³" [setup.selfhosted.unreachable] +changeServer = "別ã®ã‚µãƒ¼ãƒãƒ¼ã«æŽ¥ç¶š" +changeServerLocked = "ã‚ãªãŸã®çµ„ç¹”ã¯ã“ã®ã‚¢ãƒ—リを特定ã®ã‚µãƒ¼ãƒãƒ¼ã«åˆ¶é™ã—ã¦ã„ã¾ã™" continueOffline = "代ã‚りã«ãƒ­ãƒ¼ã‚«ãƒ«ãƒ„ールを使用" message = "{{url}} ã«åˆ°é”ã§ãã¾ã›ã‚“。サーãƒãƒ¼ãŒç¨¼åƒã—ã€ã‚¢ã‚¯ã‚»ã‚¹å¯èƒ½ã‹ç¢ºèªã—ã¦ãã ã•ã„。" retry = "å†è©¦è¡Œ" @@ -6529,6 +7068,15 @@ saved = "ä¿å­˜æ¸ˆã¿" text = "テキスト" title = "ç½²åタイプ" +[signRequest] +declined = "ç½²åリクエストを辞退ã—ã¾ã—ãŸ" +fetchFailed = "ç½²åリクエストã®èª­ã¿è¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸ" +signed = "ドキュメントã®ç½²åã«æˆåŠŸã—ã¾ã—ãŸ" + +[signSession] +createFailed = "ç½²åリクエストã®ä½œæˆã«å¤±æ•—ã—ã¾ã—ãŸ" +created = "ç½²åリクエストをé€ä¿¡ã—ã¾ã—ãŸ" + [signup] accountCreatedSuccessfully = "アカウントãŒä½œæˆã•れã¾ã—ãŸã€‚今ã™ãサインインã§ãã¾ã™ã€‚" alreadyHaveAccount = "ã™ã§ã«ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’ãŠæŒã¡ã§ã™ã‹ï¼Ÿã‚µã‚¤ãƒ³ã‚¤ãƒ³" @@ -6807,6 +7355,106 @@ title = "PDFã‚’ãƒãƒ£ãƒ—ターã”ã¨ã«åˆ†å‰²" [splitPdfByChapters] tags = "分割,ç« ,ã—ãŠã‚Š,æ•´ç†" +[storageShare] +accessed = "アクセス済ã¿" +accessDenied = "ã“ã®å…±æœ‰ãƒ•ァイルã¸ã®ã‚¢ã‚¯ã‚»ã‚¹æ¨©ãŒã‚りã¾ã›ã‚“。所有者ã«å…±æœ‰ã‚’ä¾é ¼ã—ã¦ãã ã•ã„。" +accessFailed = "アクティビティを読ã¿è¾¼ã‚ã¾ã›ã‚“。" +accessDeniedBody = "ã“ã®ãƒ•ァイルã¸ã®ã‚¢ã‚¯ã‚»ã‚¹æ¨©ãŒã‚りã¾ã›ã‚“。所有者ã«å…±æœ‰ã‚’ä¾é ¼ã—ã¦ãã ã•ã„。" +accessDeniedTitle = "アクセス権ãŒã‚りã¾ã›ã‚“" +accessLimitedCommenter = "コメント権é™ã¯è¿‘日対応予定ã§ã™ã€‚ダウンロードãŒå¿…è¦ãªå ´åˆã¯ã€æ‰€æœ‰è€…ã«ç·¨é›†æ¨©é™ã‚’ä¾é ¼ã—ã¦ãã ã•ã„。" +accessLimitedTitle = "制é™ä»˜ãアクセス" +accessLimitedViewer = "ã“ã®ãƒªãƒ³ã‚¯ã¯é–²è¦§ã®ã¿ã§ã™ã€‚ダウンロードãŒå¿…è¦ãªå ´åˆã¯ã€æ‰€æœ‰è€…ã«ç·¨é›†æ¨©é™ã‚’ä¾é ¼ã—ã¦ãã ã•ã„。" +createdAt = "ä½œæˆæ—¥æ™‚" +download = "ダウンロード" +downloadFailed = "ã“ã®ãƒ•ァイルをダウンロードã§ãã¾ã›ã‚“。" +expiredBody = "ã“ã®å…±æœ‰ãƒªãƒ³ã‚¯ã¯ç„¡åйã‹ã€æœ‰åŠ¹æœŸé™ãŒåˆ‡ã‚Œã¦ã„ã¾ã™ã€‚" +expiredTitle = "ãƒªãƒ³ã‚¯ã®æœ‰åŠ¹æœŸé™åˆ‡ã‚Œ" +goToLogin = "ログインã¸" +loadFailed = "共有ファイルを開ã‘ã¾ã›ã‚“。" +loading = "共有リンクを読ã¿è¾¼ã¿ä¸­..." +loginPrompt = "ã“ã®å…±æœ‰ãƒ•ァイルã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã«ã¯ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã—ã¦ãã ã•ã„。" +loginRequired = "ログインãŒå¿…è¦ã§ã™" +openInApp = "Stirling PDF ã§é–‹ã" +ownerLabel = "所有者" +ownerUnknown = "䏿˜Ž" +requiresLogin = "ã“ã®å…±æœ‰ãƒ•ァイルã¯ãƒ­ã‚°ã‚¤ãƒ³ãŒå¿…è¦ã§ã™ã€‚" +roleCommenter = "コメント投稿者" +roleEditor = "編集者" +roleViewer = "閲覧者" +shareHeading = "共有ファイル" +titleDefault = "共有ファイル" +tryAgain = "後ã§ã‚‚ã†ä¸€åº¦ãŠè©¦ã—ãã ã•ã„。" +addUser = "追加" +commenterHint = "コメント機能ã¯è¿‘日対応予定ã§ã™ã€‚" +copied = "リンクをクリップボードã«ã‚³ãƒ”ーã—ã¾ã—ãŸ" +copy = "コピー" +copyFailed = "コピーã«å¤±æ•—ã—ã¾ã—ãŸ" +description = "ã“ã®ãƒ•ァイルã®å…±æœ‰ãƒªãƒ³ã‚¯ã‚’作æˆã—ã¾ã™ã€‚リンクをæŒã¤ã‚µã‚¤ãƒ³ã‚¤ãƒ³æ¸ˆã¿ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã™ã€‚" +downloadsCount = "ダウンロード数: {{count}}" +emailWarningBody = "ã“れã¯ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã®ã‚ˆã†ã§ã™ã€‚ã“ã®ç›¸æ‰‹ãŒæ—¢ã« Stirling PDF ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã§ãªã„å ´åˆã€ãƒ•ァイルã«ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã›ã‚“。" +emailWarningConfirm = "ãれã§ã‚‚共有ã™ã‚‹" +emailWarningTitle = "メールアドレス" +errorTitle = "共有ã«å¤±æ•—ã—ã¾ã—ãŸ" +failure = "共有リンクを生æˆã§ãã¾ã›ã‚“。もã†ä¸€åº¦ãŠè©¦ã—ãã ã•ã„。" +fileLabel = "ファイル" +generate = "リンクを生æˆ" +generated = "共有リンクを生æˆã—ã¾ã—ãŸ" +hideActivity = "アクティビティをéžè¡¨ç¤º" +invalidUsername = "有効ãªãƒ¦ãƒ¼ã‚¶ãƒ¼åã¾ãŸã¯ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã‚’入力ã—ã¦ãã ã•ã„。" +lastAccessed = "最終アクセス" +linkAccessTitle = "共有リンクã®ã‚¢ã‚¯ã‚»ã‚¹" +linkLabel = "共有リンク" +linksDisabled = "共有リンクã¯ç„¡åŠ¹åŒ–ã•れã¦ã„ã¾ã™ã€‚" +linksDisabledBody = "サーãƒãƒ¼è¨­å®šã«ã‚ˆã‚Šå…±æœ‰ãƒªãƒ³ã‚¯ã¯ç„¡åŠ¹åŒ–ã•れã¦ã„ã¾ã™ã€‚" +manage = "共有を管ç†" +manageDescription = "ã“ã®ãƒ•ァイルを共有ã™ã‚‹ãŸã‚ã®ãƒªãƒ³ã‚¯ã‚’作æˆãƒ»ç®¡ç†ã—ã¾ã™ã€‚" +manageLoadFailed = "共有リンクを読ã¿è¾¼ã‚ã¾ã›ã‚“。" +manageTitle = "共有ã®ç®¡ç†" +noActivity = "ã¾ã ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティã¯ã‚りã¾ã›ã‚“。" +noLinks = "アクティブãªå…±æœ‰ãƒªãƒ³ã‚¯ã¯ã¾ã ã‚りã¾ã›ã‚“。" +noSharedUsers = "ã¾ã ã‚¢ã‚¯ã‚»ã‚¹æ¨©ã‚’æŒã¤ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ã„ã¾ã›ã‚“。" +removeLink = "リンクを削除" +removeUser = "削除" +revokeFailed = "共有リンクを削除ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚" +revoked = "共有リンクを削除ã—ã¾ã—ãŸ" +roleLabel = "権é™" +sharingDisabled = "共有ã¯ç„¡åйã§ã™ã€‚" +sharingDisabledBody = "サーãƒãƒ¼è¨­å®šã«ã‚ˆã‚Šå…±æœ‰ãŒç„¡åŠ¹åŒ–ã•れã¦ã„ã¾ã™ã€‚" +sharedUsersTitle = "共有ユーザー" +title = "ファイルを共有" +unknownUser = "䏿˜Žãªãƒ¦ãƒ¼ã‚¶ãƒ¼" +userAddFailed = "ãã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¨å…±æœ‰ã§ãã¾ã›ã‚“。" +userAdded = "共有リストã«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’追加ã—ã¾ã—ãŸã€‚" +usernameLabel = "ユーザーåã¾ãŸã¯ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹" +usernamePlaceholder = "ユーザーåã¾ãŸã¯ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã‚’入力" +userRemoveFailed = "ãã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’削除ã§ãã¾ã›ã‚“。" +userRemoved = "共有リストã‹ã‚‰ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’削除ã—ã¾ã—ãŸã€‚" +viewActivity = "アクティビティを表示" +viewed = "閲覧" +viewsCount = "閲覧数: {{count}}" +downloaded = "ダウンロード" +bulkDescription = "é¸æŠžã—ãŸã™ã¹ã¦ã®ãƒ•ァイルをサインイン済ã¿ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¨å…±æœ‰ã™ã‚‹1ã¤ã®ãƒªãƒ³ã‚¯ã‚’作æˆã—ã¾ã™ã€‚" +bulkTitle = "é¸æŠžã—ãŸãƒ•ァイルを共有" +copyLink = "共有リンクをコピー" +fileCount = "{{count}} ä»¶ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠžä¸­" +ownerOnly = "共有ã®ç®¡ç†ã¯æ‰€æœ‰è€…ã®ã¿ãŒè¡Œãˆã¾ã™ã€‚" +selectSingleFile = "共有を管ç†ã™ã‚‹ã«ã¯ãƒ•ァイルを1ä»¶é¸æŠžã—ã¦ãã ã•ã„。" + +[storageUpload] +description = "ç¾åœ¨ã®ãƒ•ァイルをサーãƒãƒ¼ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸ã«ã‚¢ãƒƒãƒ—ロードã—ã€è‡ªåˆ†ã§ã‚¢ã‚¯ã‚»ã‚¹ã§ãるよã†ã«ã—ã¾ã™ã€‚" +errorTitle = "アップロードã«å¤±æ•—ã—ã¾ã—ãŸ" +failure = "アップロードã«å¤±æ•—ã—ã¾ã—ãŸã€‚ログイン情報ã¨ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸è¨­å®šã‚’確èªã—ã¦ãã ã•ã„。" +fileLabel = "ファイル" +hint = "公開リンクã¨ã‚¢ã‚¯ã‚»ã‚¹ãƒ¢ãƒ¼ãƒ‰ã¯ã‚µãƒ¼ãƒãƒ¼è¨­å®šã§åˆ¶å¾¡ã•れã¾ã™ã€‚" +success = "サーãƒãƒ¼ã«ã‚¢ãƒƒãƒ—ロードã—ã¾ã—ãŸ" +title = "サーãƒãƒ¼ã«ã‚¢ãƒƒãƒ—ロード" +updateButton = "サーãƒãƒ¼ä¸Šã§æ›´æ–°" +uploadButton = "サーãƒãƒ¼ã«ã‚¢ãƒƒãƒ—ロード" +bulkDescription = "é¸æŠžã—ãŸãƒ•ァイルをサーãƒãƒ¼ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸ã«ã‚¢ãƒƒãƒ—ロードã—ã¾ã™ã€‚" +bulkTitle = "é¸æŠžã—ãŸãƒ•ァイルをアップロード" +fileCount = "{{count}} ä»¶ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’é¸æŠžä¸­" +more = " +{{count}} ä»¶ ã»ã‹" + [storage] approximateSize = "概算サイズ" fileTooLarge = "ファイルãŒå¤§ãã™ãŽã¾ã™ã€‚ファイルã”ã¨ã®æœ€å¤§ã‚µã‚¤ã‚ºã¯" @@ -7153,6 +7801,30 @@ title = "PDFã®è¡¨ç¤º/編集" [warning] tooltipTitle = "警告" +[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 = "ç½²åç”»åƒã‚’アップロード" + [watermark] completed = "é€ã‹ã—を追加ã—ã¾ã—ãŸ" desc = "PDF ファイルã«ãƒ†ã‚­ã‚¹ãƒˆã¾ãŸã¯ç”»åƒã®é€ã‹ã—を追加" @@ -7333,6 +8005,7 @@ activeSession = "アクティブセッション" addMembers = "メンãƒãƒ¼ã‚’追加" admin = "管ç†è€…" confirmDelete = "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’削除ã—ã¦ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿã“ã®æ“作ã¯å…ƒã«æˆ»ã›ã¾ã›ã‚“。" +confirmUnlock = "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ãƒ­ãƒƒã‚¯ã‚’解除ã—ã¦ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ" deleteUser = "ユーザーを削除" deleteUserError = "ユーザーã®å‰Šé™¤ã«å¤±æ•—ã—ã¾ã—ãŸ" deleteUserSuccess = "ユーザーを削除ã—ã¾ã—ãŸ" @@ -7341,6 +8014,8 @@ disable = "無効化" disabled = "無効" editRole = "ロールを編集" enable = "有効化" +locked = "ロック中" +lockedBadge = "ロック中" loading = "メンãƒãƒ¼ã‚’読ã¿è¾¼ã¿ä¸­..." loginRequired = "å…ˆã«ãƒ­ã‚°ã‚¤ãƒ³ãƒ¢ãƒ¼ãƒ‰ã‚’有効ã«ã—ã¦ãã ã•ã„" member = "メンãƒãƒ¼" @@ -7350,6 +8025,9 @@ searchMembers = "メンãƒãƒ¼ã‚’検索..." status = "ステータス" team = "ãƒãƒ¼ãƒ " title = "メンãƒãƒ¼" +unlockAccount = "アカウントã®ãƒ­ãƒƒã‚¯ã‚’解除" +unlockUserError = "ユーザーアカウントã®ãƒ­ãƒƒã‚¯è§£é™¤ã«å¤±æ•—ã—ã¾ã—ãŸ" +unlockUserSuccess = "ユーザーアカウントã®ãƒ­ãƒƒã‚¯ã‚’解除ã—ã¾ã—ãŸ" user = "ユーザー" [workspace.people.actions] diff --git a/frontend/public/locales/ko-KR/translation.toml b/frontend/public/locales/ko-KR/translation.toml index 758f1e7728..b6ae747b55 100644 --- a/frontend/public/locales/ko-KR/translation.toml +++ b/frontend/public/locales/ko-KR/translation.toml @@ -8,6 +8,7 @@ black = "검정" blue = "파랑" bored = "기다리는 ê²ƒì´ ì§€ë£¨í•˜ì‹ ê°€ìš”?" cancel = "취소" +confirm = "확ì¸" changedCredsMessage = "ìžê²© ì¦ëª…ì´ ë³€ê²½ë˜ì—ˆìŠµë‹ˆë‹¤!" chooseFile = "íŒŒì¼ ì„ íƒ" close = "닫기" @@ -146,6 +147,7 @@ insufficientCredits = "í¬ë ˆë”§ì´ 부족합니다. í•„ìš”: {{requiredCredits}} loadingCredits = "í¬ë ˆë”§ í™•ì¸ ì¤‘..." loadingProStatus = "êµ¬ë… ìƒíƒœ í™•ì¸ ì¤‘..." noticeTopUpOrPlan = "í¬ë ˆë”§ì´ 부족합니다. 충전하거나 요금제를 업그레ì´ë“œí•˜ì„¸ìš”" +accessInvite = "초대" [account] accountSettings = "계정 설정" @@ -1427,6 +1429,34 @@ title = "처리" description = "오류를 보고하기 ì „ 처리 ìž‘ì—…ì„ ëŒ€ê¸°í•  최대 시간입니다." label = "처리 시간 제한(ì´ˆ)" +[admin.settings.storage] +description = "서버 저장소 ë° ê³µìœ  ì˜µì…˜ì„ ê´€ë¦¬í•©ë‹ˆë‹¤." +title = "íŒŒì¼ ì €ìž¥ ë° ê³µìœ " + +[admin.settings.storage.enabled] +description = "사용ìžê°€ ì„œë²„ì— íŒŒì¼ì„ 저장할 수 있ë„ë¡ í—ˆìš©í•©ë‹ˆë‹¤." +label = "서버 íŒŒì¼ ì €ìž¥ 사용" + +[admin.settings.storage.sharing.email] +description = "ì´ë©”ì¼ ì£¼ì†Œë¡œ 공유를 허용합니다." +label = "ì´ë©”ì¼ ê³µìœ  사용" +mailLink = "ë©”ì¼ ì„¤ì • 구성" +mailNote = "ë©”ì¼ êµ¬ì„±ì´ í•„ìš”í•©ë‹ˆë‹¤. " + +[admin.settings.storage.sharing.enabled] +description = "사용ìžê°€ ì €ìž¥ëœ íŒŒì¼ì„ 공유할 수 있ë„ë¡ í—ˆìš©í•©ë‹ˆë‹¤." +label = "공유 사용" + +[admin.settings.storage.sharing.links] +description = "로그ì¸ì´ 필요한 ë§í¬ë¥¼ 통한 공유를 허용합니다." +frontendUrlLink = "시스템 설정ì—서 구성" +frontendUrlNote = "프론트엔드 URLì´ í•„ìš”í•©ë‹ˆë‹¤. " +label = "공유 ë§í¬ 사용" + +[admin.settings.storage.signing.enabled] +description = "사용ìžê°€ 다중 ì°¸ì—¬ìž ë¬¸ì„œ 서명 ì„¸ì…˜ì„ ë§Œë“¤ 수 있ë„ë¡ í—ˆìš©í•©ë‹ˆë‹¤. 서버 íŒŒì¼ ì €ìž¥ ê¸°ëŠ¥ì´ í™œì„±í™”ë˜ì–´ 있어야 합니다." +label = "그룹 서명 사용(Alpha)" + [admin.settings.unsavedChanges] cancel = "ê³„ì† íŽ¸ì§‘" discard = "변경 사항 버리기" @@ -2059,7 +2089,19 @@ numbers = "숫ìž/범위: 5, 10-20" progressions = "등차 수열: 3n, 4n+1" [certSign] +allSigned = "모든 참여ìžê°€ 서명했습니다. 최종 완료할 준비가 ë˜ì—ˆìŠµë‹ˆë‹¤." +awaitingSignatures = "서명 대기 중" +signatureProgress = "{{signedCount}}/{{totalCount}} 서명" chooseCertificate = "ì¸ì¦ì„œ íŒŒì¼ ì„ íƒ" +declined = "ê±°ë¶€ë¨" +fetchFailed = "서명 ë°ì´í„°ë¥¼ 불러오지 못했습니다" +finalized = "최종 완료ë¨" +notified = "대기 중" +partialNote = "현재 서명으로 조기 최종 완료할 수 있습니다. 서명하지 ì•Šì€ ì°¸ì—¬ìžëŠ” 제외ë©ë‹ˆë‹¤." +pending = "대기 중" +readyToFinalize = "최종 완료 준비ë¨" +signed = "서명ë¨" +viewed = "열람ë¨" chooseJksFile = "JKS íŒŒì¼ ì„ íƒ" chooseP12File = "PKCS12 íŒŒì¼ ì„ íƒ" choosePfxFile = "PFX íŒŒì¼ ì„ íƒ" @@ -2082,6 +2124,7 @@ title = "ì¸ì¦ì„œ 서명" invisible = "ë³´ì´ì§€ 않ìŒ" stepTitle = "서명 표시" visible = "ë³´ìž„" +visibility = "표시 여부" [certSign.appearance.options] title = "서명 세부정보" @@ -2188,6 +2231,252 @@ bullet4 = "ê²€ì¦ì— ì‚¬ìš©ìž ì§€ì • ì¸ì¦ì„œë¥¼ 사용할 수 있ìŒ" text = "ì„œëª…ì„ ê²€ì‚¬í•˜ë©´, 유효한지, 누가 언제 서명했는지, 서명 ì´í›„ 문서가 변경ë˜ì—ˆëŠ”ì§€ë¥¼ 알려ì¤ë‹ˆë‹¤." title = "서명 확ì¸" +[certSign.collab.finalize] +button = "최종 완료 후 ì„œëª…ëœ PDF 로드" +early = "현재 서명으로 최종 완료" + +[certSign.collab.sessionDetail] +addButton = "ì°¸ì—¬ìž ì¶”ê°€" +addParticipants = "ì°¸ì—¬ìž ì¶”ê°€" +addParticipantsError = "ì°¸ì—¬ìž ì¶”ê°€ì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤" +backToList = "세션 목ë¡ìœ¼ë¡œ ëŒì•„가기" +deleteConfirm = "ì •ë§ë¡œ 진행하시겠습니까? ì´ ìž‘ì—…ì€ ë˜ëŒë¦´ 수 없습니다." +deleteError = "세션 ì‚­ì œì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤" +deleted = "ì„¸ì…˜ì´ ì‚­ì œë˜ì—ˆìŠµë‹ˆë‹¤" +deleteSession = "세션 ì‚­ì œ" +dueDate = "마ê°ì¼" +finalizeError = "세션 최종 ì™„ë£Œì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤" +loadPdfError = "ì„œëª…ëœ PDF를 불러오지 못했습니다" +loadSignedPdf = "ì„œëª…ëœ PDF를 활성 파ì¼ë¡œ 로드" +messageLabel = "메시지" +noAdditionalInfo = "추가 ì •ë³´ ì—†ìŒ" +owner = "소유ìž" +participantRemoved = "참여ìžê°€ 제거ë˜ì—ˆìŠµë‹ˆë‹¤" +participants = "참여ìž" +participantsAdded = "참여ìžê°€ 성공ì ìœ¼ë¡œ 추가ë˜ì—ˆìŠµë‹ˆë‹¤" +removeParticipant = "제거" +removeParticipantError = "ì°¸ì—¬ìž ì œê±°ì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤" +selectUsers = "ì‚¬ìš©ìž ì„ íƒ..." +sessionInfo = "세션 ì •ë³´" +workbenchTitle = "세션 관리" + +[certSign.collab.signRequest] +addedToFiles = "문서가 활성 파ì¼ì— 추가ë˜ì—ˆìŠµë‹ˆë‹¤" +addSignature = "서명 추가" +addToFiles = "활성 파ì¼ì— 추가" +advancedSettings = "고급 설정" +backToList = "서명 요청 목ë¡ìœ¼ë¡œ ëŒì•„가기" +certificateChoice = "ì„œëª…ì— ì‚¬ìš©í•  ì¸ì¦ì„œë¥¼ ì„ íƒí•˜ì„¸ìš”" +changeSignature = "서명 변경" +clearSignature = "서명 지우기" +completeAndSign = "완료 ë° ì„œëª…" +createNewSignature = "새 서명 만들기" +declineButton = "ê±°ë¶€" +decline = "요청 ê±°ë¶€" +deleteSelected = "ì„ íƒí•œ 서명 ì‚­ì œ" +drawSignature = "ì•„ëž˜ì— ì„œëª…ì„ ê·¸ë¦¬ì„¸ìš”" +dueDate = "마ê°ì¼" +fileTooLarge = "íŒŒì¼ í¬ê¸°ëŠ” 5MB 미만ì´ì–´ì•¼ 합니다" +fontFamily = "글꼴" +fontSize = "글꼴 í¬ê¸°: {{size}}px" +fontSizePlaceholder = "í¬ê¸°" +from = "보낸 사람" +invalidCertFile = "P12 ë˜ëŠ” PFX ì¸ì¦ì„œ 파ì¼ì„ ì„ íƒí•˜ì„¸ìš”" +invalidFileType = "ì´ë¯¸ì§€ 파ì¼ì„ ì„ íƒí•˜ì„¸ìš”" +location = "위치(ì„ íƒ ì‚¬í•­)" +locationPlaceholder = "ì–´ë””ì—서 서명하나요?" +message = "메시지" +noCertificate = "ì¸ì¦ì„œ 파ì¼ì„ ì„ íƒí•˜ì„¸ìš”" +noSignatures = "PDFì— ìµœì†Œ 한 ê°œ ì´ìƒì˜ ì„œëª…ì„ ë°°ì¹˜í•˜ì„¸ìš”" +p12File = "P12/PFX ì¸ì¦ì„œ 파ì¼" +password = "ì¸ì¦ì„œ 비밀번호" +passwordPlaceholder = "비밀번호 ìž…ë ¥..." +penColor = "펜 색ìƒ" +penSize = "펜 í¬ê¸°: {{size}}px" +placementActive = "í´ë¦­í•˜ì—¬ PDFì— ë°°ì¹˜" +placeSignatureButton = "PDFì— ì„œëª… 배치" +reason = "사유(ì„ íƒ ì‚¬í•­)" +reasonPlaceholder = "왜 서명하나요?" +removeImage = "ì´ë¯¸ì§€ 제거" +removeCertFile = "íŒŒì¼ ì œê±°" +savedSignatures = "ì €ìž¥ëœ ì„œëª…" +selectFile = "ì´ë¯¸ì§€ íŒŒì¼ ì„ íƒ" +selectSignatureTitle = "서명 ì„ íƒ ë˜ëŠ” ìƒì„±" +signButton = "문서 서명" +signatureInfo = "ì´ ì„¤ì •ì€ ë¬¸ì„œ 소유ìžê°€ 구성했습니다" +signaturePlaced = "ì„œëª…ì´ íŽ˜ì´ì§€ì— 배치ë˜ì—ˆìŠµë‹ˆë‹¤" +signatureSettings = "서명 설정" +signatureText = "서명 í…스트" +signatureTextPlaceholder = "ì´ë¦„ì„ ìž…ë ¥í•˜ì„¸ìš”..." +signatureTypeLabel = "서명 유형" +signingTitle = "서명 중" +textColor = "í…스트 색ìƒ" +typeSignature = "ì´ë¦„ì„ ìž…ë ¥í•˜ì—¬ ì„œëª…ì„ ë§Œë“œì„¸ìš”" +uploadCert = "ì‚¬ìš©ìž ì§€ì • ì¸ì¦ì„œ" +uploadCertDesc = "본ì¸ì˜ P12/PFX ì¸ì¦ì„œ 사용" +uploadSignature = "서명 ì´ë¯¸ì§€ë¥¼ 업로드" +usePersonalCert = "ê°œì¸ ì¸ì¦ì„œ" +usePersonalCertDesc = "ê·€í•˜ì˜ ê³„ì •ì— ëŒ€í•´ ìžë™ ìƒì„±ë¨" +useServerCert = "ì¡°ì§ ì¸ì¦ì„œ" +useServerCertDesc = "공유 ì¡°ì§ ì¸ì¦ì„œ" +workbenchTitle = "서명 요청" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "ì„  ìƒ‰ìƒ ì„ íƒ" +continue = "계ì†" + +[certSign.collab.signRequest.certModal] +description = "ì„œëª…ì„ {{count}}ê°œ 배치했습니다. ì„œëª…ì„ ì™„ë£Œí•  ì¸ì¦ì„œë¥¼ ì„ íƒí•˜ì„¸ìš”." +sign = "문서 서명" +certValidating = "ì¸ì¦ì„œ ê²€ì¦ ì¤‘..." +certValidUntil = "ì¸ì¦ì„œ 유효 기간: {{date}}까지" +certInvalid = "ì¸ì¦ì„œê°€ 유효하지 않ìŒ: {{error}}" +certInvalidFallback = "유효하지 ì•Šì€ ì¸ì¦ì„œ" +certNetworkError = "ì¸ì¦ì„œë¥¼ ê²€ì¦í•  수 없습니다" +title = "ì¸ì¦ì„œ 구성" + +[certSign.collab.signRequest.image] +hint = "서명 ì´ë¯¸ì§€ì˜ PNG ë˜ëŠ” JPG를 업로드하세요" + +[certSign.collab.signRequest.mode] +move = "서명 ì´ë™" +place = "서명 배치" +title = "서명 ë˜ëŠ” ì´ë™ 모드" + +[certSign.collab.signRequest.modeTabs] +draw = "그리기" +image = "업로드" +text = "ìž…ë ¥" + +[certSign.collab.signRequest.placeSignature] +message = "PDF를 í´ë¦­í•˜ì—¬ ì„œëª…ì„ ë°°ì¹˜í•˜ì„¸ìš”" +title = "서명 배치" + +[certSign.collab.signRequest.preview] +imageAlt = "ì„ íƒí•œ 서명" +missing = "미리보기 ì—†ìŒ" +textFallback = "서명" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "서명 그리기" +defaultImageLabel = "업로드한 서명" +defaultLabel = "서명" +defaultTextLabel = "입력한 서명" +delete = "서명 ì‚­ì œ" +none = "ì €ìž¥ëœ ì„œëª…ì´ ì—†ìŠµë‹ˆë‹¤" + +[certSign.collab.signRequest.signatureType] +draw = "그리기" +type = "ìž…ë ¥" +upload = "업로드" + +[certSign.collab.signRequest.steps] +back = "뒤로" +cancelPlacement = "배치 취소" +certificate = "ì¸ì¦ì„œ" +clickMultipleTimes = "ì„œëª…ì„ ë°°ì¹˜í•˜ë ¤ë©´ PDF를 여러 번 í´ë¦­í•˜ì„¸ìš”. ì„œëª…ì„ ë“œëž˜ê·¸í•˜ì—¬ ì´ë™í•˜ê±°ë‚˜ í¬ê¸°ë¥¼ 조절할 수 있습니다." +clickToPlace = "ì„œëª…ì´ í‘œì‹œë  ìœ„ì¹˜ë¥¼ PDFì—서 í´ë¦­í•˜ì„¸ìš”." +continue = "ì¸ì¦ì„œ ì„ íƒìœ¼ë¡œ 계ì†" +continueToPlacement = "배치로 계ì†" +continueToReview = "검토로 계ì†" +createSignature = "서명 만들기" +invisible = "ë³´ì´ì§€ 않ìŒ" +location = "위치:" +multipleSignatures = "PDFì— ì„œëª… {{count}}개가 ì ìš©ë©ë‹ˆë‹¤" +oneSignature = "PDFì— ì„œëª… 1개가 ì ìš©ë©ë‹ˆë‹¤" +placeOnPdf = "PDFì— ë°°ì¹˜" +reason = "사유:" +reviewTitle = "서명 ì „ 검토" +signaturePlaced = "{{page}}페ì´ì§€ì— ì„œëª…ì„ ë°°ì¹˜í–ˆìŠµë‹ˆë‹¤. 다시 í´ë¦­í•˜ì—¬ 위치를 조정하거나 검토를 계ì†í•  수 있습니다." +visible = "ë³´ìž„" +visibility = "표시 여부:" +yourSignatures = "ë‚´ 서명({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "색ìƒ" +fontLabel = "글꼴" +fontSizeLabel = "í¬ê¸°" +fontSizePlaceholder = "16" +label = "서명 í…스트" +modalHint = "ì´ë¦„ì„ ìž…ë ¥í•œ 후 '계ì†'ì„ í´ë¦­í•˜ì—¬ PDFì— ë°°ì¹˜í•˜ì„¸ìš”." +placeholder = "ì´ë¦„ì„ ìž…ë ¥í•˜ì„¸ìš”..." + +[certSign.collab.participant] +certValidating = "ì¸ì¦ì„œ ê²€ì¦ ì¤‘..." +certValid = "✓ ì¸ì¦ì„œ 유효함" +certValidUntil = " {{date}}까지" +certInvalid = "✗ {{error}}" +certInvalidFallback = "유효하지 ì•Šì€ ì¸ì¦ì„œ" +certNetworkError = "ì¸ì¦ì„œë¥¼ ê²€ì¦í•  수 없습니다" + +[certSign.collab.addParticipants] +add = "ì°¸ì—¬ìž {{count}}명 추가" +back = "뒤로" +configureSignatures = "서명 설정 구성" +continue = "서명 설정으로 계ì†" +reasonHelp = "ì´ ì°¸ì—¬ìžë“¤ì„ 위한 서명 사유를 미리 설정합니다(ì„ íƒ ì‚¬í•­, 서명 시 변경 가능)" +reasonPlaceholder = "예: 승ì¸, 검토..." +selectUsers = "ì‚¬ìš©ìž ì„ íƒ" + +[certSign.collab.sessionCreation] +includeSummaryPage = "서명 요약 페ì´ì§€ í¬í•¨" +includeSummaryPageHelp = "모든 서명 메타ë°ì´í„°ê°€ í¬í•¨ëœ 요약 페ì´ì§€ê°€ ë§ˆì§€ë§‰ì— ì¶”ê°€ë©ë‹ˆë‹¤. 개별 페ì´ì§€ì˜ 디지털 ì¸ì¦ì„œ 서명 ìƒìžëŠ” 숨겨집니다(ìží•„ 서명ì—는 ì˜í–¥ ì—†ìŒ)." + +[certSign.collab.sessionList] +active = "ì§„í–‰ 중" +finalized = "최종 완료ë¨" + +[certSign.collab.signatureSettings] +description = "모든 참여ìžì— 대한 서명 표시 ë°©ì‹ì„ 구성합니다" +title = "서명 모양" + +[certSign.collab.userSelector] +inviteUsers = "ì‚¬ìš©ìž ì¶”ê°€" +loadError = "사용ìžë¥¼ 불러오지 못했습니다" +noTeam = "팀 ì—†ìŒ" +noUsers = "다른 사용ìžë¥¼ ì°¾ì„ ìˆ˜ 없습니다." +placeholder = "ì‚¬ìš©ìž ì„ íƒ..." + +[certSign.mobile] +panelActions = "작업" +panelDocument = "문서" +panelPeople = "사람" + +[certSign.sessions] +deleted = "ì„¸ì…˜ì´ ì‚­ì œë˜ì—ˆìŠµë‹ˆë‹¤" +fetchFailed = "세션 세부 정보를 불러오지 못했습니다" +finalized = "ì„¸ì…˜ì´ ìµœì¢… 완료ë˜ì—ˆìŠµë‹ˆë‹¤" +loaded = "ì„œëª…ëœ PDFê°€ 로드ë˜ì—ˆìŠµë‹ˆë‹¤" +pdfNotReady = "PDF 준비ë˜ì§€ 않ìŒ" +pdfNotReadyDesc = "ì„œëª…ëœ PDF를 ìƒì„±í•˜ëŠ” 중입니다. 잠시 후 다시 시ë„하세요." + +[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 업로드" + [changeCreds] changePassword = "기본 ë¡œê·¸ì¸ ìžê²© ì¦ëª…ì„ ì‚¬ìš© 중입니다. 새 비밀번호를 입력하세요" changeUsername = "ì‚¬ìš©ìž ì´ë¦„ì„ ì—…ë°ì´íŠ¸í•©ë‹ˆë‹¤. ì—…ë°ì´íЏ 후 로그아웃ë©ë‹ˆë‹¤." @@ -3242,6 +3531,46 @@ totalSelected = "ì´ ì„ íƒ" unsupported = "ì§€ì›ë˜ì§€ 않ìŒ" unzip = "ì••ì¶• í•´ì œ" uploadError = "ì¼ë¶€ 파ì¼ì„ 업로드하지 못했습니다." +copyCreated = "ë³µì‚¬ë³¸ì´ ì´ ê¸°ê¸°ì— ì €ìž¥ë˜ì—ˆìŠµë‹ˆë‹¤." +copyFailed = "ë³µì‚¬ë³¸ì„ ìƒì„±í•  수 없습니다." +leaveShare = "ë‚´ 목ë¡ì—서 제거" +leaveShareFailed = "ê³µìœ ëœ íŒŒì¼ì„ 제거할 수 없습니다." +leaveShareSuccess = "공유 목ë¡ì—서 제거ë˜ì—ˆìŠµë‹ˆë‹¤." +removeBoth = "둘 다ì—서 제거" +removeFilePrompt = "ì´ íŒŒì¼ì€ ì´ ê¸°ê¸°ì™€ ì„œë²„ì— ëª¨ë‘ ì €ìž¥ë˜ì–´ 있습니다. ì–´ë””ì—서 제거하시겠습니까?" +removeFileTitle = "íŒŒì¼ ì œê±°" +removeLocalOnly = "ì´ ê¸°ê¸°ë§Œ" +removeServerFailed = "서버ì—서 파ì¼ì„ 제거할 수 없습니다." +removeServerOnly = "서버만" +removeServerOnlyPrompt = "ì´ íŒŒì¼ì€ 서버ì—ë§Œ 저장ë˜ì–´ 있습니다. 서버ì—서 제거하시겠습니까?" +removeServerSuccess = "서버ì—서 제거ë¨." +removeSharedPrompt = "ì´ íŒŒì¼ì€ 귀하와 공유ë˜ì—ˆìŠµë‹ˆë‹¤. ì´ ê¸°ê¸°ì—서 제거하거나 공유 목ë¡ì—서 제거할 수 있습니다." +removeSharedServerOnlyBlockedPrompt = "ì´ íŒŒì¼ì€ 귀하와 공유ë˜ì—ˆìœ¼ë©° 서버ì—ë§Œ 저장ë˜ì–´ 있습니다." +removeSharedServerOnlyPrompt = "ì´ íŒŒì¼ì€ 귀하와 공유ë˜ì—ˆìœ¼ë©° 서버ì—ë§Œ 저장ë˜ì–´ 있습니다. 목ë¡ì—서 제거하시겠습니까?" +changesNotUploaded = "변경 ì‚¬í•­ì´ ì—…ë¡œë“œë˜ì§€ 않ìŒ" +cloudFile = "í´ë¼ìš°ë“œ 파ì¼" +filterAll = "모ë‘" +filterLocal = "로컬" +filterSharedByMe = "ë‚´ê°€ 공유함" +filterSharedWithMe = "나와 공유ë¨" +lastSynced = "마지막 ë™ê¸°í™”" +localOnly = "로컬 ì „ìš©" +makeCopy = "복사본 만들기" +owner = "소유ìž" +ownerUnknown = "알 수 ì—†ìŒ" +share = "공유" +shareSelected = "ì„ íƒ í•­ëª© 공유" +sharedByYou = "ë‚´ê°€ 공유함" +sharedEditNoticeBody = "서버 ë²„ì „ì˜ ì´ íŒŒì¼ì— 대한 편집 ê¶Œí•œì´ ì—†ìŠµë‹ˆë‹¤. 편집 ë‚´ìš©ì€ ë¡œì»¬ 복사본으로 저장ë©ë‹ˆë‹¤." +sharedEditNoticeConfirm = "알겠습니다" +sharedEditNoticeTitle = "ì½ê¸° ì „ìš© 서버 사본" +sharedWithYou = "나와 공유ë¨" +sharing = "공유" +storageState = "저장소" +synced = "ë™ê¸°í™”ë¨" +updateOnServer = "ì„œë²„ì— ì—…ë°ì´íЏ" +uploadSelected = "ì„ íƒ í•­ëª© 업로드" +uploadToServer = "ì„œë²„ì— ì—…ë¡œë“œ" [files] addFiles = "íŒŒì¼ ì¶”ê°€" @@ -3367,6 +3696,77 @@ title = "PDF í‰íƒ„í™” 안내" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "그룹 서명 ì •ë³´" + +[groupSigning.tooltip.finalization] +bullet1 = "모든 ì„œëª…ì€ ì§€ì •í•œ ì°¸ì—¬ìž ìˆœì„œëŒ€ë¡œ ì ìš©ë©ë‹ˆë‹¤" +bullet2 = "í•„ìš” 시 ì¼ë¶€ ì„œëª…ë§Œìœ¼ë¡œë„ ìµœì¢… 완료할 수 있습니다" +bullet3 = "최종 완료 후ì—는 ì„¸ì…˜ì„ ìˆ˜ì •í•  수 없습니다" +description = "모든 참여ìžê°€ 서명하면(ë˜ëŠ” 조기 최종 완료를 ì„ íƒí•˜ë©´) 최종 ì„œëª…ëœ PDF를 ìƒì„±í•  수 있습니다." +title = "최종 완료 프로세스" + +[groupSigning.tooltip.roles] +bullet1 = "소유ìž(본ì¸): 세션 ìƒì„±, 서명 기본값 구성, 문서 최종 완료" +bullet2 = "참여ìž: ìžì‹ ì˜ 서명 ìƒì„±, ì¸ì¦ì„œ ì„ íƒ, PDFì— ë°°ì¹˜" +bullet3 = "참여ìžëŠ” ì„œëª…ì˜ í‘œì‹œ 여부, 사유, 위치 ì„¤ì •ì„ ë³€ê²½í•  수 없습니다" +description = "모든 참여ìžì— 대한 서명 표시 ì„¤ì •ì„ ì œì–´í•©ë‹ˆë‹¤." +title = "ì°¸ì—¬ìž ì—­í• " + +[groupSigning.tooltip.sequential] +bullet1 = "첫 번째 참여ìžê°€ 서명해야 ë‘ ë²ˆì§¸ 참여ìžê°€ ë¬¸ì„œì— ì ‘ê·¼í•  수 있습니다" +bullet2 = "ë²•ì  ì¤€ìˆ˜ë¥¼ 위한 올바른 서명 순서를 보장합니다" +bullet3 = "목ë¡ì—서 드래그하여 ì°¸ì—¬ìž ìˆœì„œë¥¼ 변경할 수 있습니다" +description = "지정한 순서대로 참여ìžê°€ ë¬¸ì„œì— ì„œëª…í•©ë‹ˆë‹¤. ê° ì„œëª…ìžëŠ” ìžì‹ ì˜ 차례가 ë˜ë©´ ì•Œë¦¼ì„ ë°›ìŠµë‹ˆë‹¤." +title = "순차 서명" + +[groupSigning.steps] +back = "뒤로" +completed = "완료ë¨" +current = "현재" +stepLabel = "단계 {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "검토로 계ì†" +invisible = "ì„œëª…ì€ ë³´ì´ì§€ 않습니다(메타ë°ì´í„°ë§Œ)" +locationLabel = "위치:" +preview = "미리보기" +reasonLabel = "사유:" +title = "서명 설정 구성" +visible = "ì„œëª…ì€ íŽ˜ì´ì§€ {{page}}ì— í‘œì‹œë©ë‹ˆë‹¤" + +[groupSigning.steps.review] +document = "문서" +dueDate = "마ê°ì¼(ì„ íƒ ì‚¬í•­)" +dueDatePlaceholder = "마ê°ì¼ ì„ íƒ..." +invisible = "ë³´ì´ì§€ 않ìŒ(메타ë°ì´í„°ë§Œ)" +location = "위치:" +logo = "로고:" +logoHidden = "로고 ì—†ìŒ" +logoShown = "Stirling PDF 로고 표시ë¨" +participants = "참여ìž" +reason = "사유:" +send = "서명 요청 보내기" +signatureSettings = "서명 설정" +title = "세션 세부 ì •ë³´ 검토" +titleShort = "검토 ë° ë³´ë‚´ê¸°" +visibility = "표시 여부:" +visible = "페ì´ì§€ {{page}}ì— í‘œì‹œ" +participantCount = "ì°¸ì—¬ìž {{count}}ëª…ì´ ìˆœì„œëŒ€ë¡œ 서명합니다" + +[groupSigning.steps.selectDocument] +continue = "ì°¸ì—¬ìž ì„ íƒìœ¼ë¡œ 계ì†" +noFile = "서명 ì„¸ì…˜ì„ ë§Œë“¤ë ¤ë©´ 활성 파ì¼ì—서 ë‹¨ì¼ PDF 파ì¼ì„ ì„ íƒí•˜ì„¸ìš”." +selectedFile = "ì„ íƒí•œ 문서" +title = "문서 ì„ íƒ" + +[groupSigning.steps.selectParticipants] +continue = "서명 설정으로 계ì†" +count = "ì°¸ì—¬ìž {{count}}명 ì„ íƒë¨" +label = "ì°¸ì—¬ìž ì„ íƒ" +placeholder = "서명할 ì°¸ì—¬ìž ì„ íƒ..." +title = "ì°¸ì—¬ìž ì„ íƒ" + [getPdfInfo] downloadJson = "JSON 다운로드" downloads = "다운로드" @@ -4460,7 +4860,10 @@ zoomOut = "축소" [viewer] cannotPreviewFile = "파ì¼ì„ 미리보기할 수 없습니다" +disableColorFilter = "ìƒ‰ìƒ í•„í„° 비활성화" dualPageView = "ë‘ íŽ˜ì´ì§€ 보기" +enableDarkFilter = "ë‹¤í¬ í•„í„° 활성화" +enableSepiaFilter = "세피아 í•„í„° 활성화" firstPage = "첫 페ì´ì§€" lastPage = "마지막 페ì´ì§€" nextPage = "ë‹¤ìŒ íŽ˜ì´ì§€" @@ -4470,6 +4873,22 @@ singlePageView = "ë‹¨ì¼ íŽ˜ì´ì§€ 보기" unknownFile = "알 수 없는 파ì¼" zoomIn = "확대" zoomOut = "축소" +resetZoom = "줌 재설정" + +[viewer.nonPdf] +fileTypeBadge = "{{type}} 파ì¼" +convertToPdf = "PDF로 변환" +loading = "로딩 중..." +emptyFile = "빈 파ì¼" +csvStats = "{{rows}}í–‰ · {{columns}}ì—´ · {{size}}" +sortedBy = "ì •ë ¬ 기준: {{column}}" +columnDefault = "ì—´ {{index}}" +htmlPreviewWarning = "HTML 미리보기 — 외부 리소스가 로드ë˜ì§€ ì•Šì„ ìˆ˜ 있습니다 · {{size}}" +htmlPreview = "HTML 미리보기" +invalidJson = "ìž˜ëª»ëœ JSON — ì›ë³¸ ë‚´ìš©ì„ í‘œì‹œí•©ë‹ˆë‹¤" +textStats = "{{lines}}줄 · {{size}}" +lineNumbers = "줄 번호" +renderMarkdown = "마í¬ë‹¤ìš´ ë Œë”ë§" [viewer.attachments] title = "첨부 파ì¼" @@ -4531,6 +4950,7 @@ toggleAttachments = "첨부 íŒŒì¼ í‘œì‹œ/숨기기" toggleTheme = "테마 전환" language = "언어" toggleAnnotations = "ì£¼ì„ ê°€ì‹œì„± 전환" +toggleLayers = "ë ˆì´ì–´ 전환" search = "PDF 검색" panMode = "ì´ë™ 모드" applyRedactionsFirst = "먼저 가리기 ì ìš©" @@ -5407,20 +5827,72 @@ title = "íŒŒì¼ ì¸ì‡„" 2 = "프린터 ì´ë¦„ ìž…ë ¥" [quickAccess] +access = "액세스" +accessAddPerson = "사람 추가" +accessBack = "뒤로" +accessCopyLink = "ë§í¬ 복사" +accessEmail = "ì´ë©”ì¼ ì£¼ì†Œ" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "파ì¼" +accessGeneral = "ì¼ë°˜ 액세스" +accessInviteTitle = "사람 초대" +accessOwner = "소유ìž" +accessPanel = "문서 액세스" +accessPeople = "액세스 ê¶Œí•œì´ ìžˆëŠ” 사람" +accessRemove = "제거" +accessRestricted = "제한ë¨" +accessRestrictedHint = "액세스 ê¶Œí•œì´ ìžˆëŠ” 사람만 ì—´ 수 있습니다" +accessRole = "ì—­í• " +accessRoleCommenter = "댓글 작성ìž" +accessRoleEditor = "편집ìž" +accessRoleViewer = "열람ìž" +accessSelectedFile = "ì„ íƒí•œ 파ì¼" +accessSendInvite = "초대 전송" +accessTitle = "문서 액세스" +accessYou = "나" account = "계정" +activeSessions = "ì§„í–‰ 중 세션" +activeTab = "ì§„í–‰ 중" activity = "활ë™" adminSettings = "ê´€ë¦¬ìž ì„¤ì •" +allSessions = "모든 세션" allTools = "All Tools" automate = "ìžë™í™”" +back = "뒤로" +certSign = "ì¸ì¦ì„œ 서명" +completedSessions = "ì™„ë£Œëœ ì„¸ì…˜" +completedTab = "완료ë¨" config = "구성" +createNew = "새 요청 만들기" +createSession = "서명 요청 ìƒì„±" +dueDate = "마ê°ì¼(ì„ íƒ ì‚¬í•­)" files = "파ì¼" help = "ë„움ë§" +noActiveSessions = "보류 ì¤‘ì¸ ì„œëª… 요청ì´ë‚˜ ì§„í–‰ 중 ì„¸ì…˜ì´ ì—†ìŠµë‹ˆë‹¤" +noCompletedSessions = "ì™„ë£Œëœ ì„¸ì…˜ì´ ì—†ìŠµë‹ˆë‹¤" +noFile = "파ì¼ì´ ì„ íƒë˜ì§€ 않았습니다" read = "ì½ê¸°" reader = "리ë”" +refresh = "새로 고침" +requestSignatures = "서명 요청" +selectSingleFileToRequest = "ì„œëª…ì„ ìš”ì²­í•˜ë ¤ë©´ ë‹¨ì¼ PDF 파ì¼ì„ ì„ íƒí•˜ì„¸ìš”" +selectedFile = "ì„ íƒí•œ 파ì¼" +selectUsers = "서명할 ì‚¬ìš©ìž ì„ íƒ" +selectUsersPlaceholder = "ì°¸ì—¬ìž ì„ íƒ..." +sendingRequest = "전송 중..." settings = "설정" showMeAround = "둘러보기 시작" sign = "서명" +signatureRequests = "서명 요청" +signYourself = "ì§ì ‘ 서명" +newRequest = "새 요청" tours = "둘러보기" +wetSign = "서명 추가" +filterMine = "ë‚´ 것" +filterOverdue = "기한 초과" +filterSigned = "서명ë¨" +filterDeclined = "ê±°ë¶€ë¨" +searchDocuments = "문서 검색…" [quickAccess.helpMenu] adminTour = "ê´€ë¦¬ìž ë‘˜ëŸ¬ë³´ê¸°" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Stirling-PDF 서버가 오프ë¼ì¸ì´ë©° 로컬 ë°± expired = "ì„¸ì…˜ì´ ë§Œë£Œë˜ì—ˆìŠµë‹ˆë‹¤. 페ì´ì§€ë¥¼ 새로 고침하고 다시 시ë„하세요." refreshPage = "페ì´ì§€ 새로 고침" +[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 = "서명 순서" + +[signatureSettings.tooltip] +header = "서명 모양 설정" + +[signatureSettings.tooltip.location] +bullet1 = "예: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "페ì´ì§€ 위치와는 다릅니다" +bullet3 = "ì¼ë¶€ ë²•ì  ê´€í•  구역ì—서 필요할 수 있습니다" +description = "ì„œëª…ì´ ì ìš©ëœ ì§€ë¦¬ì  ìœ„ì¹˜(ì„ íƒ ì‚¬í•­). ì¸ì¦ì„œ 메타ë°ì´í„°ì— 저장ë©ë‹ˆë‹¤." +title = "서명 위치" + +[signatureSettings.tooltip.logo] +bullet1 = "서명 ë° í…스트와 함께 표시" +bullet2 = "PNG, JPG í˜•ì‹ ì§€ì›" +bullet3 = "전문ì ì¸ ì¸ìƒì„ í–¥ìƒ" +description = "브랜딩과 ì‹ ë¢°ì„±ì„ ìœ„í•´ ë³´ì´ëŠ” ì„œëª…ì— íšŒì‚¬ 로고를 추가합니다." +title = "회사 로고" + +[signatureSettings.tooltip.reason] +bullet1 = "예: \"Approval\", \"Contract Agreement\", \"Review Complete\"" +bullet2 = "PDF 서명 ì†ì„±ì— 표시" +bullet3 = "ê°ì‚¬ ì¶”ì ê³¼ 컴플ë¼ì´ì–¸ìŠ¤ì— ìœ ìš©" +description = "ë¬¸ì„œì— ì„œëª…í•˜ëŠ” ì´ìœ ë¥¼ 설명하는 ì„ íƒì  í…스트입니다. ì¸ì¦ì„œ 메타ë°ì´í„°ì— 저장ë©ë‹ˆë‹¤." +title = "서명 사유" + +[signatureSettings.tooltip.visibility] +bullet1 = "ë³´ìž„: ì‚¬ìš©ìž ì§€ì • 모양으로 PDFì— ì„œëª…ì´ í‘œì‹œë©ë‹ˆë‹¤" +bullet2 = "ë³´ì´ì§€ 않ìŒ: 시ê°ì  표시 ì—†ì´ ì¸ì¦ì„œë§Œ 임베드ë¨" +bullet3 = "ë³´ì´ì§€ 않는 ì„œëª…ë„ ì•”í˜¸í•™ì  ê²€ì¦ì„ 제공합니다" +description = "ë¬¸ì„œì— ì„œëª…ì„ ë³´ì´ê²Œ í• ì§€, ë³´ì´ì§€ 않게 임베드할지 제어합니다." +title = "서명 표시 여부" + [settings.configuration] advanced = "고급" database = "ë°ì´í„°ë² ì´ìФ" endpoints = "엔드í¬ì¸íЏ" features = "기능" +storageSharing = "íŒŒì¼ ì €ìž¥ ë° ê³µìœ " systemSettings = "시스템 설정" title = "구성" @@ -6332,10 +6868,13 @@ title = "Stirlingì— ë¡œê·¸ì¸" [setup.selfhosted] link = "ë˜ëŠ” 셀프 호스팅 ê³„ì •ì— ì—°ê²°" subtitle = "서버 ìžê²© ì¦ëª…ì„ ìž…ë ¥í•˜ì„¸ìš”" +changeServerLocked = "ì¡°ì§ì—서 ì´ ì•±ì„ íŠ¹ì • 서버로 제한했습니다" switchToLocal = "대신 로컬 ë„구 사용" title = "ì„œë²„ì— ë¡œê·¸ì¸" [setup.selfhosted.unreachable] +changeServer = "다른 ì„œë²„ì— ì—°ê²°" +changeServerLocked = "ì¡°ì§ì—서 ì´ ì•±ì„ íŠ¹ì • 서버로 제한했습니다" continueOffline = "대신 로컬 ë„구 사용" message = "{{url}}ì— ì ‘ì†í•  수 없습니다. 서버가 실행 중ì´ê³  액세스 가능한지 확ì¸í•˜ì„¸ìš”." retry = "재시ë„" @@ -6529,6 +7068,15 @@ saved = "저장ë¨" text = "í…스트" title = "서명 유형" +[signRequest] +declined = "서명 ìš”ì²­ì´ ê±°ë¶€ë˜ì—ˆìŠµë‹ˆë‹¤" +fetchFailed = "서명 ìš”ì²­ì„ ë¶ˆëŸ¬ì˜¤ì§€ 못했습니다" +signed = "문서가 성공ì ìœ¼ë¡œ 서명ë˜ì—ˆìŠµë‹ˆë‹¤" + +[signSession] +createFailed = "서명 요청 ìƒì„±ì— 실패했습니다" +created = "서명 ìš”ì²­ì„ ì „ì†¡í–ˆìŠµë‹ˆë‹¤" + [signup] accountCreatedSuccessfully = "ê³„ì •ì´ ì„±ê³µì ìœ¼ë¡œ ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤! ì´ì œ 로그ì¸í•  수 있습니다." alreadyHaveAccount = "ì´ë¯¸ ê³„ì •ì´ ìžˆìœ¼ì‹ ê°€ìš”? 로그ì¸" @@ -6807,6 +7355,106 @@ title = "챕터별 PDF ë¶„í• " [splitPdfByChapters] tags = "ë¶„í• ,챕터,ë¶ë§ˆí¬,정리" +[storageShare] +accessed = "액세스함" +accessDenied = "ì´ ê³µìœ  파ì¼ì— 액세스할 수 없습니다. 소유ìžì—게 공유를 요청하세요." +accessFailed = "활ë™ì„ 불러올 수 없습니다." +accessDeniedBody = "ì´ íŒŒì¼ì— 액세스할 수 없습니다. 소유ìžì—게 공유를 요청하세요." +accessDeniedTitle = "액세스 ì—†ìŒ" +accessLimitedCommenter = "댓글 액세스는 ê³§ ì œê³µë  ì˜ˆì •ìž…ë‹ˆë‹¤. 다운로드가 필요하면 소유ìžì—게 íŽ¸ì§‘ìž ê¶Œí•œì„ ìš”ì²­í•˜ì„¸ìš”." +accessLimitedTitle = "ì œí•œëœ ì•¡ì„¸ìŠ¤" +accessLimitedViewer = "ì´ ë§í¬ëŠ” 보기 전용입니다. 다운로드가 필요하면 소유ìžì—게 íŽ¸ì§‘ìž ê¶Œí•œì„ ìš”ì²­í•˜ì„¸ìš”." +createdAt = "ìƒì„±ë¨" +download = "다운로드" +downloadFailed = "ì´ íŒŒì¼ì„ 다운로드할 수 없습니다." +expiredBody = "ì´ ê³µìœ  ë§í¬ê°€ 유효하지 않거나 만료ë˜ì—ˆìŠµë‹ˆë‹¤." +expiredTitle = "ë§í¬ 만료ë¨" +goToLogin = "로그ì¸ìœ¼ë¡œ ì´ë™" +loadFailed = "ê³µìœ ëœ íŒŒì¼ì„ ì—´ 수 없습니다." +loading = "공유 ë§í¬ë¥¼ 불러오는 중..." +loginPrompt = "ì´ ê³µìœ  파ì¼ì— 액세스하려면 로그ì¸í•˜ì„¸ìš”." +loginRequired = "ë¡œê·¸ì¸ í•„ìš”" +openInApp = "Stirling PDFì—서 열기" +ownerLabel = "소유ìž" +ownerUnknown = "알 수 ì—†ìŒ" +requiresLogin = "ì´ ê³µìœ  파ì¼ì€ 로그ì¸ì´ 필요합니다." +roleCommenter = "댓글 작성ìž" +roleEditor = "편집ìž" +roleViewer = "열람ìž" +shareHeading = "ê³µìœ ëœ íŒŒì¼" +titleDefault = "ê³µìœ ëœ íŒŒì¼" +tryAgain = "ë‚˜ì¤‘ì— ë‹¤ì‹œ 시ë„하세요." +addUser = "추가" +commenterHint = "댓글 ê¸°ëŠ¥ì€ ê³§ ì œê³µë  ì˜ˆì •ìž…ë‹ˆë‹¤." +copied = "ë§í¬ê°€ í´ë¦½ë³´ë“œì— 복사ë˜ì—ˆìŠµë‹ˆë‹¤" +copy = "복사" +copyFailed = "복사 실패" +description = "ì´ íŒŒì¼ì˜ 공유 ë§í¬ë¥¼ ìƒì„±í•©ë‹ˆë‹¤. ë§í¬ë¥¼ 가진 로그ì¸í•œ 사용ìžëŠ” 액세스할 수 있습니다." +downloadsCount = "다운로드: {{count}}" +emailWarningBody = "ì´ë©”ì¼ ì£¼ì†Œë¡œ 보입니다. ì´ ì‚¬ëžŒì´ ì•„ì§ Stirling PDF 사용ìžê°€ 아니ë¼ë©´ 파ì¼ì— 액세스할 수 없습니다." +emailWarningConfirm = "ê·¸ëž˜ë„ ê³µìœ " +emailWarningTitle = "ì´ë©”ì¼ ì£¼ì†Œ" +errorTitle = "공유 실패" +failure = "공유 ë§í¬ë¥¼ ìƒì„±í•  수 없습니다. 다시 시ë„하세요." +fileLabel = "파ì¼" +generate = "ë§í¬ ìƒì„±" +generated = "공유 ë§í¬ê°€ ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤" +hideActivity = "í™œë™ ìˆ¨ê¸°ê¸°" +invalidUsername = "유효한 ì‚¬ìš©ìž ì´ë¦„ ë˜ëŠ” ì´ë©”ì¼ ì£¼ì†Œë¥¼ 입력하세요." +lastAccessed = "마지막 액세스" +linkAccessTitle = "공유 ë§í¬ 액세스" +linkLabel = "공유 ë§í¬" +linksDisabled = "공유 ë§í¬ê°€ 비활성화ë˜ì–´ 있습니다." +linksDisabledBody = "공유 ë§í¬ëŠ” 서버 ì„¤ì •ì— ì˜í•´ 비활성화ë˜ì–´ 있습니다." +manage = "공유 관리" +manageDescription = "ì´ íŒŒì¼ì„ 공유하기 위한 ë§í¬ë¥¼ ìƒì„±í•˜ê³  관리합니다." +manageLoadFailed = "공유 ë§í¬ë¥¼ 불러올 수 없습니다." +manageTitle = "공유 관리" +noActivity = "ì•„ì§ í™œë™ ì—†ìŒ." +noLinks = "활성 공유 ë§í¬ê°€ ì•„ì§ ì—†ìŠµë‹ˆë‹¤." +noSharedUsers = "ì•„ì§ ì•¡ì„¸ìŠ¤ ê¶Œí•œì´ ë¶€ì—¬ëœ ì‚¬ìš©ìžê°€ 없습니다." +removeLink = "ë§í¬ 제거" +removeUser = "제거" +revokeFailed = "공유 ë§í¬ë¥¼ 제거할 수 없습니다." +revoked = "공유 ë§í¬ê°€ 제거ë˜ì—ˆìŠµë‹ˆë‹¤" +roleLabel = "ì—­í• " +sharingDisabled = "공유가 비활성화ë˜ì—ˆìŠµë‹ˆë‹¤." +sharingDisabledBody = "서버 ì„¤ì •ì— ì˜í•´ 공유가 비활성화ë˜ì—ˆìŠµë‹ˆë‹¤." +sharedUsersTitle = "공유 사용ìž" +title = "íŒŒì¼ ê³µìœ " +unknownUser = "알 수 없는 사용ìž" +userAddFailed = "해당 사용ìžì™€ 공유할 수 없습니다." +userAdded = "사용ìžê°€ 공유 목ë¡ì— 추가ë˜ì—ˆìŠµë‹ˆë‹¤." +usernameLabel = "ì‚¬ìš©ìž ì´ë¦„ ë˜ëŠ” ì´ë©”ì¼" +usernamePlaceholder = "ì‚¬ìš©ìž ì´ë¦„ ë˜ëŠ” ì´ë©”ì¼ì„ 입력하세요" +userRemoveFailed = "해당 사용ìžë¥¼ 제거할 수 없습니다." +userRemoved = "사용ìžê°€ 공유 목ë¡ì—서 제거ë˜ì—ˆìŠµë‹ˆë‹¤." +viewActivity = "í™œë™ ë³´ê¸°" +viewed = "조회ë¨" +viewsCount = "조회수: {{count}}" +downloaded = "다운로드ë¨" +bulkDescription = "ì„ íƒí•œ 모든 파ì¼ì„ 로그ì¸í•œ 사용ìžì™€ 공유할 수 있는 í•˜ë‚˜ì˜ ë§í¬ë¥¼ ìƒì„±í•©ë‹ˆë‹¤." +bulkTitle = "ì„ íƒí•œ íŒŒì¼ ê³µìœ " +copyLink = "공유 ë§í¬ 복사" +fileCount = "{{count}}ê°œ íŒŒì¼ ì„ íƒë¨" +ownerOnly = "소유ìžë§Œ 공유를 관리할 수 있습니다." +selectSingleFile = "공유를 관리하려면 í•˜ë‚˜ì˜ íŒŒì¼ì„ ì„ íƒí•˜ì„¸ìš”." + +[storageUpload] +description = "현재 파ì¼ì„ 서버 ì €ìž¥ì†Œì— ì—…ë¡œë“œí•˜ì—¬ 본ì¸ì´ 액세스할 수 있습니다." +errorTitle = "업로드 실패" +failure = "ì—…ë¡œë“œì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. ë¡œê·¸ì¸ ë° ì €ìž¥ì†Œ ì„¤ì •ì„ í™•ì¸í•˜ì„¸ìš”." +fileLabel = "파ì¼" +hint = "공개 ë§í¬ì™€ 액세스 모드는 서버 설정ì—서 제어ë©ë‹ˆë‹¤." +success = "ì„œë²„ì— ì—…ë¡œë“œë˜ì—ˆìŠµë‹ˆë‹¤" +title = "서버로 업로드" +updateButton = "서버ì—서 ì—…ë°ì´íЏ" +uploadButton = "서버로 업로드" +bulkDescription = "ì„ íƒí•œ 파ì¼ì„ 서버 ì €ìž¥ì†Œì— ì—…ë¡œë“œí•©ë‹ˆë‹¤." +bulkTitle = "ì„ íƒí•œ íŒŒì¼ ì—…ë¡œë“œ" +fileCount = "{{count}}ê°œ íŒŒì¼ ì„ íƒë¨" +more = " +{{count}}ê°œ ë”" + [storage] approximateSize = "대략ì ì¸ í¬ê¸°" fileTooLarge = "파ì¼ì´ 너무 í½ë‹ˆë‹¤. 파ì¼ë‹¹ 최대 í¬ê¸°:" @@ -7153,6 +7801,30 @@ title = "PDF 보기/편집" [warning] tooltipTitle = "경고" +[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 = "서명 ì´ë¯¸ì§€ 업로드" + [watermark] completed = "워터마í¬ê°€ 추가ë˜ì—ˆìŠµë‹ˆë‹¤" desc = "PDFì— í…스트 ë˜ëŠ” ì´ë¯¸ì§€ ì›Œí„°ë§ˆí¬ ì¶”ê°€" @@ -7333,6 +8005,7 @@ activeSession = "활성 세션" addMembers = "멤버 추가" admin = "관리ìž" confirmDelete = "ì´ ì‚¬ìš©ìžë¥¼ 삭제하시겠습니까? ì´ ìž‘ì—…ì€ ë˜ëŒë¦´ 수 없습니다." +confirmUnlock = "ì´ ì‚¬ìš©ìž ê³„ì •ì„ ìž ê¸ˆ 해제하시겠습니까?" deleteUser = "ì‚¬ìš©ìž ì‚­ì œ" deleteUserError = "ì‚¬ìš©ìž ì‚­ì œì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤" deleteUserSuccess = "사용ìžë¥¼ 성공ì ìœ¼ë¡œ 삭제했습니다" @@ -7341,6 +8014,8 @@ disable = "비활성화" disabled = "비활성화ë¨" editRole = "ì—­í•  편집" enable = "활성화" +locked = "ìž ê¹€" +lockedBadge = "ìž ê¹€" loading = "êµ¬ì„±ì› ë¶ˆëŸ¬ì˜¤ëŠ” 중..." loginRequired = "먼저 ë¡œê·¸ì¸ ëª¨ë“œë¥¼ 활성화하세요" member = "멤버" @@ -7350,6 +8025,9 @@ searchMembers = "멤버 검색..." status = "ìƒíƒœ" team = "팀" title = "구성ì›" +unlockAccount = "계정 잠금 í•´ì œ" +unlockUserError = "ì‚¬ìš©ìž ê³„ì • 잠금 í•´ì œì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤" +unlockUserSuccess = "ì‚¬ìš©ìž ê³„ì •ì´ ì„±ê³µì ìœ¼ë¡œ 잠금 í•´ì œë˜ì—ˆìŠµë‹ˆë‹¤" user = "사용ìž" [workspace.people.actions] diff --git a/frontend/public/locales/ml-ML/translation.toml b/frontend/public/locales/ml-ML/translation.toml index f8f5c51657..0764a8d5fa 100644 --- a/frontend/public/locales/ml-ML/translation.toml +++ b/frontend/public/locales/ml-ML/translation.toml @@ -8,6 +8,7 @@ black = "à´•à´±àµà´ªàµà´ªàµ" blue = "നീല" bored = "കാതàµà´¤à´¿à´°àµà´¨àµà´¨àµ à´®àµà´·à´¿à´žàµà´žàµ‹?" cancel = "റദàµà´¦à´¾à´•àµà´•àµà´•" +confirm = "à´¸àµà´¥à´¿à´°àµ€à´•à´°à´¿à´•àµà´•àµà´•" changedCredsMessage = "വിവരങàµà´™àµ¾ മാറàµà´±à´¿!" chooseFile = "ഫയൽ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" close = "à´…à´Ÿà´¯àµà´•àµà´•àµà´•" @@ -146,6 +147,7 @@ insufficientCredits = "à´•àµà´°àµ†à´¡à´¿à´±àµà´±àµà´•ൾ പോരാ. ആവ loadingCredits = "à´•àµà´°àµ†à´¡à´¿à´±àµà´±àµà´•ൾ പരിശോധികàµà´•àµà´¨àµà´¨àµ..." loadingProStatus = "സബàµà´¸àµà´•àµà´°à´¿à´ªàµà´·àµ» നില പരിശോധികàµà´•àµà´¨àµà´¨àµ..." noticeTopUpOrPlan = "à´•àµà´°àµ†à´¡à´¿à´±àµà´±àµà´•ൾ മതി വരàµà´¨àµà´¨à´¿à´²àµà´², ദയവായി ടോപàµà´ªàµ അപൠചെയàµà´¯àµà´• à´…à´²àµà´²àµ†à´™àµà´•ിൽ ഒരൠപàµà´²à´¾à´¨à´¿à´²àµ‡à´•àµà´•ൠഅപàµâ€Œà´—àµà´°àµ‡à´¡àµ ചെയàµà´¯àµà´•" +accessInvite = "à´•àµà´·à´£à´¿à´•àµà´•àµà´•" [account] accountSettings = "à´…à´•àµà´•ൗണàµà´Ÿàµ à´•àµà´°à´®àµ€à´•രണങàµà´™àµ¾" @@ -1427,6 +1429,34 @@ title = "à´ªàµà´°àµ‹à´¸à´¸àµà´¸à´¿à´‚à´—àµ" description = "പിശകൠറിപàµà´ªàµ‹àµ¼à´Ÿàµà´Ÿàµ ചെയàµà´¯àµà´¨àµà´¨à´¤à´¿à´¨àµ à´®àµà´®àµà´ªàµ à´ªàµà´°àµ‹à´¸à´¸àµà´¸à´¿à´‚ഗൠജോബിനായി കാതàµà´¤à´¿à´°à´¿à´•àµà´•àµà´¨àµà´¨ പരമാവധി സമയം." label = "à´ªàµà´°àµ‹à´¸à´¸àµà´¸à´¿à´‚ഗൠടൈംഔടàµà´Ÿàµ (സെകàµà´•ൻഡàµâ€Œ)" +[admin.settings.storage] +description = "സർവർ സംഭരണവàµà´‚ പങàµà´•ിടൽ à´“à´ªàµà´·à´¨àµà´•à´³àµà´‚ നിയനàµà´¤àµà´°à´¿à´•àµà´•àµà´•." +title = "ഫയൽ സംഭരണവàµà´‚ പങàµà´•à´¿à´Ÿà´²àµà´‚" + +[admin.settings.storage.enabled] +description = "ഉപയോകàµà´¤à´¾à´•àµà´•ൾകàµà´•ൠസെർവറിൽ ഫയലàµà´•ൾ സംഭരികàµà´•ാൻ à´…à´¨àµà´µà´¦à´¿à´•àµà´•àµà´•." +label = "സെർവർ ഫയൽ സംഭരണം à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´•àµà´·à´®à´®à´¾à´•àµà´•àµà´•" + +[admin.settings.storage.sharing.email] +description = "ഇമെയിൽ വിലാസങàµà´™à´³à´¿à´²àµ‚ടെ പങàµà´•ിടൽ à´…à´¨àµà´µà´¦à´¿à´•àµà´•àµà´•." +label = "ഇമെയിൽ ഷെയറിംഗൠപàµà´°à´µàµ¼à´¤àµà´¤à´¨à´•àµà´·à´®à´®à´¾à´•àµà´•àµà´•" +mailLink = "മെയിൽ à´•àµà´°à´®àµ€à´•രണങàµà´™àµ¾ കോൺഫിഗർ ചെയàµà´¯àµà´•" +mailNote = "മെയിൽ കോൺഫിഗറേഷൻ ആവശàµà´¯à´®à´¾à´£àµ. " + +[admin.settings.storage.sharing.enabled] +description = "സംഭരിചàµà´š ഫയലàµà´•ൾ ഉപയോകàµà´¤à´¾à´•àµà´•ൾകàµà´•ൠപങàµà´•ിടാൻ à´…à´¨àµà´µà´¦à´¿à´•àµà´•àµà´•." +label = "പങàµà´•ിടൽ à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´•àµà´·à´®à´®à´¾à´•àµà´•àµà´•" + +[admin.settings.storage.sharing.links] +description = "സൈൻ ഇൻ ചെയàµà´¤ ലിങàµà´•àµà´•ളിലൂടെ പങàµà´•ിടൽ à´…à´¨àµà´µà´¦à´¿à´•àµà´•àµà´•." +frontendUrlLink = "സിസàµà´±àµà´±à´‚ സജàµà´œàµ€à´•രണങàµà´™à´³à´¿àµ½ കോൺഫിഗർ ചെയàµà´¯àµà´•" +frontendUrlNote = "ഒരൠFrontend URL ആവശàµà´¯à´®à´¾à´£àµ. " +label = "ഷെയർ ലിങàµà´•àµà´•ൾ à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´•àµà´·à´®à´®à´¾à´•àµà´•àµà´•" + +[admin.settings.storage.signing.enabled] +description = "ബഹàµ-പങàµà´•ാളി ഡോകàµà´¯àµà´®àµ†à´¨àµà´±àµ à´’à´ªàµà´ªà´¿à´Ÿàµ½ സെഷനàµà´•ൾ സൃഷàµà´Ÿà´¿à´•àµà´•ാൻ ഉപയോകàµà´¤à´¾à´•àµà´•ൾകàµà´•ൠഅനàµà´µà´¾à´¦à´‚ നൽകàµà´•. ഇതിനൠസെർവർ ഫയൽ സംഭരണം à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´•àµà´·à´®à´®à´¾à´•àµà´•ണം." +label = "à´—àµà´°àµ‚à´ªàµà´ªàµ സൈൻ ചെയàµà´¯àµ½ à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´•àµà´·à´®à´®à´¾à´•àµà´•àµà´• (ആൽഫ)" + [admin.settings.unsavedChanges] cancel = "à´Žà´¡à´¿à´±àµà´±à´¿à´‚ഗൠതàµà´Ÿà´°àµà´•" discard = "മാറàµà´±à´™àµà´™àµ¾ തളàµà´³àµà´•" @@ -2059,7 +2089,19 @@ numbers = "സംഖàµà´¯à´•ൾ/പരിധികൾ: 5, 10-20" progressions = "à´ªàµà´°àµ‹à´—തികൾ: 3n, 4n+1" [certSign] +allSigned = "à´Žà´²àµà´²à´¾ പങàµà´•ാളികളàµà´‚ à´’à´ªàµà´ªàµà´µà´šàµà´šàµ. à´…à´¨àµà´¤à´¿à´®àµ€à´•à´°à´¿à´•àµà´•ാൻ തയàµà´¯à´¾à´±à´¾à´£àµ." +awaitingSignatures = "à´’à´ªàµà´ªàµà´•ൾ കാതàµà´¤à´¿à´°à´¿à´•àµà´•àµà´¨àµà´¨àµ" +signatureProgress = "{{signedCount}}/{{totalCount}} à´’à´ªàµà´ªàµà´•ൾ" chooseCertificate = "സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ ഫയൽ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" +declined = "നിരസിചàµà´šàµ" +fetchFailed = "സൈൻ ചെയàµà´¯àµà´¨àµà´¨ ഡാറàµà´± ലോഡൠചെയàµà´¯à´¾àµ» à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²" +finalized = "à´…à´¨àµà´¤à´¿à´®àµ€à´•à´°à´¿à´šàµà´šàµ" +notified = "കാതàµà´¤à´¿à´°à´¿à´ªàµà´ªà´¿àµ½" +partialNote = "നിലവിലെ à´’à´ªàµà´ªàµà´•ളോടെ നേരതàµà´¤àµ†à´¾à´¨àµà´¤à´¿à´®àµ€à´•à´°à´¿à´•àµà´•ാവàµà´¨àµà´¨à´¤à´¾à´£àµ. à´’à´ªàµà´ªà´¿à´Ÿà´¾à´¤àµà´¤ പങàµà´•ാളികൾ ഒഴിവാകàµà´•à´ªàµà´ªàµ†à´Ÿàµà´‚." +pending = "കാതàµà´¤à´¿à´°à´¿à´ªàµà´ªà´¿àµ½" +readyToFinalize = "à´…à´¨àµà´¤à´¿à´®àµ€à´•à´°à´¿à´•àµà´•ാൻ തയàµà´¯à´¾à´±à´¾à´£àµ" +signed = "à´’à´ªàµà´ªà´¿à´Ÿàµà´Ÿàµ" +viewed = "à´•à´£àµà´Ÿàµ" chooseJksFile = "JKS ഫയൽ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" chooseP12File = "PKCS12 ഫയൽ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" choosePfxFile = "PFX ഫയൽ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" @@ -2082,6 +2124,7 @@ title = "സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ à´’à´ªàµà´ªà´¿à´Ÿàµ½" invisible = "അദൃശàµà´¯" stepTitle = "à´’à´ªàµà´ªà´¿à´¨àµà´±àµ† ദൃശàµà´¯à´°àµ‚പം" visible = "ദൃശàµà´¯à´‚" +visibility = "ദൃശàµà´¯à´¤" [certSign.appearance.options] title = "à´’à´ªàµà´ªà´¿à´¨àµà´±àµ† വിശദാംശങàµà´™àµ¾" @@ -2188,6 +2231,252 @@ bullet4 = "പരിശോധനയàµà´•àµà´•ായി ഇചàµà´›à´¾à´¨àµ text = "നിങàµà´™àµ¾ à´’à´ªàµà´ªàµà´•ൾ പരിശോധികàµà´•àµà´®àµà´ªàµ‹àµ¾, à´…à´µ സാധàµà´µà´¾à´£àµ‹, ആരാണൠഒപàµà´ªà´¿à´Ÿàµà´Ÿà´¤àµ, à´Žà´ªàµà´ªàµ‹àµ¾ à´’à´ªàµà´ªà´¿à´Ÿàµà´Ÿà´¤àµ, à´’à´ªàµà´ªà´¿à´Ÿàµà´Ÿà´¤à´¿à´¨àµ ശേഷം ഡോകàµà´¯àµà´®àµ†à´¨àµà´±à´¿àµ½ മാറàµà´±à´®àµà´£àµà´Ÿàµ‹ à´Žà´¨àµà´¨à´¿à´µ ടൂൾ അറിയികàµà´•àµà´‚." title = "à´’à´ªàµà´ªàµà´•ൾ പരിശോധന" +[certSign.collab.finalize] +button = "à´…à´¨àµà´¤à´¿à´®àµ€à´•à´°à´¿à´šàµà´šàµ à´’à´ªàµà´ªà´¿à´Ÿàµà´Ÿ PDF ലോഡൠചെയàµà´¯àµà´•" +early = "നിലവിലàµà´³àµà´³ à´’à´ªàµà´ªàµà´•ളോടെ à´…à´¨àµà´¤à´¿à´®àµ€à´•à´°à´¿à´•àµà´•àµà´•" + +[certSign.collab.sessionDetail] +addButton = "പങàµà´•ാളികളെ ചേർകàµà´•àµà´•" +addParticipants = "പങàµà´•ാളികളെ ചേർകàµà´•àµà´•" +addParticipantsError = "പങàµà´•ാളികളെ ചേർകàµà´•ാൻ à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²" +backToList = "സെഷനàµà´•ളിലേകàµà´•ൠതിരികെ" +deleteConfirm = "ഉറപàµà´ªà´¾à´£àµ‹? ഇതൠപിൻവലികàµà´•ാൻ കഴിയിലàµà´²." +deleteError = "സെഷൻ ഇലàµà´²à´¾à´¤à´¾à´•àµà´•ാൻ à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²" +deleted = "സെഷൻ ഇലàµà´²à´¾à´¤à´¾à´•àµà´•à´¿" +deleteSession = "സെഷൻ ഇലàµà´²à´¾à´¤à´¾à´•àµà´•àµà´•" +dueDate = "അവസാന തീയതി" +finalizeError = "സെഷൻ à´…à´¨àµà´¤à´¿à´®àµ€à´•à´°à´¿à´•àµà´•ാൻ à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²" +loadPdfError = "à´’à´ªàµà´ªà´¿à´Ÿàµà´Ÿ PDF ലോഡൠചെയàµà´¯à´¾àµ» à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²" +loadSignedPdf = "à´’à´ªàµà´ªà´¿à´Ÿàµà´Ÿ PDF സജീവ ഫയലàµà´•ളിലേകàµà´•ൠലോഡൠചെയàµà´¯àµà´•" +messageLabel = "സനàµà´¦àµ‡à´¶à´‚" +noAdditionalInfo = "à´…à´§à´¿à´• വിവരങàµà´™à´³àµŠà´¨àµà´¨àµà´®à´¿à´²àµà´²" +owner = "ഉടമ" +participantRemoved = "പങàµà´•ാളിയെ നീകàµà´•à´¿" +participants = "പങàµà´•ാളികൾ" +participantsAdded = "പങàµà´•ാളികളെ വിജയകരമായി ചേർതàµà´¤àµ" +removeParticipant = "നീകàµà´•àµà´•" +removeParticipantError = "പങàµà´•ാളിയെ നീകàµà´•ാൻ à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²" +selectUsers = "ഉപയോകàµà´¤à´¾à´•àµà´•ളെ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•..." +sessionInfo = "സെഷൻ വിവരം" +workbenchTitle = "സെഷൻ മാനേജàµà´®àµ†à´¨àµà´±àµ" + +[certSign.collab.signRequest] +addedToFiles = "ഡോകàµà´¯àµà´®àµ†à´¨àµà´±àµ സജീവ ഫയലàµà´•ളിൽ ചേർതàµà´¤àµ" +addSignature = "നിങàµà´™à´³àµà´Ÿàµ† à´’à´ªàµà´ªàµ ചേർകàµà´•àµà´•" +addToFiles = "സജീവ ഫയലàµà´•ളിലേകàµà´•ൠചേർകàµà´•àµà´•" +advancedSettings = "à´…à´¡àµà´µà´¾àµ»à´¸àµà´¡àµ à´•àµà´°à´®àµ€à´•രണങàµà´™àµ¾" +backToList = "à´’à´ªàµà´ªàµ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¨à´•ളിലേകàµà´•ൠതിരികെ" +certificateChoice = "à´’à´ªàµà´ªà´¿à´Ÿà´¾àµ» ഒരൠസർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" +changeSignature = "à´’à´ªàµà´ªàµ മാറàµà´±àµà´•" +clearSignature = "à´’à´ªàµà´ªàµ മായàµà´•àµà´•àµà´•" +completeAndSign = "പൂർതàµà´¤à´¿à´¯à´¾à´•àµà´•à´¿ à´’à´ªàµà´ªà´¿à´Ÿàµà´•" +createNewSignature = "à´ªàµà´¤à´¿à´¯ à´’à´ªàµà´ªàµ സൃഷàµà´Ÿà´¿à´•àµà´•àµà´•" +declineButton = "നിരസികàµà´•àµà´•" +decline = "à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¨ നിരസികàµà´•àµà´•" +deleteSelected = "തിരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤ à´’à´ªàµà´ªàµ ഇലàµà´²à´¾à´¤à´¾à´•àµà´•àµà´•" +drawSignature = "താഴെ നിങàµà´™à´³àµà´Ÿàµ† à´’à´ªàµà´ªàµ വരയàµà´•àµà´•àµà´•" +dueDate = "അവസാന തീയതി" +fileTooLarge = "ഫയൽ വലàµà´ªàµà´ªà´‚ 5MB-ൽ à´•àµà´±à´µà´¾à´¯à´¿à´°à´¿à´•àµà´•ണം" +fontFamily = "à´…à´•àµà´·à´°à´¶àµˆà´²à´¿" +fontSize = "ഫോണàµà´Ÿàµ വലàµà´ªàµà´ªà´‚: {{size}}px" +fontSizePlaceholder = "വലàµà´ªàµà´ªà´‚" +from = "അയചàµà´šà´¤àµ" +invalidCertFile = "ദയവായി P12 à´…à´²àµà´²àµ†à´™àµà´•ിൽ PFX സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ ഫയൽ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" +invalidFileType = "ദയവായി ഒരൠഇമേജൠഫയൽ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" +location = "à´¸àµà´¥à´¾à´¨à´‚ (à´à´šàµà´›à´¿à´•à´‚)" +locationPlaceholder = "നിങàµà´™àµ¾ à´à´¤àµ à´¸àµà´¥à´²à´¤àµà´¤àµ നിനàµà´¨àµ à´’à´ªàµà´ªà´¿à´Ÿàµà´¨àµà´¨àµ?" +message = "സനàµà´¦àµ‡à´¶à´‚" +noCertificate = "ദയവായി ഒരൠസർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ ഫയൽ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" +noSignatures = "PDF-ൽ à´•àµà´±à´žàµà´žà´¤àµ ഒരൠഒപàµà´ªàµ†à´™àµà´•à´¿à´²àµà´‚ ഇടàµà´•" +p12File = "P12/PFX സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ ഫയൽ" +password = "സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ പാസàµâ€Œà´µàµ‡à´¡àµ" +passwordPlaceholder = "പാസàµâ€Œà´µàµ‡à´¡àµ നൽകàµà´•..." +penColor = "പെനàµà´¨à´¿à´¨àµà´±àµ† നിറം" +penSize = "പെനàµà´¨à´¿à´¨àµà´±àµ† വലàµà´ªàµà´ªà´‚: {{size}}px" +placementActive = "PDF-ൽ à´•àµà´²à´¿à´•àµà´•ൠചെയàµà´¤àµ ഇടàµà´•" +placeSignatureButton = "PDF-ൽ à´’à´ªàµà´ªàµ ഇടàµà´•" +reason = "കാരണം (à´à´šàµà´›à´¿à´•à´‚)" +reasonPlaceholder = "à´à´¤àµ കാരണതàµà´¤à´¾à´²à´¾à´£àµ à´’à´ªàµà´ªà´¿à´Ÿàµà´¨àµà´¨à´¤àµ?" +removeImage = "à´šà´¿à´¤àµà´°à´‚ നീകàµà´•àµà´•" +removeCertFile = "ഫയൽ നീകàµà´•àµà´•" +savedSignatures = "സംരകàµà´·à´¿à´šàµà´š à´’à´ªàµà´ªàµà´•ൾ" +selectFile = "ഇമേജൠഫയൽ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" +selectSignatureTitle = "à´’à´ªàµà´ªàµ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´• à´…à´²àµà´²àµ†à´™àµà´•ിൽ സൃഷàµà´Ÿà´¿à´•àµà´•àµà´•" +signButton = "ഡോകàµà´¯àµà´®àµ†à´¨àµà´±àµ à´’à´ªàµà´ªà´¿à´Ÿàµà´•" +signatureInfo = "à´ˆ à´•àµà´°à´®àµ€à´•രണങàµà´™àµ¾ ഡോകàµà´¯àµà´®àµ†à´¨àµà´±à´¿à´¨àµà´±àµ† ഉടമ നിർണàµà´£à´¯à´¿à´šàµà´šà´µà´¯à´¾à´£àµ" +signaturePlaced = "പേജിൽ à´’à´ªàµà´ªàµ വചàµà´šàµ" +signatureSettings = "à´’à´ªàµà´ªàµ à´•àµà´°à´®àµ€à´•രണങàµà´™àµ¾" +signatureText = "à´’à´ªàµà´ªàµ വാചകം" +signatureTextPlaceholder = "നിങàµà´™à´³àµà´Ÿàµ† പേരൠനൽകàµà´•..." +signatureTypeLabel = "à´’à´ªàµà´ªà´¿à´¨àµà´±àµ† തരം" +signingTitle = "à´’à´ªàµà´ªà´¿à´Ÿàµ½" +textColor = "വാചകതàµà´¤à´¿à´¨àµà´±àµ† നിറം" +typeSignature = "à´’à´ªàµà´ªàµ സൃഷàµà´Ÿà´¿à´•àµà´•ാൻ നിങàµà´™à´³àµà´Ÿàµ† പേരൠടൈപàµà´ªàµ ചെയàµà´¯àµà´•" +uploadCert = "ഇഷàµà´Ÿà´¾à´¨àµà´¸àµƒà´¤ സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ" +uploadCertDesc = "നിങàµà´™à´³àµà´Ÿàµ† à´¸àµà´µà´¨àµà´¤à´‚ P12/PFX സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ ഉപയോഗികàµà´•àµà´•" +uploadSignature = "നിങàµà´™à´³àµà´Ÿàµ† à´’à´ªàµà´ªàµ ഇമേജൠഅപàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¯àµà´•" +usePersonalCert = "പേഴàµâ€Œà´¸à´£àµ½ സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ" +usePersonalCertDesc = "നിങàµà´™à´³àµà´Ÿàµ† à´…à´•àµà´•ൗണàµà´Ÿà´¿à´¨àµ à´¸àµà´µà´¯à´‚ സൃഷàµà´Ÿà´¿à´šàµà´šà´¤àµ" +useServerCert = "ഓർഗനൈസേഷൻ സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ" +useServerCertDesc = "ഓർഗനൈസേഷനിൽ പങàµà´•à´¿à´Ÿàµà´¨àµà´¨ സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ" +workbenchTitle = "à´’à´ªàµà´ªàµ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¨" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "വരയàµà´Ÿàµ† നിറം തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" +continue = "à´¤àµà´Ÿà´°àµà´•" + +[certSign.collab.signRequest.certModal] +description = "നിങàµà´™àµ¾ {{count}} à´’à´ªàµà´ªàµ(കൾ) വചàµà´šà´¿à´°à´¿à´•àµà´•àµà´¨àµà´¨àµ. à´’à´ªàµà´ªà´¿à´Ÿàµ½ പൂർതàµà´¤à´¿à´¯à´¾à´•àµà´•ാൻ നിങàµà´™à´³àµà´Ÿàµ† സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•." +sign = "ഡോകàµà´¯àµà´®àµ†à´¨àµà´±àµ à´’à´ªàµà´ªà´¿à´Ÿàµà´•" +certValidating = "സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ സാധൂകരികàµà´•àµà´¨àµà´¨àµ..." +certValidUntil = "സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ {{date}} വരെ സാധàµà´µà´¾à´£àµ" +certInvalid = "സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ അസാധàµà´µà´¾à´£àµ: {{error}}" +certInvalidFallback = "അസാധàµà´µà´¾à´¯ സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ" +certNetworkError = "സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ സാധൂകരികàµà´•ാൻ à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²" +title = "സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ കോൺഫിഗർ ചെയàµà´¯àµà´•" + +[certSign.collab.signRequest.image] +hint = "നിങàµà´™à´³àµà´Ÿàµ† à´’à´ªàµà´ªà´¿à´¨àµà´±àµ† PNG à´…à´²àµà´²àµ†à´™àµà´•ിൽ JPG à´šà´¿à´¤àµà´°à´‚ à´…à´ªàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¯àµà´•" + +[certSign.collab.signRequest.mode] +move = "à´’à´ªàµà´ªàµ നീകàµà´•àµà´•" +place = "à´’à´ªàµà´ªàµ ഇടàµà´•" +title = "à´’à´ªàµà´ªàµ ഇടൽ à´…à´²àµà´²àµ†à´™àµà´•ിൽ നീകàµà´•ൽ മോഡàµ" + +[certSign.collab.signRequest.modeTabs] +draw = "വരയàµâ€Œà´•àµà´•àµà´•" +image = "à´…à´ªàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¯àµà´•" +text = "ടൈപàµà´ªàµ ചെയàµà´¯àµà´•" + +[certSign.collab.signRequest.placeSignature] +message = "PDF-ൽ à´•àµà´²à´¿à´•àµà´•ൠചെയàµà´¤àµ നിങàµà´™à´³àµà´Ÿàµ† à´’à´ªàµà´ªàµ ഇടàµà´•" +title = "à´’à´ªàµà´ªàµ ഇടൽ" + +[certSign.collab.signRequest.preview] +imageAlt = "തിരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤ à´’à´ªàµà´ªàµ" +missing = "à´®àµà´¨àµâ€à´¦àµƒà´·àµà´¯à´‚ ഇലàµà´²" +textFallback = "à´’à´ªàµà´ªàµ" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "വരചàµà´šàµ സൃഷàµà´Ÿà´¿à´šàµà´š à´’à´ªàµà´ªàµ" +defaultImageLabel = "à´…à´ªàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¤ à´’à´ªàµà´ªàµ" +defaultLabel = "à´’à´ªàµà´ªàµ" +defaultTextLabel = "ടൈപàµà´ªàµ ചെയàµà´¤ à´’à´ªàµà´ªàµ" +delete = "à´’à´ªàµà´ªàµ ഇലàµà´²à´¾à´¤à´¾à´•àµà´•àµà´•" +none = "സംരകàµà´·à´¿à´šàµà´š à´’à´ªàµà´ªàµà´•ളൊനàµà´¨àµà´®à´¿à´²àµà´²" + +[certSign.collab.signRequest.signatureType] +draw = "വരയàµâ€Œà´•àµà´•àµà´•" +type = "ടൈപàµà´ªàµ ചെയàµà´¯àµà´•" +upload = "à´…à´ªàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¯àµà´•" + +[certSign.collab.signRequest.steps] +back = "തിരികെ" +cancelPlacement = "ഇടൽ റദàµà´¦à´¾à´•àµà´•àµà´•" +certificate = "സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ" +clickMultipleTimes = "à´’à´¨àµà´¨àµ‹ അതിലധികമോ à´’à´ªàµà´ªàµà´•ൾ ഇടാൻ PDF-ൽ പല തവണ à´•àµà´²à´¿à´•àµà´•ൠചെയàµà´¯àµà´•. à´à´¤àµ†à´™àµà´•à´¿à´²àµà´‚ à´’à´ªàµà´ªàµ നീകàµà´•àµà´•യോ വലàµà´ªàµà´ªà´®à´¾à´±àµà´±àµà´•യോ ചെയàµà´¯à´¾àµ» അതൠഇഴàµà´¤àµà´•." +clickToPlace = "നിങàµà´™à´³àµà´Ÿàµ† à´’à´ªàµà´ªàµ à´ªàµà´°à´¤àµà´¯à´•àµà´·à´ªàµà´ªàµ†à´Ÿàµ‡à´£àµà´Ÿà´¿à´Ÿà´¤àµà´¤àµ PDF-ൽ à´•àµà´²à´¿à´•àµà´•ൠചെയàµà´¯àµà´•." +continue = "സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ തിരഞàµà´žàµ†à´Ÿàµà´ªàµà´ªà´¿à´²àµ‡à´•àµà´•ൠതàµà´Ÿà´°àµà´•" +continueToPlacement = "ഇടലിലേകàµà´•ൠതàµà´Ÿà´°àµà´•" +continueToReview = "പരിശോധനയിലേകàµà´•ൠതàµà´Ÿà´°àµà´•" +createSignature = "à´’à´ªàµà´ªàµ സൃഷàµà´Ÿà´¿à´•àµà´•àµà´•" +invisible = "അദൃശàµà´¯à´®à´¾à´¯à´¤àµ" +location = "à´¸àµà´¥à´²à´‚:" +multipleSignatures = "{{count}} à´’à´ªàµà´ªàµà´•ൾ PDF-ലേകàµà´•ൠപàµà´°à´¯àµ‹à´—à´¿à´•àµà´•à´ªàµà´ªàµ†à´Ÿàµà´‚" +oneSignature = "1 à´’à´ªàµà´ªàµ PDF-ലേകàµà´•ൠപàµà´°à´¯àµ‹à´—à´¿à´•àµà´•à´ªàµà´ªàµ†à´Ÿàµà´‚" +placeOnPdf = "PDF-ൽ ഇടàµà´•" +reason = "കാരണം:" +reviewTitle = "à´’à´ªàµà´ªà´¿à´Ÿàµà´¨àµà´¨à´¤à´¿à´¨àµ à´®àµà´®àµà´ªàµ പരിശോധികàµà´•àµà´•" +signaturePlaced = "പേജൠ{{page}}-ൽ à´’à´ªàµà´ªàµ വെചàµà´šàµ. à´¸àµà´¥à´¾à´¨à´‚ à´•àµà´°à´®àµ€à´•à´°à´¿à´•àµà´•ാൻ വീണàµà´Ÿàµà´‚ à´•àµà´²à´¿à´•àµà´•ൠചെയàµà´¯àµà´•യോ റിവàµà´¯àµ‚വിലേകàµà´•ൠതàµà´Ÿà´°àµà´•യോ ചെയàµà´¯à´¾à´‚." +visible = "ദൃശàµà´¯à´®à´¾à´¯" +visibility = "ദൃശàµà´¯à´¤:" +yourSignatures = "നിങàµà´™à´³àµà´Ÿàµ† à´’à´ªàµà´ªàµà´•ൾ ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "നിറം" +fontLabel = "ഫോണàµà´Ÿàµ" +fontSizeLabel = "വലàµà´ªàµà´ªà´‚" +fontSizePlaceholder = "16" +label = "à´’à´ªàµà´ªàµ വാചകം" +modalHint = "നിങàµà´™à´³àµà´Ÿàµ† പേരൠനൽകàµà´•, ശേഷം PDF-ൽ ഇടാൻ Continue à´•àµà´²à´¿à´•àµà´•ൠചെയàµà´¯àµà´•." +placeholder = "നിങàµà´™à´³àµà´Ÿàµ† പേരൠനൽകàµà´•..." + +[certSign.collab.participant] +certValidating = "സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ സാധൂകരികàµà´•àµà´¨àµà´¨àµ..." +certValid = "✓ സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ സാധàµà´µà´¾à´£àµ" +certValidUntil = " {{date}} വരെ" +certInvalid = "✗ {{error}}" +certInvalidFallback = "അസാധàµà´µà´¾à´¯ സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ" +certNetworkError = "സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ സാധൂകരികàµà´•ാൻ à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²" + +[certSign.collab.addParticipants] +add = "{{count}} പങàµà´•ാളികളെ ചേർകàµà´•àµà´•" +back = "തിരികെ" +configureSignatures = "à´’à´ªàµà´ªàµ à´•àµà´°à´®àµ€à´•രണങàµà´™àµ¾ കോൺഫിഗർ ചെയàµà´¯àµà´•" +continue = "à´’à´ªàµà´ªàµ à´•àµà´°à´®àµ€à´•രണങàµà´™à´³à´¿à´²àµ‡à´•àµà´•ൠതàµà´Ÿà´°àµà´•" +reasonHelp = "à´ˆ പങàµà´•ാളികൾകàµà´•ായി à´®àµàµ»à´•ൂടàµà´Ÿà´¿ ഒരൠഒപàµà´ªà´¿à´Ÿàµ½ കാരണം നിശàµà´šà´¯à´¿à´•àµà´•àµà´• (à´à´šàµà´›à´¿à´•à´‚, അവർ à´’à´ªàµà´ªà´¿à´Ÿàµà´®àµà´ªàµ‹àµ¾ മാറàµà´±à´¾à´‚)" +reasonPlaceholder = "ഉദാ: അംഗീകാരം, പരിശോധന..." +selectUsers = "ഉപയോകàµà´¤à´¾à´•àµà´•ളെ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" + +[certSign.collab.sessionCreation] +includeSummaryPage = "à´’à´ªàµà´ªàµ സംഗàµà´°à´¹ പേജൠഉൾപàµà´ªàµ†à´Ÿàµà´¤àµà´¤àµà´•" +includeSummaryPageHelp = "അവസാനതàµà´¤à´¿àµ½ à´Žà´²àµà´²à´¾ à´’à´ªàµà´ªàµ മെറàµà´±à´¾à´¡àµ‡à´±àµà´±à´¯àµà´‚ ഉൾപàµà´ªàµ†à´Ÿàµà´Ÿ ഒരൠസംഗàµà´°à´¹ പേജൠചേർകàµà´•àµà´‚. à´µàµà´¯à´•àµà´¤à´¿à´—à´¤ പേജàµà´•ളിലെ ഡിജിറàµà´±àµ½ സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ à´’à´ªàµà´ªàµ ബോകàµà´¸àµà´•ൾ à´ªàµà´°à´¦àµ¼à´¶à´¿à´ªàµà´ªà´¿à´•àµà´•à´¿à´²àµà´² (വെറàµà´±àµ à´’à´ªàµà´ªàµà´•ൾ ബാധികàµà´•à´¿à´²àµà´²)." + +[certSign.collab.sessionList] +active = "സജീവം" +finalized = "à´…à´¨àµà´¤à´¿à´®àµ€à´•à´°à´¿à´šàµà´šàµ" + +[certSign.collab.signatureSettings] +description = "à´Žà´²àµà´²à´¾ പങàµà´•ാളികളàµà´Ÿàµ†à´¯àµà´‚ à´’à´ªàµà´ªà´¿à´¨àµà´±àµ† രൂപം à´Žà´™àµà´™à´¨àµ†à´¯à´¿à´°à´¿à´•àµà´•ണമെനàµà´¨àµ കോൺഫിഗർ ചെയàµà´¯àµà´•" +title = "à´’à´ªàµà´ªà´¿à´¨àµà´±àµ† രൂപഭാവം" + +[certSign.collab.userSelector] +inviteUsers = "ഉപയോകàµà´¤à´¾à´•àµà´•ളെ ചേർകàµà´•àµà´•" +loadError = "ഉപയോകàµà´¤à´¾à´•àµà´•ളെ ലോഡൠചെയàµà´¯à´¾àµ» à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²" +noTeam = "ടീം ഇലàµà´²" +noUsers = "മറàµà´±àµ ഉപയോകàµà´¤à´¾à´•àµà´•ളെ à´•à´£àµà´Ÿàµ†à´¤àµà´¤à´¾à´¨à´¾à´¯à´¿à´²àµà´²." +placeholder = "ഉപയോകàµà´¤à´¾à´•àµà´•ളെ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•..." + +[certSign.mobile] +panelActions = "à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¿à´•ൾ" +panelDocument = "ഡോകàµà´¯àµà´®àµ†à´¨àµà´±àµ" +panelPeople = "à´µàµà´¯à´•àµà´¤à´¿à´•ൾ" + +[certSign.sessions] +deleted = "സെഷൻ ഇലàµà´²à´¾à´¤à´¾à´•àµà´•à´¿" +fetchFailed = "സെഷൻ വിവരങàµà´™àµ¾ ലോഡൠചെയàµà´¯à´¾àµ» à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²" +finalized = "സെഷൻ à´…à´¨àµà´¤à´¿à´®àµ€à´•à´°à´¿à´šàµà´šàµ" +loaded = "à´’à´ªàµà´ªà´¿à´Ÿàµà´Ÿ PDF ലോഡൠചെയàµà´¤àµ" +pdfNotReady = "PDF തയàµà´¯à´¾à´±à´¾à´¯à´¿à´Ÿàµà´Ÿà´¿à´²àµà´²" +pdfNotReadyDesc = "à´’à´ªàµà´ªà´¿à´Ÿàµà´Ÿ PDF സൃഷàµà´Ÿà´¿à´šàµà´šàµ കൊണàµà´Ÿà´¿à´°à´¿à´•àµà´•àµà´¨àµà´¨àµ. ദയവായി à´•àµà´±à´šàµà´šàµ സമയം à´•à´´à´¿à´žàµà´žàµ വീണàµà´Ÿàµà´‚ à´¶àµà´°à´®à´¿à´•àµà´•àµà´•." + +[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 à´…à´ªàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¯àµà´•" + [changeCreds] changePassword = "നിങàµà´™àµ¾ à´¸àµà´¥à´¿à´° ലോഗിൻ വിവരങàµà´™à´³à´¾à´£àµ ഉപയോഗികàµà´•àµà´¨àµà´¨à´¤àµ. ദയവായി ഒരൠപàµà´¤à´¿à´¯ പാസàµâ€Œà´µàµ‡à´¡àµ നൽകàµà´•" changeUsername = "നിങàµà´™à´³àµà´Ÿàµ† യൂസർനെയിം à´…à´ªàµâ€Œà´¡àµ‡à´±àµà´±àµ ചെയàµà´¯àµà´•. à´…à´ªàµâ€Œà´¡àµ‡à´±àµà´±à´¿à´¨àµ ശേഷം നിങàµà´™àµ¾ ലോഗൠഔടàµà´Ÿàµ ചെയàµà´¯à´ªàµà´ªàµ†à´Ÿàµà´‚." @@ -3242,6 +3531,46 @@ totalSelected = "ആകെ തിരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤à´¤àµ" unsupported = "പിനàµà´¤àµà´£à´¯à´¿à´²àµà´²" unzip = "അൺസിപàµà´ªàµ" uploadError = "à´šà´¿à´² ഫയലàµà´•ൾ à´…à´ªàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¯à´¾àµ» പരാജയപàµà´ªàµ†à´Ÿàµà´Ÿàµ." +copyCreated = "പകർപàµà´ªàµ à´ˆ ഉപകരണതàµà´¤à´¿àµ½ സേവൠചെയàµà´¤àµ." +copyFailed = "ഒരൠപകർപàµà´ªàµ സൃഷàµà´Ÿà´¿à´•àµà´•ാൻ à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²." +leaveShare = "à´Žà´¨àµà´±àµ† ലിസàµà´±àµà´±à´¿àµ½ നിനàµà´¨àµ നീകàµà´•à´‚ ചെയàµà´¯àµà´•" +leaveShareFailed = "പങàµà´•à´¿à´Ÿàµà´Ÿ ഫയൽ നീകàµà´•ാൻ à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²." +leaveShareSuccess = "നിങàµà´™à´³àµà´Ÿàµ† ഷെയർ ചെയàµà´¤ ലിസàµà´±àµà´±à´¿àµ½ നിനàµà´¨àµ നീകàµà´•à´¿." +removeBoth = "à´°à´£àµà´Ÿà´¿à´Ÿà´¤àµà´¤àµà´¨à´¿à´¨àµà´¨àµà´‚ നീകàµà´•à´‚ ചെയàµà´¯àµà´•" +removeFilePrompt = "à´ˆ ഫയൽ à´ˆ ഉപകരണതàµà´¤à´¿à´²àµà´‚ നിങàµà´™à´³àµà´Ÿàµ† സെർവറിലàµà´‚ സേവൠചെയàµà´¤à´¿à´°à´¿à´•àµà´•àµà´¨àµà´¨àµ. à´à´¤àµ ഇടതàµà´¤àµ നിനàµà´¨àµ നീകàµà´•ണം?" +removeFileTitle = "ഫയൽ നീകàµà´•àµà´•" +removeLocalOnly = "à´ˆ ഉപകരണം മാതàµà´°à´‚" +removeServerFailed = "സെർവറിൽ നിനàµà´¨àµ ഫയൽ നീകàµà´•ാൻ à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²." +removeServerOnly = "സെർവർ മാതàµà´°à´‚" +removeServerOnlyPrompt = "à´ˆ ഫയൽ നിങàµà´™à´³àµà´Ÿàµ† സെർവറിൽ മാതàµà´°à´‚ സംഭരിചàµà´šà´¿à´°à´¿à´•àµà´•àµà´¨àµà´¨àµ. ഇതൠസെർവറിൽ നിനàµà´¨àµ നീകàµà´•ണോ?" +removeServerSuccess = "സെർവറിൽ നിനàµà´¨àµ നീകàµà´•à´¿." +removeSharedPrompt = "à´ˆ ഫയൽ നിങàµà´™à´³àµà´®à´¾à´¯à´¿ പങàµà´•à´¿à´Ÿàµà´Ÿà´¤à´¾à´£àµ. ഇതൠഈ ഉപകരണതàµà´¤à´¿àµ½ നിനàµà´¨àµ‹ നിങàµà´™à´³àµà´Ÿàµ† ഷെയർ ചെയàµà´¤ ലിസàµà´±àµà´±à´¿àµ½ നിനàµà´¨àµ‹ നീകàµà´•ാം." +removeSharedServerOnlyBlockedPrompt = "à´ˆ ഫയൽ നിങàµà´™à´³àµà´®à´¾à´¯à´¿ പങàµà´•à´¿à´Ÿàµà´Ÿà´¤à´¾à´£àµ, സെർവറിൽ മാതàµà´°à´‚ സംഭരിചàµà´šà´¿à´°à´¿à´•àµà´•àµà´¨àµà´¨àµ." +removeSharedServerOnlyPrompt = "à´ˆ ഫയൽ നിങàµà´™à´³àµà´®à´¾à´¯à´¿ പങàµà´•à´¿à´Ÿàµà´Ÿà´¤àµà´‚ സെർവറിൽ മാതàµà´°à´‚ സംഭരിചàµà´šà´¤àµà´®à´¾à´£àµ. നിങàµà´™à´³àµà´Ÿàµ† ലിസàµà´±àµà´±à´¿àµ½ നിനàµà´¨àµ നീകàµà´•ണോ?" +changesNotUploaded = "മാറàµà´±à´™àµà´™àµ¾ à´…à´ªàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¤à´¿à´Ÿàµà´Ÿà´¿à´²àµà´²" +cloudFile = "à´•àµà´²àµ—ഡൠഫയൽ" +filterAll = "à´Žà´²àµà´²à´¾à´‚" +filterLocal = "à´ªàµà´°à´¾à´¦àµ‡à´¶à´¿à´•à´‚" +filterSharedByMe = "ഞാൻ പങàµà´•à´¿à´Ÿàµà´Ÿà´¤àµ" +filterSharedWithMe = "à´Žà´¨àµà´¨àµ†à´•àµà´•ൊപàµà´ªà´‚ പങàµà´•à´¿à´Ÿàµà´Ÿà´¤àµ" +lastSynced = "അവസാനം സിങàµà´•ൠചെയàµà´¤à´¤àµ" +localOnly = "à´ªàµà´°à´¾à´¦àµ‡à´¶à´¿à´•à´‚ മാതàµà´°à´‚" +makeCopy = "ഒരൠപകർപàµà´ªàµ സൃഷàµà´Ÿà´¿à´•àµà´•àµà´•" +owner = "ഉടമ" +ownerUnknown = "à´…à´œàµà´žà´¾à´¤à´‚" +share = "പങàµà´•à´¿à´Ÿàµà´•" +shareSelected = "തിരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤à´¤àµ പങàµà´•à´¿à´Ÿàµà´•" +sharedByYou = "നിങàµà´™àµ¾ പങàµà´•à´¿à´Ÿàµà´Ÿà´¤àµ" +sharedEditNoticeBody = "à´ˆ ഫയലിനàµà´±àµ† സെർവർ പതിപàµà´ªà´¿àµ½ നിങàµà´™àµ¾à´•àµà´•ൠഎഡിറàµà´±àµ അവകാശമിലàµà´². നിങàµà´™àµ¾ ചെയàµà´¯àµà´¨àµà´¨ മാറàµà´±à´™àµà´™àµ¾ ഒരൠപàµà´°à´¾à´¦àµ‡à´¶à´¿à´• പകർപàµà´ªà´¾à´¯à´¿ സേവൠചെയàµà´¯àµà´‚." +sharedEditNoticeConfirm = "മനസàµà´¸à´¿à´²à´¾à´¯à´¿" +sharedEditNoticeTitle = "വായികàµà´•ാനേ കഴിയൂ à´Žà´¨àµà´¨ സെർവർ പകർപàµà´ªàµ" +sharedWithYou = "നിങàµà´™à´³àµà´®à´¾à´¯à´¿ പങàµà´•à´¿à´Ÿàµà´Ÿà´¤àµ" +sharing = "പങàµà´•ിടൽ" +storageState = "സംഭരണം" +synced = "സിങàµà´•ൠചെയàµà´¤àµ" +updateOnServer = "സെർവറിൽ à´…à´ªàµâ€Œà´¡àµ‡à´±àµà´±àµ ചെയàµà´¯àµà´•" +uploadSelected = "തിരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤à´¤àµ à´…à´ªàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¯àµà´•" +uploadToServer = "സെർവറിലേകàµà´•ൠഅപàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¯àµà´•" [files] addFiles = "ഫയലàµà´•ൾ ചേർകàµà´•àµà´•" @@ -3367,6 +3696,77 @@ title = "PDF-കൾ à´«àµà´²à´¾à´±àµà´±àµ» ചെയàµà´¯àµ½ à´•àµà´±à´¿à´š discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "à´—àµà´°àµ‚à´ªàµà´ªàµ സൈൻ ചെയàµà´¯àµ½ സംബനàµà´§à´¿à´šàµà´šàµ" + +[groupSigning.tooltip.finalization] +bullet1 = "നിങàµà´™àµ¾ നൽകിയിരികàµà´•àµà´¨àµà´¨ പങàµà´•ാളികളàµà´Ÿàµ† à´•àµà´°à´®à´¤àµà´¤à´¿àµ½ à´Žà´²àµà´²à´¾ à´’à´ªàµà´ªàµà´•à´³àµà´‚ à´ªàµà´°à´¯àµ‹à´—à´¿à´•àµà´•àµà´‚" +bullet2 = "ആവശàµà´¯à´®àµ†à´™àµà´•ിൽ ഭാഗിക à´’à´ªàµà´ªàµà´•ളോടെയàµà´‚ à´…à´¨àµà´¤à´¿à´®àµ€à´•à´°à´¿à´•àµà´•ാം" +bullet3 = "à´…à´¨àµà´¤à´¿à´®àµ€à´•à´°à´¿à´šàµà´šà´¤à´¿à´¨àµ ശേഷം സെഷൻ മാറàµà´±à´¾àµ» കഴിയിലàµà´²" +description = "à´Žà´²àµà´²à´¾ പങàµà´•ാളികളàµà´‚ à´’à´ªàµà´ªà´¿à´Ÿàµà´Ÿà´¤à´¿à´¨àµ ശേഷം (à´…à´²àµà´²àµ†à´™àµà´•ിൽ നിങàµà´™àµ¾ നേരതàµà´¤àµ† à´…à´¨àµà´¤à´¿à´®àµ€à´•à´°à´¿à´•àµà´•ാൻ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´®àµà´ªàµ‹àµ¾) à´…à´¨àµà´¤à´¿à´® à´’à´ªàµà´ªà´¿à´Ÿàµà´Ÿ PDF സൃഷàµà´Ÿà´¿à´•àµà´•ാം." +title = "à´…à´¨àµà´¤à´¿à´®àµ€à´•à´°à´£ à´ªàµà´°à´•àµà´°à´¿à´¯" + +[groupSigning.tooltip.roles] +bullet1 = "ഉടമ (നിങàµà´™àµ¾): സെഷൻ സൃഷàµà´Ÿà´¿à´•àµà´•àµà´¨àµà´¨àµ, à´’à´ªàµà´ªàµ ഡിഫോൾടàµà´Ÿàµà´•ൾ കോൺഫിഗർ ചെയàµà´¯àµà´¨àµà´¨àµ, ഡോകàµà´¯àµà´®àµ†à´¨àµà´±àµ à´…à´¨àµà´¤à´¿à´®àµ€à´•à´°à´¿à´•àµà´•àµà´¨àµà´¨àµ" +bullet2 = "പങàµà´•ാളികൾ: അവരàµà´Ÿàµ† à´’à´ªàµà´ªàµ സൃഷàµà´Ÿà´¿à´•àµà´•àµà´¨àµà´¨àµ, സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´¨àµà´¨àµ, PDF-ൽ ഇടàµà´¨àµà´¨àµ" +bullet3 = "പങàµà´•ാളികൾകàµà´•ൠഒപàµà´ªà´¿à´¨àµà´±àµ† ദൃശàµà´¯à´¤, കാരണമോ, à´¸àµà´¥à´¾à´¨à´®àµ‹ മാറàµà´±à´¾àµ» കഴിയിലàµà´²" +description = "à´Žà´²àµà´²à´¾ പങàµà´•ാളികൾകàµà´•àµà´‚ à´’à´ªàµà´ªà´¿à´¨àµà´±àµ† രൂപഭാവ à´•àµà´°à´®àµ€à´•രണങàµà´™àµ¾ നിങàµà´™àµ¾ നിയനàµà´¤àµà´°à´¿à´•àµà´•àµà´¨àµà´¨àµ." +title = "പങàµà´•ാളികളàµà´Ÿàµ† പങàµà´•àµà´•ൾ" + +[groupSigning.tooltip.sequential] +bullet1 = "ആദàµà´¯à´¤àµà´¤àµ† പങàµà´•ാളി à´’à´ªàµà´ªà´¿à´Ÿàµà´¨àµà´¨à´¤à´¿à´¨àµ à´®àµà´®àµà´ªàµ à´°à´£àµà´Ÿà´¾à´®à´¤àµà´¤à´µàµ»à´•àµà´•ൠഡോകàµà´¯àµà´®àµ†à´¨àµà´±àµ ആകàµâ€Œà´¸à´¸àµ ലഭികàµà´•à´¿à´²àµà´²" +bullet2 = "നിയമാനàµà´¸àµƒà´¤ à´•àµà´°à´®à´‚ ഉറപàµà´ªà´¾à´•àµà´•àµà´¨àµà´¨àµ" +bullet3 = "പടàµà´Ÿà´¿à´•യിൽ ഇഴàµà´¤à´¿ നീകàµà´•à´¿ പങàµà´•ാളികളàµà´Ÿàµ† à´•àµà´°à´®à´‚ മാറàµà´±à´¾à´‚" +description = "നിങàµà´™àµ¾ നിർദàµà´¦àµ‡à´¶à´¿à´šàµà´š à´•àµà´°à´®à´¤àµà´¤à´¿àµ½ പങàµà´•ാളികൾ ഡോകàµà´¯àµà´®àµ†à´¨àµà´±à´¿àµ½ à´’à´ªàµà´ªà´¿à´Ÿàµà´‚. അവരàµà´Ÿàµ† ടേൺ വരàµà´®àµà´ªàµ‹àµ¾ ഓരോ à´’à´ªàµà´ªàµà´µàµ†à´¯àµà´•àµà´•àµà´¨àµà´¨à´µàµ¼à´•àµà´•àµà´‚ അറിയിപàµà´ªàµ ലഭികàµà´•àµà´‚." +title = "à´•àµà´°à´®à´¾à´¨àµà´¸àµƒà´¤ à´’à´ªàµà´ªà´¿à´Ÿàµ½" + +[groupSigning.steps] +back = "തിരികെ" +completed = "പൂർതàµà´¤à´¿à´¯à´¾à´¯à´¿" +current = "നിലവിലെ" +stepLabel = "ഘടàµà´Ÿà´‚ {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "പരിശോധനയിലേകàµà´•ൠതàµà´Ÿà´°àµà´•" +invisible = "à´’à´ªàµà´ªàµà´•ൾ അദൃശàµà´¯à´®à´¾à´¯à´¿à´°à´¿à´•àµà´•àµà´‚ (മെറàµà´±à´¾à´¡àµ‡à´±àµà´± മാതàµà´°à´‚)" +locationLabel = "à´¸àµà´¥à´²à´‚:" +preview = "à´ªàµà´°à´¿à´µàµà´¯àµ‚" +reasonLabel = "കാരണം:" +title = "à´’à´ªàµà´ªàµ à´•àµà´°à´®àµ€à´•രണങàµà´™àµ¾ കോൺഫിഗർ ചെയàµà´¯àµà´•" +visible = "à´’à´ªàµà´ªàµà´•ൾ പേജൠ{{page}}-ൽ ദൃശàµà´¯à´®à´¾à´¯à´¿à´°à´¿à´•àµà´•àµà´‚" + +[groupSigning.steps.review] +document = "ഡോകàµà´¯àµà´®àµ†à´¨àµà´±àµ" +dueDate = "അവസാന തീയതി (à´à´šàµà´›à´¿à´•à´‚)" +dueDatePlaceholder = "അവസാന തീയതി തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•..." +invisible = "അദൃശàµà´¯à´®à´¾à´¯à´¤àµ (മെറàµà´±à´¾à´¡àµ‡à´±àµà´± മാതàµà´°à´‚)" +location = "à´¸àµà´¥à´²à´‚:" +logo = "ലോഗോ:" +logoHidden = "ലോഗോ ഇലàµà´²" +logoShown = "Stirling PDF ലോഗോ à´ªàµà´°à´¦àµ¼à´¶à´¿à´ªàµà´ªà´¿à´•àµà´•àµà´‚" +participants = "പങàµà´•ാളികൾ" +reason = "കാരണം:" +send = "à´’à´ªàµà´ªàµ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¨à´•ൾ അയയàµà´•àµà´•àµà´•" +signatureSettings = "à´’à´ªàµà´ªàµ à´•àµà´°à´®àµ€à´•രണങàµà´™àµ¾" +title = "സെഷൻ വിശദാംശങàµà´™àµ¾ പരിശോധികàµà´•àµà´•" +titleShort = "പരിശോധിചàµà´šàµ അയയàµà´•àµà´•àµà´•" +visibility = "ദൃശàµà´¯à´¤:" +visible = "പേജൠ{{page}}-ൽ ദൃശàµà´¯à´®à´¾à´¯à´¿" +participantCount = "{{count}} പങàµà´•ാളികൾ à´•àµà´°à´®à´¤àµà´¤à´¿àµ½ à´’à´ªàµà´ªà´¿à´Ÿàµà´‚" + +[groupSigning.steps.selectDocument] +continue = "പങàµà´•ാളികളെ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•ലിലേകàµà´•ൠതàµà´Ÿà´°àµà´•" +noFile = "ഒരൠസൈൻ സെഷൻ സൃഷàµà´Ÿà´¿à´•àµà´•ാൻ നിങàµà´™à´³àµà´Ÿàµ† സജീവ ഫയലàµà´•ളിൽ നിനàµà´¨àµ à´’à´±àµà´± PDF ഫയൽ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•." +selectedFile = "തിരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤ ഡോകàµà´¯àµà´®àµ†à´¨àµà´±àµ" +title = "ഡോകàµà´¯àµà´®àµ†à´¨àµà´±àµ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" + +[groupSigning.steps.selectParticipants] +continue = "à´’à´ªàµà´ªàµ à´•àµà´°à´®àµ€à´•രണങàµà´™à´³à´¿à´²àµ‡à´•àµà´•ൠതàµà´Ÿà´°àµà´•" +count = "{{count}} പങàµà´•ാളികളെ തിരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤àµ" +label = "പങàµà´•ാളികളെ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" +placeholder = "à´’à´ªàµà´ªà´¿à´Ÿà´¾àµ» പങàµà´•ാളികളെ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•..." +title = "പങàµà´•ാളികളെ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" + [getPdfInfo] downloadJson = "JSON ഡൗൺലോഡൠചെയàµà´¯àµà´•" downloads = "ഡൗൺലോഡàµà´•ൾ" @@ -4460,7 +4860,10 @@ zoomOut = "സൂം ഔടàµà´Ÿàµ" [viewer] cannotPreviewFile = "ഫയൽ à´ªàµà´°à´¿à´µàµà´¯àµ‚ ചെയàµà´¯à´¾àµ» കഴിയിലàµà´²" +disableColorFilter = "നിറ ഫിൽടàµà´Ÿàµ¼ à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´°à´¹à´¿à´¤à´®à´¾à´•àµà´•àµà´•" dualPageView = "à´°à´£àµà´Ÿàµà´ªàµ‡à´œàµ ദൃശàµà´¯à´‚" +enableDarkFilter = "ഡാർകàµà´•ൠഫിൽടàµà´Ÿàµ¼ à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´•àµà´·à´®à´®à´¾à´•àµà´•àµà´•" +enableSepiaFilter = "സെപിയ ഫിൽടàµà´Ÿàµ¼ à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´•àµà´·à´®à´®à´¾à´•àµà´•àµà´•" firstPage = "ആദàµà´¯ പേജàµ" lastPage = "അവസാന പേജàµ" nextPage = "à´…à´Ÿàµà´¤àµà´¤ പേജàµ" @@ -4470,6 +4873,22 @@ singlePageView = "à´’à´±àµà´± പേജൠദൃശàµà´¯à´‚" unknownFile = "അപരിചിതമായ ഫയൽ" zoomIn = "സൂം ഇൻ" zoomOut = "സൂം ഔടàµà´Ÿàµ" +resetZoom = "സൂം റീസെറàµà´±àµ ചെയàµà´¯àµà´•" + +[viewer.nonPdf] +fileTypeBadge = "{{type}} ഫയൽ" +convertToPdf = "PDF ആയി മാറികàµà´•àµà´•" +loading = "ലോഡൠചെയàµà´¯àµà´¨àµà´¨àµ..." +emptyFile = "ശൂനàµà´¯ ഫയൽ" +csvStats = "{{rows}} വരികൾ · {{columns}} നിരകൾ · {{size}}" +sortedBy = "à´•àµà´°à´®àµ€à´•à´°à´¿à´šàµà´šà´¤àµ: {{column}}" +columnDefault = "നിര {{index}}" +htmlPreviewWarning = "HTML à´ªàµà´°à´¿à´µàµà´¯àµ‚ — ബാഹàµà´¯ വിഭവങàµà´™àµ¾ ലോഡൠചെയàµà´¯à´¾à´¤à´¿à´°à´¿à´•àµà´•ാം · {{size}}" +htmlPreview = "HTML à´ªàµà´°à´¿à´µàµà´¯àµ‚" +invalidJson = "അസാധàµà´µà´¾à´¯ JSON — അസംസàµà´•ൃത ഉളàµà´³à´Ÿà´•àµà´•à´‚ കാണികàµà´•àµà´¨àµà´¨àµ" +textStats = "{{lines}} വരികൾ · {{size}}" +lineNumbers = "വരി നമàµà´ªà´±àµà´•ൾ" +renderMarkdown = "മാർകàµà´•àµà´¡àµ—ൺ റെൻഡർ ചെയàµà´¯àµà´•" [viewer.attachments] title = "à´…à´±àµà´±à´¾à´šàµà´šàµà´®àµ†à´¨àµà´±àµà´•ൾ" @@ -4531,6 +4950,7 @@ toggleAttachments = "à´…à´±àµà´±à´¾à´šàµà´šàµà´®àµ†à´¨àµà´±àµà´•ൾ à´•à´¾ toggleTheme = "തീം മാറàµà´±àµà´•" language = "ഭാഷ" toggleAnnotations = "അനോടàµà´Ÿàµ‡à´·àµ» ദൃശàµà´¯à´®à´¾à´¨à´‚ മാറàµà´±àµà´•" +toggleLayers = "ലെയറàµà´•ൾ ഓൺ/ഓഫൠചെയàµà´¯àµà´•" search = "PDF തിരയàµà´•" panMode = "പാൻ മോഡàµ" applyRedactionsFirst = "ആദàµà´¯à´‚ റെഡാകàµà´·à´¨àµà´•ൾ à´ªàµà´°à´¯àµ‹à´—à´¿à´•àµà´•àµà´•" @@ -5407,20 +5827,72 @@ title = "ഫയൽ à´…à´šàµà´šà´Ÿà´¿à´•àµà´•àµà´•" 2 = "à´ªàµà´°à´¿à´¨àµà´±àµ¼ പേരൠനൽകàµà´•" [quickAccess] +access = "ആകàµà´¸à´¸àµ" +accessAddPerson = "മറàµà´±àµŠà´°à´¾à´³àµ† ചേർകàµà´•àµà´•" +accessBack = "തിരികെ" +accessCopyLink = "ലിങàµà´•ൠപകർതàµà´¤àµà´•" +accessEmail = "ഇമെയിൽ വിലാസം" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "ഫയൽ" +accessGeneral = "സാധാരണ ആകàµà´¸à´¸àµ" +accessInviteTitle = "ആളàµà´•ളെ à´•àµà´·à´£à´¿à´•àµà´•àµà´•" +accessOwner = "ഉടമ" +accessPanel = "ഡോകàµà´¯àµà´®àµ†à´¨àµà´±àµ ആകàµà´¸à´¸àµ" +accessPeople = "ആകàµà´¸à´¸àµ ഉളàµà´³à´µàµ¼" +accessRemove = "നീകàµà´•àµà´•" +accessRestricted = "പരിമിതപàµà´ªàµ†à´Ÿàµà´¤àµà´¤à´¿à´¯" +accessRestrictedHint = "ആകàµà´¸à´¸àµ ഉളàµà´³à´µàµ¼à´•àµà´•ൠമാതàµà´°à´®àµ‡ à´¤àµà´±à´•àµà´•ാൻ കഴിയൂ" +accessRole = "പങàµà´•àµ" +accessRoleCommenter = "à´…à´­à´¿à´ªàµà´°à´¾à´¯à´•ർതàµà´¤à´¾à´µàµ" +accessRoleEditor = "à´Žà´¡à´¿à´±àµà´±àµ¼" +accessRoleViewer = "വീകàµà´·à´•ൻ" +accessSelectedFile = "തിരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤ ഫയൽ" +accessSendInvite = "à´•àµà´·à´£à´‚ അയയàµà´•àµà´•àµà´•" +accessTitle = "ഡോകàµà´¯àµà´®àµ†à´¨àµà´±àµ ആകàµà´¸à´¸àµ" +accessYou = "നിങàµà´™àµ¾" account = "à´…à´•àµà´•ൗണàµà´Ÿàµ" +activeSessions = "സജീവ സെഷനàµà´•ൾ" +activeTab = "സജീവം" activity = "à´šà´°à´¿à´¤àµà´°à´‚" adminSettings = "à´…à´¡àµà´®à´¿àµ» സെറàµà´±à´¿à´™àµà´™àµà´•ൾ" +allSessions = "à´Žà´²àµà´²à´¾ സെഷനàµà´•à´³àµà´‚" allTools = "All Tools" automate = "à´“à´Ÿàµà´Ÿàµ‹" +back = "തിരികെ" +certSign = "സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ à´’à´ªàµà´ªà´¿à´Ÿàµ½" +completedSessions = "പൂർതàµà´¤à´¿à´¯à´¾à´¯ സെഷനàµà´•ൾ" +completedTab = "പൂർതàµà´¤à´¿à´¯à´¾à´¯à´¿" config = "കോൺഫിഗàµ" +createNew = "à´ªàµà´¤à´¿à´¯ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¨ സൃഷàµà´Ÿà´¿à´•àµà´•àµà´•" +createSession = "സൈൻ ചെയàµà´¯àµ½ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¨ സൃഷàµà´Ÿà´¿à´•àµà´•àµà´•" +dueDate = "അവസാന തീയതി (à´à´šàµà´›à´¿à´•à´‚)" files = "ഫയലàµà´•ൾ" help = "സഹായം" +noActiveSessions = "പെൻഡിംഗൠഒപàµà´ªàµ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¨à´•ളോ സജീവ സെഷനàµà´•ളോ ഇലàµà´²" +noCompletedSessions = "പൂർതàµà´¤à´¿à´¯à´¾à´¯ സെഷനàµà´•ളൊനàµà´¨àµà´®à´¿à´²àµà´²" +noFile = "ഫയൽ à´’à´¨àµà´¨àµà´‚ തെരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤à´¿à´Ÿàµà´Ÿà´¿à´²àµà´²" read = "വായന" reader = "റീഡർ" +refresh = "റിഫàµà´°àµ†à´·àµ ചെയàµà´¯àµà´•" +requestSignatures = "à´’à´ªàµà´ªàµà´•ൾ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¿à´•àµà´•àµà´•" +selectSingleFileToRequest = "à´’à´ªàµà´ªàµà´•ൾ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¿à´•àµà´•ാൻ ഒരൠPDF ഫയൽ മാതàµà´°à´‚ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" +selectedFile = "തിരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤ ഫയൽ" +selectUsers = "à´’à´ªàµà´ªà´¿à´Ÿà´¾àµ» ഉപയോകàµà´¤à´¾à´•àµà´•ളെ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•" +selectUsersPlaceholder = "പങàµà´•ാളികളെ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•..." +sendingRequest = "അയകàµà´•àµà´¨àµà´¨àµ..." settings = "സെറàµà´±à´¿à´™àµà´™àµà´•ൾ" showMeAround = "എനികàµà´•ൠകാണിചàµà´šàµ തരൂ" sign = "à´’à´ªàµà´ªà´¿à´Ÿàµà´•" +signatureRequests = "à´’à´ªàµà´ªàµ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¨à´•ൾ" +signYourself = "നിങàµà´™àµ¾ തനàµà´¨àµ† à´’à´ªàµà´ªà´¿à´Ÿàµà´•" +newRequest = "à´ªàµà´¤à´¿à´¯ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¨" tours = "ടൂറàµà´•ൾ" +wetSign = "à´’à´ªàµà´ªàµ ചേർകàµà´•àµà´•" +filterMine = "à´Žà´¨àµà´±àµ†à´¤àµ" +filterOverdue = "കാലാവധി à´•à´´à´¿à´žàµà´žà´¤àµ" +filterSigned = "à´’à´ªàµà´ªà´¿à´Ÿàµà´Ÿà´¤àµ" +filterDeclined = "നിരസിചàµà´šà´¤àµ" +searchDocuments = "ഡോകàµà´¯àµà´®àµ†à´¨àµà´±àµà´•ൾ തിരയàµà´•…" [quickAccess.helpMenu] adminTour = "à´…à´¡àµà´®à´¿àµ» പരിചയം" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "നിങàµà´™à´³àµà´Ÿàµ† Stirling-PDF സെർവ expired = "നിങàµà´™à´³àµà´Ÿàµ† സെഷൻ കാലഹരണപàµà´ªàµ†à´Ÿàµà´Ÿàµ. ദയവായി പേജൠപàµà´¤àµà´•àµà´•à´¿ വീണàµà´Ÿàµà´‚ à´¶àµà´°à´®à´¿à´•àµà´•àµà´•." refreshPage = "പേജൠപàµà´¤àµà´•àµà´•àµà´•" +[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 = "à´’à´ªàµà´ªà´¿à´Ÿàµ½ à´•àµà´°à´®à´‚" + +[signatureSettings.tooltip] +header = "à´’à´ªàµà´ªà´¿à´¨àµà´±àµ† രൂപഭാവ à´•àµà´°à´®àµ€à´•രണങàµà´™àµ¾" + +[signatureSettings.tooltip.location] +bullet1 = "ഉദാഹരണങàµà´™àµ¾: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "പേജിലെ à´¸àµà´¥à´¾à´¨à´µàµà´®à´¾à´¯à´¿ ഇതൊനàµà´¨àµà´®à´²àµà´²" +bullet3 = "à´šà´¿à´² നിയമപരമായ അധികാരപàµà´°à´¦àµ‡à´¶à´™àµà´™àµ¾ ആവശàµà´¯à´ªàµà´ªàµ†à´Ÿà´¾à´‚" +description = "à´’à´ªàµà´ªàµ à´ªàµà´°à´¯àµ‹à´—à´¿à´šàµà´š ഭൗഗോളിക à´¸àµà´¥à´²à´‚ (à´à´šàµà´›à´¿à´•à´‚). സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ മെറàµà´±à´¾à´¡àµ‡à´±àµà´±à´¯à´¿àµ½ സേവൠചെയàµà´¯à´ªàµà´ªàµ†à´Ÿàµà´¨àµà´¨àµ." +title = "à´’à´ªàµà´ªà´¿à´¨àµà´±àµ† à´¸àµà´¥à´²à´‚" + +[signatureSettings.tooltip.logo] +bullet1 = "à´’à´ªàµà´ªà´¿à´¨àµà´±àµ†à´¯àµà´‚ വാചകതàµà´¤à´¿à´¨àµà´±àµ†à´¯àµà´‚ കൂടെ à´ªàµà´°à´¦àµ¼à´¶à´¿à´ªàµà´ªà´¿à´•àµà´•àµà´¨àµà´¨àµ" +bullet2 = "PNG, JPG ഫോർമാറàµà´±àµà´•ൾ പിനàµà´¤àµà´£à´¯àµà´•àµà´•àµà´¨àµà´¨àµ" +bullet3 = "തൊഴിൽപരമായ രൂപം മെചàµà´šà´ªàµà´ªàµ†à´Ÿàµà´¤àµà´¤àµà´¨àµà´¨àµ" +description = "à´¬àµà´°à´¾àµ»à´¡à´¿à´‚à´—à´¿à´¨àµà´‚ വിശàµà´µà´¾à´¸àµà´¯à´¤à´¯àµà´•àµà´•àµà´®à´¾à´¯à´¿ ദൃശàµà´¯ à´’à´ªàµà´ªàµà´•ളിൽ à´•à´®àµà´ªà´¨à´¿ ലോഗോ ചേർകàµà´•àµà´•." +title = "à´•à´®àµà´ªà´¨à´¿ ലോഗോ" + +[signatureSettings.tooltip.reason] +bullet1 = "ഉദാഹരണങàµà´™àµ¾: \"Approval\", \"Contract Agreement\", \"Review Complete\"" +bullet2 = "PDF സിഗàµà´¨àµ‡à´šàµà´šàµ¼ à´ªàµà´°àµ‹à´ªàµà´ªàµ¼à´Ÿàµà´Ÿàµ€à´¸à´¿àµ½ ദൃശàµà´¯à´®à´¾à´•àµà´‚" +bullet3 = "à´“à´¡à´¿à´±àµà´±àµ à´Ÿàµà´°àµ†à´¯à´¿à´²àµà´•ൾകàµà´•àµà´‚ നിയമാനàµà´¸àµƒà´¤à´¤à´¯àµà´•àµà´•àµà´‚ ഉപകരികàµà´•àµà´¨àµà´¨àµ" +description = "ഡോകàµà´¯àµà´®àµ†à´¨àµà´±àµ à´’à´ªàµà´ªà´¿à´Ÿà´¾à´¨àµà´£àµà´Ÿà´¾à´¯ കാരണം വിശദീകരികàµà´•àµà´¨àµà´¨ à´à´šàµà´›à´¿à´• വാചകം. സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ മെറàµà´±à´¾à´¡àµ‡à´±àµà´±à´¯à´¿àµ½ സംഭരികàµà´•àµà´¨àµà´¨àµ." +title = "à´’à´ªàµà´ªà´¿à´¨àµà´±àµ† കാരണം" + +[signatureSettings.tooltip.visibility] +bullet1 = "ദൃശàµà´¯à´‚: ഇഷàµà´Ÿà´¾à´¨àµà´¸àµƒà´¤ രൂപഭാവതàµà´¤àµ‹à´Ÿàµ† PDF-ൽ à´’à´ªàµà´ªàµ à´ªàµà´°à´¤àµà´¯à´•àµà´·à´ªàµà´ªàµ†à´Ÿàµà´‚" +bullet2 = "അദൃശàµà´¯à´‚: ദൃശàµà´¯ അടയാളമിലàµà´²à´¾à´¤àµ† സർടàµà´Ÿà´¿à´«à´¿à´•àµà´•à´±àµà´±àµ എംബെഡൠചെയàµà´¯àµà´‚" +bullet3 = "അദൃശàµà´¯ à´’à´ªàµà´ªàµà´•ൾകàµà´•àµà´‚ à´•àµà´°à´¿à´ªàµà´±àµà´±àµ‹à´—àµà´°à´¾à´«à´¿à´•ൠസാധൂകരണം ലഭàµà´¯à´®à´¾à´£àµ" +description = "à´’à´ªàµà´ªàµ ഡോകàµà´¯àµà´®àµ†à´¨àµà´±à´¿àµ½ ദൃശàµà´¯à´®à´¾à´•ണോ അദൃശàµà´¯à´®à´¾à´•ണോ à´Žà´¨àµà´¨à´¤àµ നിയനàµà´¤àµà´°à´¿à´•àµà´•àµà´¨àµà´¨àµ." +title = "à´’à´ªàµà´ªà´¿à´¨àµà´±àµ† ദൃശàµà´¯à´¤" + [settings.configuration] advanced = "à´…à´¡àµà´µà´¾àµ»à´¸àµà´¡àµ" database = "ഡാറàµà´±à´¾à´¬àµ‡à´¸àµ" endpoints = "എൻഡàµà´ªàµ‹à´¯à´¿à´¨àµà´±àµà´•ൾ" features = "ഫീചàµà´šà´±àµà´•ൾ" +storageSharing = "ഫയൽ സംഭരണവàµà´‚ പങàµà´•à´¿à´Ÿà´²àµà´‚" systemSettings = "സിസàµà´±àµà´±à´‚ à´•àµà´°à´®àµ€à´•രണങàµà´™àµ¾" title = "കോൺഫിഗറേഷൻ" @@ -6332,10 +6868,13 @@ title = "Stirling-യിൽ സൈൻ ഇൻ ചെയàµà´¯àµà´•" [setup.selfhosted] link = "à´…à´²àµà´²àµ†à´™àµà´•ിൽ à´¸àµà´µà´¯à´‚-ഹോസàµà´±àµà´±àµà´šàµ†à´¯àµà´¤ à´…à´•àµà´•ൗണàµà´Ÿàµà´®à´¾à´¯à´¿ ബനàµà´§à´¿à´ªàµà´ªà´¿à´•àµà´•àµà´•" subtitle = "നിങàµà´™à´³àµà´Ÿàµ† സെർവർ à´•àµà´°àµ†à´¡àµ»à´·àµà´¯à´²àµà´•ൾ നൽകàµà´•" +changeServerLocked = "നിങàµà´™à´³àµà´Ÿàµ† à´¸àµà´¥à´¾à´ªà´¨o à´ˆ ആപàµà´ªàµ ഒരൠപàµà´°à´¤àµà´¯àµ‡à´• സെർവറിലേകàµà´•ൠപരിമിതപàµà´ªàµ†à´Ÿàµà´¤àµà´¤à´¿à´¯à´¿à´°à´¿à´•àµà´•àµà´¨àµà´¨àµ" switchToLocal = "പകരം ലോകàµà´•ൽ ഉപകരണങàµà´™àµ¾ ഉപയോഗികàµà´•àµà´•" title = "സെർവറിൽ സൈൻ ഇൻ ചെയàµà´¯àµà´•" [setup.selfhosted.unreachable] +changeServer = "മറàµà´±àµŠà´°àµ സെർവറിലേകàµà´•ൠകണകàµà´±àµà´±àµ ചെയàµà´¯àµà´•" +changeServerLocked = "നിങàµà´™à´³àµà´Ÿàµ† à´¸àµà´¥à´¾à´ªà´¨o à´ˆ ആപàµà´ªàµ ഒരൠപàµà´°à´¤àµà´¯àµ‡à´• സെർവറിലേകàµà´•ൠപരിമിതപàµà´ªàµ†à´Ÿàµà´¤àµà´¤à´¿à´¯à´¿à´°à´¿à´•àµà´•àµà´¨àµà´¨àµ" continueOffline = "പകരം ലോകàµà´•ൽ ഉപകരണങàµà´™àµ¾ ഉപയോഗികàµà´•àµà´•" message = "{{url}} à´Žà´¤àµà´¤à´¿à´šàµà´šàµ‡à´°à´¾àµ» à´•à´´à´¿à´žàµà´žà´¿à´²àµà´². സെർവർ à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¿à´•àµà´•àµà´•യാണോ ആകàµâ€Œà´¸à´¸àµ ചെയàµà´¯à´¾à´¨à´¾à´•àµà´¨àµà´¨àµà´µàµ‹à´¯àµ†à´¨àµà´¨àµ പരിശോധികàµà´•àµà´•." retry = "വീണàµà´Ÿàµà´‚ à´¶àµà´°à´®à´¿à´•àµà´•àµà´•" @@ -6529,6 +7068,15 @@ saved = "സേവൠചെയàµà´¤à´¤àµ" text = "വാചകം" title = "à´’à´ªàµà´ªàµ തരം" +[signRequest] +declined = "à´’à´ªàµà´ªàµ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¨ നിരസിചàµà´šàµ" +fetchFailed = "à´’à´ªàµà´ªàµ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¨ ലോഡൠചെയàµà´¯à´¾àµ» à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²" +signed = "ഡോകàµà´¯àµà´®àµ†à´¨àµà´±àµ വിജയകരമായി à´’à´ªàµà´ªà´¿à´Ÿàµà´Ÿàµ" + +[signSession] +createFailed = "സൈൻ ചെയàµà´¯àµ½ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¨ സൃഷàµà´Ÿà´¿à´•àµà´•ാൻ à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²" +created = "സൈൻ ചെയàµà´¯àµ½ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¨ അയചàµà´šàµ" + [signup] accountCreatedSuccessfully = "à´…à´•àµà´•ൗണàµà´Ÿàµ വിജയകരമായി സൃഷàµà´Ÿà´¿à´šàµà´šàµ! നിങàµà´™àµ¾à´•àµà´•ൠഇപàµà´ªàµ‹àµ¾ സൈൻ ഇൻ ചെയàµà´¯à´¾à´‚." alreadyHaveAccount = "ഇതിനകം à´…à´•àµà´•ൗണàµà´Ÿàµ ഉണàµà´Ÿàµ‹? സൈൻ ഇൻ ചെയàµà´¯àµà´•" @@ -6807,6 +7355,106 @@ title = "à´…à´§àµà´¯à´¾à´¯à´™àµà´™àµ¾ à´…à´¨àµà´¸à´°à´¿à´šàµà´šàµ PDF à´µ [splitPdfByChapters] tags = "വിഭജികàµà´•àµà´•,à´…à´§àµà´¯à´¾à´¯à´™àµà´™àµ¾,à´¬àµà´•àµà´•àµà´®à´¾àµ¼à´•àµà´•àµà´•ൾ,à´•àµà´°à´®àµ€à´•à´°à´¿à´•àµà´•àµà´•" +[storageShare] +accessed = "ആകàµâ€Œà´¸à´¸àµ ചെയàµà´¤àµ" +accessDenied = "à´ˆ ഷെയർ ചെയàµà´¤ ഫയലിലേകàµà´•ൠനിങàµà´™àµ¾à´•àµà´•ൠആകàµâ€Œà´¸à´¸àµ ഇലàµà´². ഉടമയോടൠഇതൠനിങàµà´™à´³àµà´®à´¾à´¯à´¿ പങàµà´•ിടാൻ ആവശàµà´¯à´ªàµà´ªàµ†à´Ÿàµà´•." +accessFailed = "à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´™àµà´™àµ¾ ലോഡൠചെയàµà´¯à´¾àµ» à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²." +accessDeniedBody = "à´ˆ ഫയലിലേകàµà´•ൠനിങàµà´™àµ¾à´•àµà´•ൠആകàµâ€Œà´¸à´¸àµ ഇലàµà´². ഉടമയോടൠഅതൠനിങàµà´™à´³àµà´®à´¾à´¯à´¿ പങàµà´•ിടാൻ ആവശàµà´¯à´ªàµà´ªàµ†à´Ÿàµà´•." +accessDeniedTitle = "ആകàµâ€Œà´¸à´¸àµ ഇലàµà´²" +accessLimitedCommenter = "à´…à´­à´¿à´ªàµà´°à´¾à´¯ ആകàµâ€Œà´¸à´¸àµ ഉടൻ ലഭàµà´¯à´®à´¾à´•àµà´‚. ഡൗൺലോഡൠവേണമെങàµà´•ിൽ ഉടമയോടൠഎഡിറàµà´±àµ¼ ആകàµâ€Œà´¸à´¸àµ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¿à´•àµà´•àµà´•." +accessLimitedTitle = "പരിമിത ആകàµâ€Œà´¸à´¸àµ" +accessLimitedViewer = "à´ˆ ലിങàµà´•ൠകാണàµà´¨àµà´¨à´¤à´¿à´¨à´¾à´¯à´¿ മാതàµà´°à´®à´¾à´£àµ. ഡൗൺലോഡൠവേണമെങàµà´•ിൽ ഉടമയോടൠഎഡിറàµà´±àµ¼ ആകàµâ€Œà´¸à´¸àµ à´…à´­àµà´¯àµ¼à´¤àµà´¥à´¿à´•àµà´•àµà´•." +createdAt = "സൃഷàµà´Ÿà´¿à´šàµà´šà´¤àµ" +download = "ഡൗൺലോഡàµ" +downloadFailed = "à´ˆ ഫയൽ ഡൗൺലോഡൠചെയàµà´¯à´¾àµ» à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²." +expiredBody = "à´ˆ ഷെയർ ലിങàµà´•ൠഅസാധàµà´µà´¾à´£àµ à´…à´²àµà´²àµ†à´™àµà´•ിൽ കാലഹരണപàµà´ªàµ†à´Ÿàµà´Ÿà´¿à´°à´¿à´•àµà´•àµà´¨àµà´¨àµ." +expiredTitle = "ലിങàµà´•à´¿à´¨àµà´±àµ† കാലാവധി à´•à´´à´¿à´žàµà´žàµ" +goToLogin = "ലോഗിനിലേകàµà´•ൠപോകàµà´•" +loadFailed = "പങàµà´•à´¿à´Ÿàµà´Ÿ ഫയൽ à´¤àµà´±à´•àµà´•ാൻ à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²." +loading = "ഷെയർ ലിങàµà´•ൠലോഡൠചെയàµà´¯àµà´¨àµà´¨àµ..." +loginPrompt = "à´ˆ പങàµà´•à´¿à´Ÿàµà´Ÿ ഫയലിൽ à´ªàµà´°à´µàµ‡à´¶à´¿à´•àµà´•ാൻ സൈൻ ഇൻ ചെയàµà´¯àµà´•." +loginRequired = "ലോഗിൻ ആവശàµà´¯à´®à´¾à´£àµ" +openInApp = "Stirling PDF-ൽ à´¤àµà´±à´•àµà´•àµà´•" +ownerLabel = "ഉടമ" +ownerUnknown = "à´…à´œàµà´žà´¾à´¤à´‚" +requiresLogin = "à´ˆ പങàµà´•à´¿à´Ÿàµà´Ÿ ഫയലിനൠലോഗിൻ ആവശàµà´¯à´®à´¾à´£àµ." +roleCommenter = "à´…à´­à´¿à´ªàµà´°à´¾à´¯à´•ർതàµà´¤à´¾à´µàµ" +roleEditor = "à´Žà´¡à´¿à´±àµà´±àµ¼" +roleViewer = "വീകàµà´·à´•ൻ" +shareHeading = "പങàµà´•à´¿à´Ÿàµà´Ÿ ഫയൽ" +titleDefault = "പങàµà´•à´¿à´Ÿàµà´Ÿ ഫയൽ" +tryAgain = "ദയവായി പിനàµà´¨àµ€à´Ÿàµ വീണàµà´Ÿàµà´‚ à´¶àµà´°à´®à´¿à´•àµà´•àµà´•." +addUser = "ചേർകàµà´•àµà´•" +commenterHint = "à´…à´­à´¿à´ªàµà´°à´¾à´¯ രേഖപàµà´ªàµ†à´Ÿàµà´¤àµà´¤àµ½ ഉടൻ വരàµà´¨àµà´¨àµ." +copied = "ലിങàµà´•ൠകàµà´²à´¿à´ªàµà´ªàµâ€Œà´¬àµ‹àµ¼à´¡à´¿à´²àµ‡à´•àµà´•ൠപകർതàµà´¤à´¿" +copy = "പകർതàµà´¤àµà´•" +copyFailed = "പകർതàµà´¤àµ½ പരാജയപàµà´ªàµ†à´Ÿàµà´Ÿàµ" +description = "à´ˆ ഫയലിനായി ഒരൠഷെയർ ലിങàµà´•ൠസൃഷàµà´Ÿà´¿à´•àµà´•àµà´•. ലിങàµà´•àµà´³àµà´³ സൈൻ-ഇൻ ചെയàµà´¤ ഉപയോകàµà´¤à´¾à´•àµà´•ൾകàµà´•ൠആകàµà´¸à´¸àµ ലഭികàµà´•àµà´‚." +downloadsCount = "ഡൗൺലോഡàµà´•ൾ: {{count}}" +emailWarningBody = "ഇതൠഒരൠഇമെയിൽ വിലാസം പോലെയാണൠതോനàµà´¨àµà´¨àµà´¨à´¤àµ. à´† à´µàµà´¯à´•àµà´¤à´¿ Stirling PDF ഉപയോകàµà´¤à´¾à´µà´²àµà´²àµ†à´™àµà´•ിൽ, അവർകàµà´•ൠഫയലിൽ à´ªàµà´°à´µàµ‡à´¶à´¿à´•àµà´•ാൻ കഴിയിലàµà´²." +emailWarningConfirm = "à´Žà´™àµà´•à´¿à´²àµà´‚ പങàµà´•à´¿à´Ÿàµà´•" +emailWarningTitle = "ഇമെയിൽ വിലാസം" +errorTitle = "പങàµà´•ിടൽ പരാജയപàµà´ªàµ†à´Ÿàµà´Ÿàµ" +failure = "ഒരൠഷെയർ ലിങàµà´•ൠസൃഷàµà´Ÿà´¿à´•àµà´•ാൻ à´•à´´à´¿à´žàµà´žà´¿à´²àµà´². ദയവായി വീണàµà´Ÿàµà´‚ à´¶àµà´°à´®à´¿à´•àµà´•àµà´•." +fileLabel = "ഫയൽ" +generate = "ലിങàµà´•ൠസൃഷàµà´Ÿà´¿à´•àµà´•àµà´•" +generated = "ഷെയർ ലിങàµà´•ൠസൃഷàµà´Ÿà´¿à´šàµà´šàµ" +hideActivity = "à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´‚ മറയàµà´•àµà´•àµà´•" +invalidUsername = "സാധàµà´µà´¾à´¯ യൂസർനെയിം à´…à´²àµà´²àµ†à´™àµà´•ിൽ ഇമെയിൽ വിലാസം നൽകàµà´•." +lastAccessed = "അവസാനം ആകàµà´¸à´¸àµ ചെയàµà´¤à´¤àµ" +linkAccessTitle = "ഷെയർ ലിങàµà´•ൠആകàµà´¸à´¸àµ" +linkLabel = "ഷെയർ ലിങàµà´•àµ" +linksDisabled = "ഷെയർ ലിങàµà´•àµà´•ൾ à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´°à´¹à´¿à´¤à´®à´¾à´•àµà´•ിയിരികàµà´•àµà´¨àµà´¨àµ." +linksDisabledBody = "നിങàµà´™à´³àµà´Ÿàµ† സെർവർ à´•àµà´°à´®àµ€à´•രണങàµà´™àµ¾ ഷെയർ ലിങàµà´•àµà´•ൾ à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´°à´¹à´¿à´¤à´®à´¾à´•àµà´•ിയിരികàµà´•àµà´¨àµà´¨àµ." +manage = "പങàµà´•ിടൽ നിയനàµà´¤àµà´°à´¿à´•àµà´•àµà´•" +manageDescription = "à´ˆ ഫയൽ പങàµà´•ിടാൻ ലിങàµà´•àµà´•ൾ സൃഷàµà´Ÿà´¿à´•àµà´•àµà´•à´¯àµà´‚ നിയനàµà´¤àµà´°à´¿à´•àµà´•àµà´•à´¯àµà´‚ ചെയàµà´¯àµà´•." +manageLoadFailed = "ഷെയർ ലിങàµà´•àµà´•ൾ ലോഡൠചെയàµà´¯à´¾àµ» à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²." +manageTitle = "പങàµà´•ിടൽ നിയനàµà´¤àµà´°à´£à´‚" +noActivity = "ഇനàµà´¨àµà´µà´°àµ† à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´™àµà´™à´³àµŠà´¨àµà´¨àµà´®à´¿à´²àµà´²." +noLinks = "സജീവ ഷെയർ ലിങàµà´•àµà´•ളൊനàµà´¨àµà´®à´¿à´²àµà´²." +noSharedUsers = "ഇനിയàµà´‚ ആരàµà´•àµà´•àµà´‚ ആകàµà´¸à´¸àµ നൽകിയിടàµà´Ÿà´¿à´²àµà´²." +removeLink = "ലിങàµà´•ൠനീകàµà´•àµà´•" +removeUser = "നീകàµà´•àµà´•" +revokeFailed = "ഷെയർ ലിങàµà´•ൠനീകàµà´•ാൻ à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²." +revoked = "പങàµà´•ിടൽ ലിങàµà´•ൠനീകàµà´•à´‚ ചെയàµà´¤àµ" +roleLabel = "പങàµà´•àµ" +sharingDisabled = "പങàµà´•ിടൽ à´…à´ªàµà´°à´¾à´ªàµà´¤à´®à´¾à´•àµà´•ിയിരികàµà´•àµà´¨àµà´¨àµ." +sharingDisabledBody = "നിങàµà´™à´³àµà´Ÿàµ† സെർവർ à´•àµà´°à´®àµ€à´•രണങàµà´™à´³à´¾àµ½ പങàµà´•ിടൽ à´…à´ªàµà´°à´¾à´ªàµà´¤à´®à´¾à´•àµà´•ിയിരികàµà´•àµà´¨àµà´¨àµ." +sharedUsersTitle = "പങàµà´•à´¿à´Ÿàµà´Ÿ ഉപയോകàµà´¤à´¾à´•àµà´•ൾ" +title = "ഫയൽ പങàµà´•à´¿à´Ÿàµà´•" +unknownUser = "à´…à´œàµà´žà´¾à´¤ ഉപയോകàµà´¤à´¾à´µàµ" +userAddFailed = "à´† ഉപയോകàµà´¤à´¾à´µàµà´®à´¾à´¯à´¿ പങàµà´•ിടാൻ à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²." +userAdded = "പങàµà´•à´¿à´Ÿàµà´Ÿ പടàµà´Ÿà´¿à´•യിൽ ഉപയോകàµà´¤à´¾à´µà´¿à´¨àµ† ചേർതàµà´¤àµ." +usernameLabel = "ഉപയോകàµà´¤àµƒà´¨à´¾à´®à´‚ à´…à´²àµà´²àµ†à´™àµà´•ിൽ ഇമെയിൽ" +usernamePlaceholder = "ഉപയോകàµà´¤àµƒà´¨à´¾à´®à´®àµ‹ ഇമെയിലോ നൽകàµà´•" +userRemoveFailed = "à´† ഉപയോകàµà´¤à´¾à´µà´¿à´¨àµ† നീകàµà´•à´‚ ചെയàµà´¯à´¾àµ» à´•à´´à´¿à´žàµà´žà´¿à´²àµà´²." +userRemoved = "പങàµà´•à´¿à´Ÿàµà´Ÿ പടàµà´Ÿà´¿à´•യിൽ നിനàµà´¨àµ ഉപയോകàµà´¤à´¾à´µà´¿à´¨àµ† നീകàµà´•à´¿." +viewActivity = "à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´™àµà´™àµ¾ കാണàµà´•" +viewed = "à´•à´£àµà´Ÿàµ" +viewsCount = "കാഴàµà´šà´•ൾ: {{count}}" +downloaded = "ഡൗൺലോഡൠചെയàµà´¤àµ" +bulkDescription = "തിരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤ à´Žà´²àµà´²à´¾ ഫയലàµà´•à´³àµà´‚ സൈൻ-ഇൻ ചെയàµà´¤ ഉപയോകàµà´¤à´¾à´•àµà´•à´³àµà´®à´¾à´¯à´¿ പങàµà´•ിടാൻ ഒരൠലിങàµà´•ൠസൃഷàµà´Ÿà´¿à´•àµà´•àµà´•." +bulkTitle = "തിരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤ ഫയലàµà´•ൾ പങàµà´•à´¿à´Ÿàµà´•" +copyLink = "പങàµà´•ിടൽ ലിങàµà´•ൠപകർതàµà´¤àµà´•" +fileCount = "{{count}} ഫയലàµà´•ൾ തിരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤àµ" +ownerOnly = "ഉടമയàµà´•àµà´•àµà´®à´¾à´¤àµà´°à´®àµ‡ പങàµà´•ിടൽ നിയനàµà´¤àµà´°à´¿à´•àµà´•ാൻ കഴിയൂ." +selectSingleFile = "പങàµà´•ിടൽ നിയനàµà´¤àµà´°à´¿à´•àµà´•ാൻ à´’à´±àµà´± ഫയൽ തിരഞàµà´žàµ†à´Ÿàµà´•àµà´•àµà´•." + +[storageUpload] +description = "ഇതൠനിലവിലെ ഫയൽ നിങàµà´™à´³àµà´Ÿàµ‡à´¤à´¾à´¯ ആകàµà´¸à´¸à´¿à´¨à´¾à´¯à´¿ സെർവർ à´¸àµà´±àµà´±àµ‹à´±àµ‡à´œà´¿à´²àµ‡à´•àµà´•ൠഅപàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¯àµà´‚." +errorTitle = "à´…à´ªàµâ€Œà´²àµ‹à´¡àµ പരാജയപàµà´ªàµ†à´Ÿàµà´Ÿàµ" +failure = "à´…à´ªàµâ€Œà´²àµ‹à´¡àµ പരാജയപàµà´ªàµ†à´Ÿàµà´Ÿàµ. ദയവായി നിങàµà´™à´³àµà´Ÿàµ† ലോഗിൻ, à´¸àµà´±àµà´±àµ‹à´±àµ‡à´œàµ à´•àµà´°à´®àµ€à´•രണങàµà´™àµ¾ പരിശോധികàµà´•àµà´•." +fileLabel = "ഫയൽ" +hint = "പബàµà´²à´¿à´•ൠലിങàµà´•àµà´•à´³àµà´‚ ആകàµà´¸à´¸àµ മോഡàµà´•à´³àµà´‚ നിങàµà´™à´³àµà´Ÿàµ† സെർവർ à´•àµà´°à´®àµ€à´•രണങàµà´™àµ¾ നിയനàµà´¤àµà´°à´¿à´•àµà´•àµà´¨àµà´¨àµ." +success = "സെർവറിലേകàµà´•ൠഅപàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¤àµ" +title = "സെർവറിലേകàµà´•ൠഅപàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¯àµà´•" +updateButton = "സെർവറിൽ à´…à´ªàµâ€Œà´¡àµ‡à´±àµà´±àµ ചെയàµà´¯àµà´•" +uploadButton = "സെർവറിലേകàµà´•ൠഅപàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¯àµà´•" +bulkDescription = "ഇതൠതെരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤ ഫയലàµà´•ൾ നിങàµà´™à´³àµà´Ÿàµ† സെർവർ à´¸àµà´±àµà´±àµ‹à´±àµ‡à´œà´¿à´²àµ‡à´•àµà´•ൠഅപàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¯àµà´‚." +bulkTitle = "തിരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤ ഫയലàµà´•ൾ à´…à´ªàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¯àµà´•" +fileCount = "{{count}} ഫയലàµà´•ൾ തിരഞàµà´žàµ†à´Ÿàµà´¤àµà´¤àµ" +more = " +{{count}} കൂടàµà´¤àµ½" + [storage] approximateSize = "à´à´•ദേശ വലàµà´ªàµà´ªà´‚" fileTooLarge = "ഫയൽ വളരെ വലàµà´¤à´¾à´£àµ. ഓരോ ഫയലിനàµà´‚ à´…à´¨àµà´µà´¦à´¨àµ€à´¯à´®à´¾à´¯ പരമാവധി വലàµà´ªàµà´ªà´‚" @@ -7153,6 +7801,30 @@ title = "PDF കാണàµà´•/തിരàµà´¤àµà´¤àµà´•" [warning] tooltipTitle = "à´®àµà´¨àµà´¨à´±à´¿à´¯à´¿à´ªàµà´ªàµ" +[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 = "à´•à´¯àµà´¯àµŠà´ªàµà´ªà´¿à´¨àµà´±àµ† à´šà´¿à´¤àµà´°à´‚ à´…à´ªàµâ€Œà´²àµ‹à´¡àµ ചെയàµà´¯àµà´•" + [watermark] completed = "വാടàµà´Ÿàµ¼à´®à´¾àµ¼à´•àµà´•ൠചേർതàµà´¤àµ" desc = "PDF ഫയലàµà´•ളിൽ ടെകàµà´¸àµà´±àµà´±àµ à´…à´²àµà´²àµ†à´™àµà´•ിൽ ഇമേജൠവാടàµà´Ÿàµ¼à´®à´¾àµ¼à´•àµà´•àµà´•ൾ ചേർകàµà´•àµà´•" @@ -7333,6 +8005,7 @@ activeSession = "സജീവ സെഷൻ" addMembers = "à´…à´‚à´—à´™àµà´™à´³àµ† ചേർകàµà´•àµà´•" admin = "à´…à´¡àµà´®à´¿àµ»" confirmDelete = "à´ˆ ഉപയോകàµà´¤à´¾à´µà´¿à´¨àµ† ഇലàµà´²à´¾à´¤à´¾à´•àµà´•ണോ? ഇതൠതിരിചàµà´šàµ†à´Ÿàµà´•àµà´•ാൻ കഴിയിലàµà´²." +confirmUnlock = "à´ˆ ഉപയോകàµà´¤àµƒ à´…à´•àµà´•ൗണàµà´Ÿàµ അൺലോകàµà´•ൠചെയàµà´¯à´£à´®àµ†à´¨àµà´¨àµ നിങàµà´™àµ¾à´•àµà´•ൠഉറപàµà´ªà´¾à´£àµ‹?" deleteUser = "ഉപയോകàµà´¤à´¾à´µà´¿à´¨àµ† ഇലàµà´²à´¾à´¤à´¾à´•àµà´•àµà´•" deleteUserError = "ഉപയോകàµà´¤à´¾à´µà´¿à´¨àµ† ഇലàµà´²à´¾à´¤à´¾à´•àµà´•ൽ പരാജയപàµà´ªàµ†à´Ÿàµà´Ÿàµ" deleteUserSuccess = "ഉപയോകàµà´¤à´¾à´µà´¿à´¨àµ† വിജയകരമായി ഇലàµà´²à´¾à´¤à´¾à´•àµà´•à´¿" @@ -7341,6 +8014,8 @@ disable = "à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´°à´¹à´¿à´¤à´®à´¾à´•àµà´•àµà´•" disabled = "à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´°à´¹à´¿à´¤à´‚" editRole = "റോൾ à´Žà´¡à´¿à´±àµà´±àµ ചെയàµà´¯àµà´•" enable = "à´ªàµà´°à´µàµ¼à´¤àµà´¤à´¨à´•àµà´·à´®à´®à´¾à´•àµà´•àµà´•" +locked = "ലോകàµà´•àµà´šàµ†à´¯àµà´¤à´¿à´°à´¿à´•àµà´•àµà´¨àµà´¨àµ" +lockedBadge = "ലോകàµà´•àµà´šàµ†à´¯àµà´¤" loading = "à´…à´‚à´—à´™àµà´™à´³àµ† ലോഡൠചെയàµà´¯àµà´¨àµà´¨àµ..." loginRequired = "ആദàµà´¯à´‚ ലോഗിൻ മോഡൠപàµà´°à´¾à´ªàµà´¤à´®à´¾à´•àµà´•àµà´•" member = "à´…à´‚à´—à´‚" @@ -7350,6 +8025,9 @@ searchMembers = "à´…à´‚à´—à´™àµà´™à´³àµ† തിരയàµà´•..." status = "à´¸àµà´¥à´¿à´¤à´¿" team = "ടീം" title = "മനàµà´·àµà´¯àµ¼" +unlockAccount = "à´…à´•àµà´•ൗണàµà´Ÿàµ അൺലോകàµà´•ൠചെയàµà´¯àµà´•" +unlockUserError = "ഉപയോകàµà´¤àµƒ à´…à´•àµà´•ൗണàµà´Ÿàµ അൺലോകàµà´•ൠചെയàµà´¯àµà´¨àµà´¨à´¤à´¿àµ½ പരാജയപàµà´ªàµ†à´Ÿàµà´Ÿàµ" +unlockUserSuccess = "ഉപയോകàµà´¤àµƒ à´…à´•àµà´•ൗണàµà´Ÿàµ വിജയകരമായി അൺലോകàµà´•ൠചെയàµà´¤àµ" user = "ഉപയോകàµà´¤à´¾à´µàµ" [workspace.people.actions] diff --git a/frontend/public/locales/nl-NL/translation.toml b/frontend/public/locales/nl-NL/translation.toml index fa4e7a67f7..1be00b3f5c 100644 --- a/frontend/public/locales/nl-NL/translation.toml +++ b/frontend/public/locales/nl-NL/translation.toml @@ -8,6 +8,7 @@ black = "Zwart" blue = "Blauw" bored = "Verveeld met wachten?" cancel = "Annuleren" +confirm = "Bevestigen" changedCredsMessage = "Inloggegevens gewijzigd!" chooseFile = "Bestand kiezen" close = "Sluiten" @@ -146,6 +147,7 @@ insufficientCredits = "Onvoldoende credits. Vereist: {{requiredCredits}}, Beschi loadingCredits = "Credits controleren..." loadingProStatus = "Abonnementsstatus controleren..." noticeTopUpOrPlan = "Onvoldoende credits, vul aan of upgrade naar een abonnement" +accessInvite = "Uitnodigen" [account] accountSettings = "Account instellingen" @@ -1427,6 +1429,34 @@ title = "Processing" description = "Maximum time to wait for a processing job before reporting an error." label = "Processing Timeout (seconds)" +[admin.settings.storage] +description = "Serveropslag en deelopties beheren." +title = "Bestandsopslag en delen" + +[admin.settings.storage.enabled] +description = "Gebruikers toestaan bestanden op de server op te slaan." +label = "Serverbestandsopslag inschakelen" + +[admin.settings.storage.sharing.email] +description = "Delen met e-mailadressen toestaan." +label = "E-mail delen inschakelen" +mailLink = "E-mailinstellingen configureren" +mailNote = "Vereist e-mailconfiguratie. " + +[admin.settings.storage.sharing.enabled] +description = "Gebruikers toestaan opgeslagen bestanden te delen." +label = "Delen inschakelen" + +[admin.settings.storage.sharing.links] +description = "Delen via aangemelde links toestaan." +frontendUrlLink = "Configureren in Systeeminstellingen" +frontendUrlNote = "Vereist een Frontend URL. " +label = "Koppelingen voor delen inschakelen" + +[admin.settings.storage.signing.enabled] +description = "Gebruikers toestaan ondertekeningssessies met meerdere deelnemers te maken. Vereist dat serverbestandsopslag is ingeschakeld." +label = "Groepsondertekenen inschakelen (Alpha)" + [admin.settings.unsavedChanges] cancel = "Verder bewerken" discard = "Wijzigingen verwerpen" @@ -2059,7 +2089,19 @@ numbers = "Cijfers/reeksen: 5, 10-20" progressions = "Voortgangen: 3n, 4n+1" [certSign] +allSigned = "Alle deelnemers hebben getekend. Klaar om te finaliseren." +awaitingSignatures = "In afwachting van handtekeningen" +signatureProgress = "{{signedCount}}/{{totalCount}} handtekeningen" chooseCertificate = "Certificaatbestand kiezen" +declined = "Geweigerd" +fetchFailed = "Ondertekeningsgegevens laden mislukt" +finalized = "Gefinaliseerd" +notified = "In afwachting" +partialNote = "U kunt vroegtijdig finaliseren met de huidige handtekeningen. Niet-ondertekende deelnemers worden uitgesloten." +pending = "In afwachting" +readyToFinalize = "Klaar om te finaliseren" +signed = "Ondertekend" +viewed = "Bekeken" chooseJksFile = "JKS-bestand kiezen" chooseP12File = "PKCS12-bestand kiezen" choosePfxFile = "PFX-bestand kiezen" @@ -2082,6 +2124,7 @@ title = "Certificaat ondertekening" invisible = "Onzichtbaar" stepTitle = "Weergave van handtekening" visible = "Zichtbaar" +visibility = "Zichtbaarheid" [certSign.appearance.options] title = "Handtekeningdetails" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Finaliseren en ondertekende PDF laden" +early = "Finaliseren met huidige handtekeningen" + +[certSign.collab.sessionDetail] +addButton = "Deelnemers toevoegen" +addParticipants = "Deelnemers toevoegen" +addParticipantsError = "Deelnemers toevoegen mislukt" +backToList = "Terug naar sessies" +deleteConfirm = "Weet u het zeker? Dit kan niet ongedaan worden gemaakt." +deleteError = "Sessie verwijderen mislukt" +deleted = "Sessie verwijderd" +deleteSession = "Sessie verwijderen" +dueDate = "Vervaldatum" +finalizeError = "Sessie finaliseren mislukt" +loadPdfError = "Ondertekende PDF laden mislukt" +loadSignedPdf = "Ondertekende PDF laden in actieve bestanden" +messageLabel = "Bericht" +noAdditionalInfo = "Geen aanvullende informatie" +owner = "Eigenaar" +participantRemoved = "Deelnemer verwijderd" +participants = "Deelnemers" +participantsAdded = "Deelnemers succesvol toegevoegd" +removeParticipant = "Verwijderen" +removeParticipantError = "Deelnemer verwijderen mislukt" +selectUsers = "Gebruikers selecteren..." +sessionInfo = "Sessie-informatie" +workbenchTitle = "Sessiebeheer" + +[certSign.collab.signRequest] +addedToFiles = "Document toegevoegd aan actieve bestanden" +addSignature = "Uw handtekening toevoegen" +addToFiles = "Toevoegen aan actieve bestanden" +advancedSettings = "Geavanceerde instellingen" +backToList = "Terug naar ondertekeningsverzoeken" +certificateChoice = "Selecteer een certificaat om mee te ondertekenen" +changeSignature = "Handtekening wijzigen" +clearSignature = "Handtekening wissen" +completeAndSign = "Voltooien en ondertekenen" +createNewSignature = "Nieuwe handtekening maken" +declineButton = "Weigeren" +decline = "Verzoek weigeren" +deleteSelected = "Geselecteerde handtekening verwijderen" +drawSignature = "Teken hieronder uw handtekening" +dueDate = "Vervaldatum" +fileTooLarge = "Bestandsgrootte moet kleiner zijn dan 5MB" +fontFamily = "Lettertype" +fontSize = "Lettergrootte: {{size}}px" +fontSizePlaceholder = "Grootte" +from = "Van" +invalidCertFile = "Selecteer een P12- of PFX-certificaatbestand" +invalidFileType = "Selecteer een afbeeldingsbestand" +location = "Locatie (optioneel)" +locationPlaceholder = "Vanwaar ondertekent u?" +message = "Bericht" +noCertificate = "Selecteer een certificaatbestand" +noSignatures = "Plaats ten minste één handtekening op de PDF" +p12File = "P12/PFX-certificaatbestand" +password = "Certificaatwachtwoord" +passwordPlaceholder = "Voer wachtwoord in..." +penColor = "Penkleur" +penSize = "Penmaat: {{size}}px" +placementActive = "Klik op de PDF om te plaatsen" +placeSignatureButton = "Handtekening op PDF plaatsen" +reason = "Reden (optioneel)" +reasonPlaceholder = "Waarom ondertekent u?" +removeImage = "Afbeelding verwijderen" +removeCertFile = "Bestand verwijderen" +savedSignatures = "Opgeslagen handtekeningen" +selectFile = "Afbeeldingsbestand selecteren" +selectSignatureTitle = "Handtekening selecteren of maken" +signButton = "Document ondertekenen" +signatureInfo = "Deze instellingen zijn geconfigureerd door de documenteigenaar" +signaturePlaced = "Handtekening geplaatst op pagina" +signatureSettings = "Handtekeninginstellingen" +signatureText = "Handtekeningtekst" +signatureTextPlaceholder = "Voer uw naam in..." +signatureTypeLabel = "Handtekeningtype" +signingTitle = "Ondertekenen" +textColor = "Tekstkleur" +typeSignature = "Typ uw naam om een handtekening te maken" +uploadCert = "Aangepast certificaat" +uploadCertDesc = "Gebruik uw eigen P12/PFX-certificaat" +uploadSignature = "Upload uw handtekeningafbeelding" +usePersonalCert = "Persoonlijk certificaat" +usePersonalCertDesc = "Automatisch gegenereerd voor uw account" +useServerCert = "Organisatiecertificaat" +useServerCertDesc = "Gedeeld organisatiecertificaat" +workbenchTitle = "Ondertekeningsverzoek" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Kies lijnkleur" +continue = "Doorgaan" + +[certSign.collab.signRequest.certModal] +description = "U hebt {{count}} handtekening(en) geplaatst. Kies uw certificaat om het ondertekenen te voltooien." +sign = "Document ondertekenen" +certValidating = "Certificaat wordt gevalideerd..." +certValidUntil = "Certificaat geldig tot {{date}}" +certInvalid = "Certificaat ongeldig: {{error}}" +certInvalidFallback = "Ongeldig certificaat" +certNetworkError = "Certificaat kon niet worden gevalideerd" +title = "Certificaat configureren" + +[certSign.collab.signRequest.image] +hint = "Upload een PNG- of JPG-afbeelding van uw handtekening" + +[certSign.collab.signRequest.mode] +move = "Handtekening verplaatsen" +place = "Handtekening plaatsen" +title = "Modus ondertekenen of verplaatsen" + +[certSign.collab.signRequest.modeTabs] +draw = "Tekenen" +image = "Uploaden" +text = "Typen" + +[certSign.collab.signRequest.placeSignature] +message = "Klik op de PDF om uw handtekening te plaatsen" +title = "Handtekening plaatsen" + +[certSign.collab.signRequest.preview] +imageAlt = "Geselecteerde handtekening" +missing = "Geen voorbeeld" +textFallback = "Handtekening" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Getekende handtekening" +defaultImageLabel = "Geüploade handtekening" +defaultLabel = "Handtekening" +defaultTextLabel = "Getypte handtekening" +delete = "Handtekening verwijderen" +none = "Geen opgeslagen handtekeningen" + +[certSign.collab.signRequest.signatureType] +draw = "Tekenen" +type = "Typen" +upload = "Uploaden" + +[certSign.collab.signRequest.steps] +back = "Terug" +cancelPlacement = "Plaatsing annuleren" +certificate = "Certificaat" +clickMultipleTimes = "Klik meerdere keren op de PDF om handtekeningen te plaatsen. Sleep een handtekening om te verplaatsen of te schalen." +clickToPlace = "Klik op de PDF waar u uw handtekening wilt laten verschijnen." +continue = "Doorgaan naar certificaatkeuze" +continueToPlacement = "Doorgaan naar plaatsing" +continueToReview = "Doorgaan naar controle" +createSignature = "Handtekening maken" +invisible = "Onzichtbaar" +location = "Locatie:" +multipleSignatures = "{{count}} handtekeningen worden toegepast op de PDF" +oneSignature = "1 handtekening wordt toegepast op de PDF" +placeOnPdf = "Plaatsen op PDF" +reason = "Reden:" +reviewTitle = "Controleren vóór ondertekenen" +signaturePlaced = "Handtekening geplaatst op pagina {{page}}. U kunt de positie aanpassen door opnieuw te klikken of doorgaan naar controle." +visible = "Zichtbaar" +visibility = "Zichtbaarheid:" +yourSignatures = "Uw handtekeningen ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Kleur" +fontLabel = "Lettertype" +fontSizeLabel = "Grootte" +fontSizePlaceholder = "16" +label = "Handtekeningtekst" +modalHint = "Voer uw naam in en klik vervolgens op Doorgaan om deze op de PDF te plaatsen." +placeholder = "Voer uw naam in..." + +[certSign.collab.participant] +certValidating = "Certificaat wordt gevalideerd..." +certValid = "✓ Certificaat geldig" +certValidUntil = " tot {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Ongeldig certificaat" +certNetworkError = "Certificaat kon niet worden gevalideerd" + +[certSign.collab.addParticipants] +add = "{{count}} deelnemer(s) toevoegen" +back = "Terug" +configureSignatures = "Handtekeninginstellingen configureren" +continue = "Doorgaan naar handtekeninginstellingen" +reasonHelp = "Stel vooraf een ondertekeningsreden in voor deze deelnemers (optioneel; ze kunnen dit aanpassen bij het ondertekenen)" +reasonPlaceholder = "bijv. Goedkeuring, Review..." +selectUsers = "Gebruikers selecteren" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Pagina met handtekeningoverzicht opnemen" +includeSummaryPageHelp = "Aan het einde wordt een overzichtspagina toegevoegd met alle handtekeningmetadata. De digitale certificaathandtekeningvakken op afzonderlijke pagina's worden onderdrukt (natte handtekeningen blijven onaangetast)." + +[certSign.collab.sessionList] +active = "Actief" +finalized = "Gefinaliseerd" + +[certSign.collab.signatureSettings] +description = "Configureer hoe handtekeningen voor alle deelnemers worden weergegeven" +title = "Weergave van handtekening" + +[certSign.collab.userSelector] +inviteUsers = "Gebruikers toevoegen" +loadError = "Gebruikers laden mislukt" +noTeam = "Geen team" +noUsers = "Geen andere gebruikers gevonden." +placeholder = "Gebruikers selecteren..." + +[certSign.mobile] +panelActions = "Acties" +panelDocument = "Document" +panelPeople = "Personen" + +[certSign.sessions] +deleted = "Sessie verwijderd" +fetchFailed = "Sessiegegevens laden mislukt" +finalized = "Sessie gefinaliseerd" +loaded = "Ondertekende PDF geladen" +pdfNotReady = "PDF niet gereed" +pdfNotReadyDesc = "De ondertekende PDF wordt gegenereerd. Probeer het zo meteen opnieuw." + +[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" + [changeCreds] changePassword = "U gebruikt de standaard inloggegevens. Voer alstublieft een nieuw wachtwoord in" changeUsername = "Werk uw gebruikersnaam bij. U wordt uitgelogd na het bijwerken." @@ -3242,6 +3531,46 @@ totalSelected = "Totaal geselecteerd" unsupported = "Niet ondersteund" unzip = "Uitpakken" uploadError = "Uploaden van sommige bestanden is mislukt." +copyCreated = "Kopie opgeslagen op dit apparaat." +copyFailed = "Kopie kon niet worden gemaakt." +leaveShare = "Verwijderen uit mijn lijst" +leaveShareFailed = "Het gedeelde bestand kon niet worden verwijderd." +leaveShareSuccess = "Verwijderd uit uw gedeelde lijst." +removeBoth = "Uit beide verwijderen" +removeFilePrompt = "Dit bestand is opgeslagen op dit apparaat en op uw server. Van waar wilt u het verwijderen?" +removeFileTitle = "Bestand verwijderen" +removeLocalOnly = "Alleen dit apparaat" +removeServerFailed = "Het bestand kon niet van de server worden verwijderd." +removeServerOnly = "Alleen server" +removeServerOnlyPrompt = "Dit bestand is alleen op uw server opgeslagen. Wilt u het van de server verwijderen?" +removeServerSuccess = "Van server verwijderd." +removeSharedPrompt = "Dit bestand is met u gedeeld. U kunt het verwijderen van dit apparaat of uit uw gedeelde lijst." +removeSharedServerOnlyBlockedPrompt = "Dit bestand is met u gedeeld en alleen op de server opgeslagen." +removeSharedServerOnlyPrompt = "Dit bestand is met u gedeeld en alleen op de server opgeslagen. Verwijderen uit uw lijst?" +changesNotUploaded = "Wijzigingen niet geüpload" +cloudFile = "Cloudbestand" +filterAll = "Alles" +filterLocal = "Lokaal" +filterSharedByMe = "Door mij gedeeld" +filterSharedWithMe = "Met mij gedeeld" +lastSynced = "Laatst gesynchroniseerd" +localOnly = "Alleen lokaal" +makeCopy = "Kopie maken" +owner = "Eigenaar" +ownerUnknown = "Onbekend" +share = "Delen" +shareSelected = "Geselecteerde delen" +sharedByYou = "Door u gedeeld" +sharedEditNoticeBody = "U hebt geen bewerkingsrechten op de serverversie van dit bestand. Eventuele bewerkingen worden als lokale kopie opgeslagen." +sharedEditNoticeConfirm = "Begrepen" +sharedEditNoticeTitle = "Alleen-lezen serverkopie" +sharedWithYou = "Met u gedeeld" +sharing = "Delen" +storageState = "Opslag" +synced = "Gesynchroniseerd" +updateOnServer = "Bijwerken op server" +uploadSelected = "Geselecteerde uploaden" +uploadToServer = "Uploaden naar server" [files] addFiles = "Bestanden toevoegen" @@ -3367,6 +3696,77 @@ title = "Over PDF's afvlakken" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Over groepsondertekenen" + +[groupSigning.tooltip.finalization] +bullet1 = "Alle handtekeningen worden toegepast in de door u opgegeven deelnemersvolgorde" +bullet2 = "U kunt indien nodig met gedeeltelijke handtekeningen finaliseren" +bullet3 = "Na finalisatie kan de sessie niet meer worden gewijzigd" +description = "Zodra alle deelnemers hebben getekend (of u ervoor kiest eerder te finaliseren), kunt u de definitieve ondertekende PDF genereren." +title = "Finalisatieproces" + +[groupSigning.tooltip.roles] +bullet1 = "Eigenaar (u): Maakt sessie, configureert standaard handtekeningen, finaliseert document" +bullet2 = "Deelnemers: Maken hun handtekening, kiezen certificaat, plaatsen op PDF" +bullet3 = "Deelnemers kunnen zichtbaarheid, reden of locatie-instellingen niet wijzigen" +description = "U beheert de instellingen voor de handtekeningweergave voor alle deelnemers." +title = "Rollen van deelnemers" + +[groupSigning.tooltip.sequential] +bullet1 = "Eerste deelnemer moet tekenen voordat de tweede toegang krijgt tot het document" +bullet2 = "Zorgt voor juiste ondertekeningsvolgorde voor juridische naleving" +bullet3 = "U kunt deelnemers herschikken door ze in de lijst te slepen" +description = "Deelnemers ondertekenen documenten in de door u opgegeven volgorde. Elke ondertekenaar ontvangt een melding wanneer hij/zij aan de beurt is." +title = "Sequentieel ondertekenen" + +[groupSigning.steps] +back = "Terug" +completed = "Voltooid" +current = "Huidig" +stepLabel = "Stap {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Doorgaan naar controle" +invisible = "Handtekeningen zijn onzichtbaar (alleen metadata)" +locationLabel = "Locatie:" +preview = "Voorbeeld" +reasonLabel = "Reden:" +title = "Handtekeninginstellingen configureren" +visible = "Handtekeningen zijn zichtbaar op pagina {{page}}" + +[groupSigning.steps.review] +document = "Document" +dueDate = "Vervaldatum (optioneel)" +dueDatePlaceholder = "Vervaldatum selecteren..." +invisible = "Onzichtbaar (alleen metadata)" +location = "Locatie:" +logo = "Logo:" +logoHidden = "Geen logo" +logoShown = "Stirling PDF-logo weergegeven" +participants = "Deelnemers" +reason = "Reden:" +send = "Ondertekeningsverzoeken verzenden" +signatureSettings = "Handtekeninginstellingen" +title = "Sessiedetails controleren" +titleShort = "Controleren en verzenden" +visibility = "Zichtbaarheid:" +visible = "Zichtbaar op pagina {{page}}" +participantCount = "{{count}} deelnemer(s) tekenen in volgorde" + +[groupSigning.steps.selectDocument] +continue = "Doorgaan naar deelnemersselectie" +noFile = "Selecteer één PDF-bestand uit uw actieve bestanden om een ondertekeningssessie te maken." +selectedFile = "Geselecteerd document" +title = "Document selecteren" + +[groupSigning.steps.selectParticipants] +continue = "Doorgaan naar handtekeninginstellingen" +count = "{{count}} deelnemer(s) geselecteerd" +label = "Deelnemers selecteren" +placeholder = "Deelnemers kiezen om te ondertekenen..." +title = "Deelnemers kiezen" + [getPdfInfo] downloadJson = "JSON downloaden" downloads = "Downloads" @@ -4460,7 +4860,10 @@ zoomOut = "Uitzoomen" [viewer] cannotPreviewFile = "Kan voorbeeld van bestand niet weergeven" +disableColorFilter = "Kleurfilter uitschakelen" dualPageView = "Dubbele paginaweergave" +enableDarkFilter = "Donkerfilter inschakelen" +enableSepiaFilter = "Sepiafilter inschakelen" firstPage = "Eerste pagina" lastPage = "Laatste pagina" nextPage = "Volgende pagina" @@ -4470,6 +4873,22 @@ singlePageView = "Enkele paginaweergave" unknownFile = "Onbekend bestand" zoomIn = "Inzoomen" zoomOut = "Uitzoomen" +resetZoom = "Zoom resetten" + +[viewer.nonPdf] +fileTypeBadge = "{{type}}-bestand" +convertToPdf = "Converteren naar PDF" +loading = "Laden..." +emptyFile = "Leeg bestand" +csvStats = "{{rows}} rijen · {{columns}} kolommen · {{size}}" +sortedBy = "Gesorteerd op: {{column}}" +columnDefault = "Kolom {{index}}" +htmlPreviewWarning = "HTML-voorbeeld — externe bronnen worden mogelijk niet geladen · {{size}}" +htmlPreview = "HTML-voorbeeld" +invalidJson = "Ongeldige JSON — ruwe inhoud wordt weergegeven" +textStats = "{{lines}} regels · {{size}}" +lineNumbers = "Regelnummers" +renderMarkdown = "Markdown renderen" [viewer.attachments] title = "Bijlagen" @@ -4531,6 +4950,7 @@ toggleAttachments = "Bijlagen tonen/verbergen" toggleTheme = "Thema wisselen" language = "Taal" toggleAnnotations = "Annotaties tonen/verbergen" +toggleLayers = "Lagen in-/uitschakelen" search = "PDF doorzoeken" panMode = "Pan-modus" applyRedactionsFirst = "Apply redactions first" @@ -5407,20 +5827,72 @@ title = "Print bestand" 2 = "Voer printernaam in" [quickAccess] +access = "Toegang" +accessAddPerson = "Nog een persoon toevoegen" +accessBack = "Terug" +accessCopyLink = "Link kopiëren" +accessEmail = "E-mailadres" +accessEmailPlaceholder = "naam@bedrijf.com" +accessFileLabel = "Bestand" +accessGeneral = "Algemene toegang" +accessInviteTitle = "Personen uitnodigen" +accessOwner = "Eigenaar" +accessPanel = "Documenttoegang" +accessPeople = "Personen met toegang" +accessRemove = "Verwijderen" +accessRestricted = "Beperkt" +accessRestrictedHint = "Alleen personen met toegang kunnen openen" +accessRole = "Rol" +accessRoleCommenter = "Opmerkinggever" +accessRoleEditor = "Bewerker" +accessRoleViewer = "Lezer" +accessSelectedFile = "Geselecteerd bestand" +accessSendInvite = "Uitnodiging verzenden" +accessTitle = "Documenttoegang" +accessYou = "U" account = "Account" +activeSessions = "Actieve sessies" +activeTab = "Actief" activity = "Logboek" adminSettings = "Beheer" +allSessions = "Alle sessies" allTools = "Tools" automate = "Automatiseren" +back = "Terug" +certSign = "Met certificaat ondertekenen" +completedSessions = "Voltooide sessies" +completedTab = "Voltooid" config = "Configuratie" +createNew = "Nieuw verzoek maken" +createSession = "Ondertekeningsverzoek maken" +dueDate = "Vervaldatum (optioneel)" files = "Bestand" help = "Hulp" +noActiveSessions = "Geen openstaande ondertekeningsverzoeken of actieve sessies" +noCompletedSessions = "Geen voltooide sessies" +noFile = "Geen bestand geselecteerd" read = "Lezen" reader = "Lezer" +refresh = "Vernieuwen" +requestSignatures = "Handtekeningen aanvragen" +selectSingleFileToRequest = "Selecteer één PDF-bestand om handtekeningen aan te vragen" +selectedFile = "Geselecteerd bestand" +selectUsers = "Gebruikers selecteren om te ondertekenen" +selectUsersPlaceholder = "Deelnemers kiezen..." +sendingRequest = "Verzenden..." settings = "Opties" showMeAround = "Leid me rond" sign = "Teken" +signatureRequests = "Ondertekeningsverzoeken" +signYourself = "Zelf ondertekenen" +newRequest = "Nieuw verzoek" tours = "Rondleidingen" +wetSign = "Handtekening toevoegen" +filterMine = "Van mij" +filterOverdue = "Achterstallig" +filterSigned = "Ondertekend" +filterDeclined = "Geweigerd" +searchDocuments = "Documenten zoeken…" [quickAccess.helpMenu] adminTour = "Rondleiding voor beheer" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Je Stirling-PDF-server is offline en \"{{endpoint}}\" expired = "Uw sessie is verlopen. Voer de pagina opnieuw in en probeer het opnieuw." refreshPage = "Pagina vernieuwen" +[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" + +[signatureSettings.tooltip] +header = "Instellingen voor handtekeningweergave" + +[signatureSettings.tooltip.location] +bullet1 = "Voorbeelden: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Niet hetzelfde als paginapositie" +bullet3 = "Kan vereist zijn voor bepaalde rechtsgebieden" +description = "Optionele geografische locatie waar de handtekening is toegepast. Opgeslagen in certificaatmetadata." +title = "Handtekeninglocatie" + +[signatureSettings.tooltip.logo] +bullet1 = "Weergegeven naast handtekening en tekst" +bullet2 = "Ondersteunt PNG-, JPG-indelingen" +bullet3 = "Verbetert professionele uitstraling" +description = "Voeg een bedrijfslogo toe aan zichtbare handtekeningen voor branding en authenticiteit." +title = "Bedrijfslogo" + +[signatureSettings.tooltip.reason] +bullet1 = "Voorbeelden: \"Goedkeuring\", \"Contractovereenkomst\", \"Review voltooid\"" +bullet2 = "Zichtbaar in PDF-handtekeningseigenschappen" +bullet3 = "Handig voor audittrails en naleving" +description = "Optionele tekst die uitlegt waarom het document wordt ondertekend. Opgeslagen in certificaatmetadata." +title = "Reden voor handtekening" + +[signatureSettings.tooltip.visibility] +bullet1 = "Zichtbaar: Handtekening verschijnt op PDF met aangepaste weergave" +bullet2 = "Onzichtbaar: Certificaat ingesloten zonder visuele markering" +bullet3 = "Onzichtbare handtekeningen bieden nog steeds cryptografische validatie" +description = "Bepaalt of de handtekening zichtbaar is op het document of onzichtbaar wordt ingesloten." +title = "Zichtbaarheid van handtekening" + [settings.configuration] advanced = "Geavanceerd" database = "Database" endpoints = "Eindpunten" features = "Functies" +storageSharing = "Bestandsopslag en delen" systemSettings = "Systeeminstellingen" title = "Configuratie" @@ -6332,10 +6868,13 @@ title = "Inloggen bij Stirling" [setup.selfhosted] link = "of maak verbinding met een zelfgehost account" subtitle = "Vul uw servergegevens in" +changeServerLocked = "Uw organisatie heeft deze app beperkt tot een specifieke server" switchToLocal = "In plaats daarvan lokale tools gebruiken" title = "Inloggen bij server" [setup.selfhosted.unreachable] +changeServer = "Verbinden met een andere server" +changeServerLocked = "Uw organisatie heeft deze app beperkt tot een specifieke server" continueOffline = "In plaats daarvan lokale tools gebruiken" message = "Kon {{url}} niet bereiken. Controleer of de server actief en toegankelijk is." retry = "Opnieuw proberen" @@ -6529,6 +7068,15 @@ saved = "Opgeslagen" text = "Tekst" title = "Type handtekening" +[signRequest] +declined = "Ondertekeningsverzoek geweigerd" +fetchFailed = "Ondertekeningsverzoek laden mislukt" +signed = "Document succesvol ondertekend" + +[signSession] +createFailed = "Ondertekeningsverzoek maken mislukt" +created = "Ondertekeningsverzoek verzonden" + [signup] accountCreatedSuccessfully = "Account succesvol aangemaakt! U kunt nu inloggen." alreadyHaveAccount = "Heeft u al een account? Log dan in" @@ -6807,6 +7355,106 @@ title = "PDF splits op hoofdstukken" [splitPdfByChapters] tags = "splitsen, hoofdstukken, bookmarks, organiseren" +[storageShare] +accessed = "Geopend" +accessDenied = "U hebt geen toegang tot dit gedeelde bestand. Vraag de eigenaar om het met u te delen." +accessFailed = "Kan activiteit niet laden." +accessDeniedBody = "U hebt geen toegang tot dit bestand. Vraag de eigenaar om het met u te delen." +accessDeniedTitle = "Geen toegang" +accessLimitedCommenter = "Opmerkingentoegang komt binnenkort. Vraag de eigenaar om bewerkersrechten als u wilt downloaden." +accessLimitedTitle = "Beperkte toegang" +accessLimitedViewer = "Deze link is alleen-lezen. Vraag de eigenaar om bewerkersrechten als u wilt downloaden." +createdAt = "Gemaakt" +download = "Downloaden" +downloadFailed = "Dit bestand kan niet worden gedownload." +expiredBody = "Deze deellink is ongeldig of verlopen." +expiredTitle = "Link verlopen" +goToLogin = "Naar inloggen" +loadFailed = "Gedeeld bestand kan niet worden geopend." +loading = "Deellink laden..." +loginPrompt = "Meld u aan om toegang te krijgen tot dit gedeelde bestand." +loginRequired = "Inloggen vereist" +openInApp = "Openen in Stirling PDF" +ownerLabel = "Eigenaar" +ownerUnknown = "Onbekend" +requiresLogin = "Dit gedeelde bestand vereist inloggen." +roleCommenter = "Opmerkinggever" +roleEditor = "Bewerker" +roleViewer = "Lezer" +shareHeading = "Gedeeld bestand" +titleDefault = "Gedeeld bestand" +tryAgain = "Probeer het later opnieuw." +addUser = "Toevoegen" +commenterHint = "Reageren komt binnenkort." +copied = "Link gekopieerd naar klembord" +copy = "Kopiëren" +copyFailed = "Kopiëren mislukt" +description = "Maak een deellink voor dit bestand. Aangemelde gebruikers met de link hebben er toegang toe." +downloadsCount = "Downloads: {{count}}" +emailWarningBody = "Dit lijkt op een e-mailadres. Als deze persoon nog geen Stirling PDF-gebruiker is, kan hij/zij geen toegang krijgen tot het bestand." +emailWarningConfirm = "Toch delen" +emailWarningTitle = "E-mailadres" +errorTitle = "Delen mislukt" +failure = "Kan geen deellink genereren. Probeer het opnieuw." +fileLabel = "Bestand" +generate = "Link genereren" +generated = "Deellink gegenereerd" +hideActivity = "Activiteit verbergen" +invalidUsername = "Voer een geldige gebruikersnaam of e-mailadres in." +lastAccessed = "Laatst geopend" +linkAccessTitle = "Toegang via deellink" +linkLabel = "Deellink" +linksDisabled = "Deellinks zijn uitgeschakeld." +linksDisabledBody = "Deellinks zijn uitgeschakeld door uw serverinstellingen." +manage = "Delen beheren" +manageDescription = "Maak en beheer links om dit bestand te delen." +manageLoadFailed = "Deellinks kunnen niet worden geladen." +manageTitle = "Delen beheren" +noActivity = "Nog geen activiteit." +noLinks = "Nog geen actieve deellinks." +noSharedUsers = "Nog geen gebruikers met toegang." +removeLink = "Link verwijderen" +removeUser = "Verwijderen" +revokeFailed = "Kan de deellink niet verwijderen." +revoked = "Deellink verwijderd" +roleLabel = "Rol" +sharingDisabled = "Delen is uitgeschakeld." +sharingDisabledBody = "Delen is uitgeschakeld door uw serverinstellingen." +sharedUsersTitle = "Gedeelde gebruikers" +title = "Bestand delen" +unknownUser = "Onbekende gebruiker" +userAddFailed = "Kan niet met die gebruiker delen." +userAdded = "Gebruiker toegevoegd aan lijst met gedeelde gebruikers." +usernameLabel = "Gebruikersnaam of e-mail" +usernamePlaceholder = "Voer een gebruikersnaam of e-mail in" +userRemoveFailed = "Kan die gebruiker niet verwijderen." +userRemoved = "Gebruiker uit de lijst met gedeelde gebruikers verwijderd." +viewActivity = "Activiteit bekijken" +viewed = "Bekeken" +viewsCount = "Weergaven: {{count}}" +downloaded = "Gedownload" +bulkDescription = "Maak één link om alle geselecteerde bestanden te delen met ingelogde gebruikers." +bulkTitle = "Geselecteerde bestanden delen" +copyLink = "Deellink kopiëren" +fileCount = "{{count}} bestanden geselecteerd" +ownerOnly = "Alleen de eigenaar kan het delen beheren." +selectSingleFile = "Selecteer één bestand om het delen te beheren." + +[storageUpload] +description = "Dit uploadt het huidige bestand naar serveropslag voor uw eigen toegang." +errorTitle = "Upload mislukt" +failure = "Upload mislukt. Controleer uw inlog- en opslaginstellingen." +fileLabel = "Bestand" +hint = "Openbare links en toegangsmodi worden beheerd door uw serverinstellingen." +success = "Geüpload naar server" +title = "Uploaden naar server" +updateButton = "Bijwerken op server" +uploadButton = "Uploaden naar server" +bulkDescription = "Dit uploadt de geselecteerde bestanden naar uw serveropslag." +bulkTitle = "Geselecteerde bestanden uploaden" +fileCount = "{{count}} bestanden geselecteerd" +more = " +{{count}} meer" + [storage] approximateSize = "Geschatte grootte" fileTooLarge = "Bestand te groot. Maximale grootte per bestand is" @@ -7153,6 +7801,30 @@ title = "PDF bekijken/bewerken" [warning] tooltipTitle = "Waarschuwing" +[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" + [watermark] completed = "Watermerk toegevoegd" desc = "Voeg tekst- of afbeeldingswatermerken toe aan PDF-bestanden" @@ -7333,6 +8005,7 @@ activeSession = "Actieve sessie" addMembers = "Leden toevoegen" admin = "Beheerder" confirmDelete = "Weet u zeker dat u deze gebruiker wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt." +confirmUnlock = "Weet u zeker dat u dit gebruikersaccount wilt ontgrendelen?" deleteUser = "Gebruiker verwijderen" deleteUserError = "Gebruiker verwijderen is mislukt" deleteUserSuccess = "Gebruiker succesvol verwijderd" @@ -7341,6 +8014,8 @@ disable = "Uitschakelen" disabled = "Uitgeschakeld" editRole = "Rol bewerken" enable = "Inschakelen" +locked = "vergrendeld" +lockedBadge = "Vergrendeld" loading = "Personen laden..." loginRequired = "Schakel eerst de loginmodus in" member = "Lid" @@ -7350,6 +8025,9 @@ searchMembers = "Leden zoeken..." status = "Status" team = "Team" title = "Personen" +unlockAccount = "Account ontgrendelen" +unlockUserError = "Gebruikersaccount ontgrendelen mislukt" +unlockUserSuccess = "Gebruikersaccount succesvol ontgrendeld" user = "Gebruiker" [workspace.people.actions] diff --git a/frontend/public/locales/no-NB/translation.toml b/frontend/public/locales/no-NB/translation.toml index 399fcf199d..93546f2638 100644 --- a/frontend/public/locales/no-NB/translation.toml +++ b/frontend/public/locales/no-NB/translation.toml @@ -8,6 +8,7 @@ black = "Svart" blue = "BlÃ¥" bored = "Lei av Ã¥ vente?" cancel = "Avbryt" +confirm = "Bekreft" changedCredsMessage = "Legitimasjon endret!" chooseFile = "Velg fil" close = "Lukk" @@ -146,6 +147,7 @@ insufficientCredits = "Ikke nok kreditter. PÃ¥krevd: {{requiredCredits}}, Tilgje loadingCredits = "Sjekker kreditter..." loadingProStatus = "Sjekker abonnementsstatus..." noticeTopUpOrPlan = "Ikke nok kreditter, fyll pÃ¥ eller oppgrader til et abonnement" +accessInvite = "Inviter" [account] accountSettings = "Kontoinnstillinger" @@ -1427,6 +1429,34 @@ title = "Behandling" description = "Maksimal tid Ã¥ vente pÃ¥ en behandlingsjobb før en feil rapporteres." label = "Behandlingstidsavbrudd (sekunder)" +[admin.settings.storage] +description = "Kontroller serverlagring og delingsalternativer." +title = "Fil-lagring og deling" + +[admin.settings.storage.enabled] +description = "Tillat brukere Ã¥ lagre filer pÃ¥ serveren." +label = "Aktiver fillagring pÃ¥ server" + +[admin.settings.storage.sharing.email] +description = "Tillat deling med e-postadresser." +label = "Aktiver deling via e-post" +mailLink = "Konfigurer e-postinnstillinger" +mailNote = "Krever e-postkonfigurasjon. " + +[admin.settings.storage.sharing.enabled] +description = "Tillat brukere Ã¥ dele lagrede filer." +label = "Aktiver deling" + +[admin.settings.storage.sharing.links] +description = "Tillat deling via pÃ¥loggede lenker." +frontendUrlLink = "Konfigurer i systeminnstillinger" +frontendUrlNote = "Krever en Frontend URL. " +label = "Aktiver delingslenker" + +[admin.settings.storage.signing.enabled] +description = "Tillat brukere Ã¥ opprette signeringsøkter med flere deltakere. Krever at fillagring pÃ¥ serveren er aktivert." +label = "Aktiver gruppesignering (Alpha)" + [admin.settings.unsavedChanges] cancel = "Fortsett redigering" discard = "Forkast endringer" @@ -2059,7 +2089,19 @@ numbers = "Tall/intervaller: 5, 10-20" progressions = "Progresjoner: 3n, 4n+1" [certSign] +allSigned = "Alle deltakere har signert. Klar til Ã¥ ferdigstille." +awaitingSignatures = "Avventer signaturer" +signatureProgress = "{{signedCount}}/{{totalCount}} signaturer" chooseCertificate = "Velg sertifikatfil" +declined = "AvslÃ¥tt" +fetchFailed = "Kunne ikke laste signeringsdata" +finalized = "Ferdigstilt" +notified = "Avventer" +partialNote = "Du kan ferdigstille tidlig med gjeldende signaturer. Usignerte deltakere blir ekskludert." +pending = "Avventer" +readyToFinalize = "Klar til Ã¥ ferdigstille" +signed = "Signert" +viewed = "Vist" chooseJksFile = "Velg JKS-fil" chooseP12File = "Velg PKCS12-fil" choosePfxFile = "Velg PFX-fil" @@ -2082,6 +2124,7 @@ title = "Sertifikatsignering" invisible = "Usynlig" stepTitle = "Signaturutseende" visible = "Synlig" +visibility = "Synlighet" [certSign.appearance.options] title = "Signaturdetaljer" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Ferdigstill og last inn signert PDF" +early = "Ferdigstill med nÃ¥værende signaturer" + +[certSign.collab.sessionDetail] +addButton = "Legg til deltakere" +addParticipants = "Legg til deltakere" +addParticipantsError = "Kunne ikke legge til deltakere" +backToList = "Tilbake til økter" +deleteConfirm = "Er du sikker? Dette kan ikke angres." +deleteError = "Kunne ikke slette økt" +deleted = "Økt slettet" +deleteSession = "Slett økt" +dueDate = "Frist" +finalizeError = "Kunne ikke ferdigstille økt" +loadPdfError = "Kunne ikke laste signert PDF" +loadSignedPdf = "Last inn signert PDF i aktive filer" +messageLabel = "Melding" +noAdditionalInfo = "Ingen tilleggsinformasjon" +owner = "Eier" +participantRemoved = "Deltaker fjernet" +participants = "Deltakere" +participantsAdded = "Deltakere lagt til" +removeParticipant = "Fjern" +removeParticipantError = "Kunne ikke fjerne deltaker" +selectUsers = "Velg brukere ..." +sessionInfo = "Øktinformasjon" +workbenchTitle = "Øktadministrasjon" + +[certSign.collab.signRequest] +addedToFiles = "Dokument lagt til i aktive filer" +addSignature = "Legg til signaturen din" +addToFiles = "Legg til i aktive filer" +advancedSettings = "Avanserte innstillinger" +backToList = "Tilbake til signeringsforespørsler" +certificateChoice = "Velg et sertifikat Ã¥ signere med" +changeSignature = "Endre signatur" +clearSignature = "Fjern signatur" +completeAndSign = "Fullfør og signer" +createNewSignature = "Opprett ny signatur" +declineButton = "AvslÃ¥" +decline = "AvslÃ¥ forespørsel" +deleteSelected = "Slett valgt signatur" +drawSignature = "Tegn signaturen din nedenfor" +dueDate = "Frist" +fileTooLarge = "Filstørrelsen mÃ¥ være mindre enn 5MB" +fontFamily = "Skrifttype" +fontSize = "Skriftstørrelse: {{size}}px" +fontSizePlaceholder = "Størrelse" +from = "Fra" +invalidCertFile = "Velg en P12- eller PFX-sertifikatfil" +invalidFileType = "Velg en bildefil" +location = "Sted (valgfritt)" +locationPlaceholder = "Hvor signerer du fra?" +message = "Melding" +noCertificate = "Velg en sertifikatfil" +noSignatures = "Plasser minst én signatur pÃ¥ PDF-en" +p12File = "P12/PFX-sertifikatfil" +password = "Sertifikatpassord" +passwordPlaceholder = "Angi passord ..." +penColor = "Pennfarge" +penSize = "Pennstørrelse: {{size}}px" +placementActive = "Klikk pÃ¥ PDF-en for Ã¥ plassere" +placeSignatureButton = "Plasser signatur pÃ¥ PDF" +reason = "Ã…rsak (valgfritt)" +reasonPlaceholder = "Hvorfor signerer du?" +removeImage = "Fjern bilde" +removeCertFile = "Fjern fil" +savedSignatures = "Lagrede signaturer" +selectFile = "Velg bildefil" +selectSignatureTitle = "Velg eller opprett signatur" +signButton = "Signer dokument" +signatureInfo = "Disse innstillingene er konfigurert av dokumenteieren" +signaturePlaced = "Signatur plassert pÃ¥ side" +signatureSettings = "Signaturinnstillinger" +signatureText = "Signaturtekst" +signatureTextPlaceholder = "Skriv inn navnet ditt ..." +signatureTypeLabel = "Signaturtype" +signingTitle = "Signering" +textColor = "Tekstfarge" +typeSignature = "Skriv inn navnet ditt for Ã¥ opprette en signatur" +uploadCert = "Egendefinert sertifikat" +uploadCertDesc = "Bruk ditt eget P12/PFX-sertifikat" +uploadSignature = "Last opp signaturbildet ditt" +usePersonalCert = "Personlig sertifikat" +usePersonalCertDesc = "Genereres automatisk for kontoen din" +useServerCert = "Organisasjonssertifikat" +useServerCertDesc = "Delt organisasjonssertifikat" +workbenchTitle = "Signeringsforespørsel" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Velg strekfarge" +continue = "Fortsett" + +[certSign.collab.signRequest.certModal] +description = "Du har plassert {{count}} signatur(er). Velg sertifikatet ditt for Ã¥ fullføre signeringen." +sign = "Signer dokument" +certValidating = "Validerer sertifikat ..." +certValidUntil = "Sertifikat gyldig til {{date}}" +certInvalid = "Ugyldig sertifikat: {{error}}" +certInvalidFallback = "Ugyldig sertifikat" +certNetworkError = "Kunne ikke validere sertifikat" +title = "Konfigurer sertifikat" + +[certSign.collab.signRequest.image] +hint = "Last opp et PNG- eller JPG-bilde av signaturen din" + +[certSign.collab.signRequest.mode] +move = "Flytt signatur" +place = "Plasser signatur" +title = "Signerings- eller flyttemodus" + +[certSign.collab.signRequest.modeTabs] +draw = "Tegn" +image = "Last opp" +text = "Skriv" + +[certSign.collab.signRequest.placeSignature] +message = "Klikk pÃ¥ PDF-en for Ã¥ plassere signaturen din" +title = "Plasser signatur" + +[certSign.collab.signRequest.preview] +imageAlt = "Valgt signatur" +missing = "Ingen forhÃ¥ndsvisning" +textFallback = "Signatur" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Tegnet signatur" +defaultImageLabel = "Opplastet signatur" +defaultLabel = "Signatur" +defaultTextLabel = "Skrevet signatur" +delete = "Slett signatur" +none = "Ingen lagrede signaturer" + +[certSign.collab.signRequest.signatureType] +draw = "Tegn" +type = "Skriv" +upload = "Last opp" + +[certSign.collab.signRequest.steps] +back = "Tilbake" +cancelPlacement = "Avbryt plassering" +certificate = "Sertifikat" +clickMultipleTimes = "Klikk flere ganger pÃ¥ PDF-en for Ã¥ plassere signaturer. Dra en signatur for Ã¥ flytte eller endre størrelse." +clickToPlace = "Klikk pÃ¥ PDF-en der du vil at signaturen skal vises." +continue = "Fortsett til valg av sertifikat" +continueToPlacement = "Fortsett til plassering" +continueToReview = "Fortsett til gjennomgang" +createSignature = "Opprett signatur" +invisible = "Usynlig" +location = "Sted:" +multipleSignatures = "{{count}} signaturer vil bli lagt til PDF-en" +oneSignature = "1 signatur vil bli lagt til PDF-en" +placeOnPdf = "Plasser pÃ¥ PDF" +reason = "Ã…rsak:" +reviewTitle = "GjennomgÃ¥ før signering" +signaturePlaced = "Signatur plassert pÃ¥ side {{page}}. Du kan justere plasseringen ved Ã¥ klikke igjen, eller fortsette til gjennomgang." +visible = "Synlig" +visibility = "Synlighet:" +yourSignatures = "Dine signaturer ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Farge" +fontLabel = "Skrift" +fontSizeLabel = "Størrelse" +fontSizePlaceholder = "16" +label = "Signaturtekst" +modalHint = "Skriv inn navnet ditt, og klikk deretter Fortsett for Ã¥ plassere det pÃ¥ PDF-en." +placeholder = "Skriv inn navnet ditt ..." + +[certSign.collab.participant] +certValidating = "Validerer sertifikat ..." +certValid = "✓ Sertifikat gyldig" +certValidUntil = " til {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Ugyldig sertifikat" +certNetworkError = "Kunne ikke validere sertifikat" + +[certSign.collab.addParticipants] +add = "Legg til {{count}} deltakere" +back = "Tilbake" +configureSignatures = "Konfigurer signaturinnstillinger" +continue = "Fortsett til signaturinnstillinger" +reasonHelp = "ForhÃ¥ndsangi en signeringsÃ¥rsak for disse deltakerne (valgfritt, de kan overstyre ved signering)" +reasonPlaceholder = "f.eks. Godkjenning, Gjennomgang ..." +selectUsers = "Velg brukere" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Inkluder side med signaturoppsummering" +includeSummaryPageHelp = "En oppsummeringsside legges til pÃ¥ slutten med all signaturmetadata. Signaturfeltene for digitale sertifikater pÃ¥ enkeltsider blir undertrykt (vÃ¥te signaturer pÃ¥virkes ikke)." + +[certSign.collab.sessionList] +active = "Aktiv" +finalized = "Ferdigstilt" + +[certSign.collab.signatureSettings] +description = "Konfigurer hvordan signaturer skal vises for alle deltakere" +title = "Signaturutforming" + +[certSign.collab.userSelector] +inviteUsers = "Legg til brukere" +loadError = "Kunne ikke laste brukere" +noTeam = "Ingen team" +noUsers = "Ingen andre brukere funnet." +placeholder = "Velg brukere ..." + +[certSign.mobile] +panelActions = "Handlinger" +panelDocument = "Dokument" +panelPeople = "Personer" + +[certSign.sessions] +deleted = "Økt slettet" +fetchFailed = "Kunne ikke laste øktdetaljer" +finalized = "Økt ferdigstilt" +loaded = "Signert PDF lastet inn" +pdfNotReady = "PDF ikke klar" +pdfNotReadyDesc = "Den signerte PDF-en genereres. Prøv igjen om et øyeblikk." + +[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" + [changeCreds] changePassword = "Du bruker standard pÃ¥loggingsdetaljer. Vennligst skriv inn et nytt passord" changeUsername = "Oppdater brukernavnet ditt. Du blir logget ut etter oppdatering." @@ -3242,6 +3531,46 @@ totalSelected = "Totalt valgt" unsupported = "Ikke støttet" unzip = "Pakk ut" uploadError = "Noen filer kunne ikke lastes opp." +copyCreated = "Kopi lagret pÃ¥ denne enheten." +copyFailed = "Kunne ikke opprette en kopi." +leaveShare = "Fjern fra listen min" +leaveShareFailed = "Kunne ikke fjerne den delte filen." +leaveShareSuccess = "Fjernet fra din delte liste." +removeBoth = "Fjern fra begge" +removeFilePrompt = "Denne filen er lagret pÃ¥ denne enheten og pÃ¥ serveren. Hvor vil du fjerne den fra?" +removeFileTitle = "Fjern fil" +removeLocalOnly = "Kun denne enheten" +removeServerFailed = "Kunne ikke fjerne filen fra serveren." +removeServerOnly = "Kun server" +removeServerOnlyPrompt = "Denne filen er lagret kun pÃ¥ serveren. Vil du fjerne den fra serveren?" +removeServerSuccess = "Fjernet fra server." +removeSharedPrompt = "Denne filen er delt med deg. Du kan fjerne den fra denne enheten eller fra din delte liste." +removeSharedServerOnlyBlockedPrompt = "Denne filen er delt med deg og lagret kun pÃ¥ serveren." +removeSharedServerOnlyPrompt = "Denne filen er delt med deg og lagret kun pÃ¥ serveren. Fjern den fra listen din?" +changesNotUploaded = "Endringer ikke lastet opp" +cloudFile = "Skyfil" +filterAll = "Alle" +filterLocal = "Lokal" +filterSharedByMe = "Delt av meg" +filterSharedWithMe = "Delt med meg" +lastSynced = "Sist synkronisert" +localOnly = "Kun lokalt" +makeCopy = "Lag en kopi" +owner = "Eier" +ownerUnknown = "Ukjent" +share = "Del" +shareSelected = "Del valgte" +sharedByYou = "Delt av deg" +sharedEditNoticeBody = "Du har ikke redigeringsrettigheter til serverversjonen av denne filen. Eventuelle endringer du gjør, lagres som en lokal kopi." +sharedEditNoticeConfirm = "ForstÃ¥tt" +sharedEditNoticeTitle = "Skrivebeskyttet serverkopi" +sharedWithYou = "Delt med deg" +sharing = "Deling" +storageState = "Lagring" +synced = "Synkronisert" +updateOnServer = "Oppdater pÃ¥ serveren" +uploadSelected = "Last opp valgte" +uploadToServer = "Last opp til serveren" [files] addFiles = "Legg til filer" @@ -3367,6 +3696,77 @@ title = "Om utflating av PDF-er" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Om gruppesignering" + +[groupSigning.tooltip.finalization] +bullet1 = "Alle signaturer legges til i den deltakerrekkefølgen du spesifiserte" +bullet2 = "Du kan ferdigstille med delvise signaturer ved behov" +bullet3 = "NÃ¥r ferdigstilt, kan ikke økten endres" +description = "NÃ¥r alle deltakere har signert (eller du velger Ã¥ ferdigstille tidlig), kan du generere den endelige signerte PDF-en." +title = "Ferdigstillingsprosess" + +[groupSigning.tooltip.roles] +bullet1 = "Eier (deg): Oppretter økt, konfigurerer signaturstandarder, ferdigstiller dokument" +bullet2 = "Deltakere: Oppretter sin signatur, velger sertifikat, plasserer pÃ¥ PDF" +bullet3 = "Deltakere kan ikke endre innstillinger for synlighet, Ã¥rsak eller sted" +description = "Du kontrollerer innstillingene for signaturutforming for alle deltakere." +title = "Deltakerroller" + +[groupSigning.tooltip.sequential] +bullet1 = "Første deltaker mÃ¥ signere før den neste fÃ¥r tilgang til dokumentet" +bullet2 = "Sikrer korrekt signeringsrekkefølge for juridisk etterlevelse" +bullet3 = "Du kan endre rekkefølgen ved Ã¥ dra deltakere i listen" +description = "Deltakere signerer dokumenter i den rekkefølgen du angir. Hver signerer fÃ¥r et varsel nÃ¥r det er deres tur." +title = "Sekvensiell signering" + +[groupSigning.steps] +back = "Tilbake" +completed = "Fullført" +current = "Gjeldende" +stepLabel = "Trinn {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Fortsett til gjennomgang" +invisible = "Signaturer vil være usynlige (kun metadata)" +locationLabel = "Sted:" +preview = "ForhÃ¥ndsvisning" +reasonLabel = "Ã…rsak:" +title = "Konfigurer signaturinnstillinger" +visible = "Signaturer vil være synlige pÃ¥ side {{page}}" + +[groupSigning.steps.review] +document = "Dokument" +dueDate = "Frist (valgfritt)" +dueDatePlaceholder = "Velg frist ..." +invisible = "Usynlig (kun metadata)" +location = "Sted:" +logo = "Logo:" +logoHidden = "Ingen logo" +logoShown = "Stirling PDF-logo vises" +participants = "Deltakere" +reason = "Ã…rsak:" +send = "Send signeringsforespørsler" +signatureSettings = "Signaturinnstillinger" +title = "GjennomgÃ¥ øktsdetaljer" +titleShort = "GjennomgÃ¥ og send" +visibility = "Synlighet:" +visible = "Synlig pÃ¥ side {{page}}" +participantCount = "{{count}} deltakere vil signere i rekkefølge" + +[groupSigning.steps.selectDocument] +continue = "Fortsett til valg av deltakere" +noFile = "Velg én PDF-fil fra dine aktive filer for Ã¥ opprette en signeringsøkt." +selectedFile = "Valgt dokument" +title = "Velg dokument" + +[groupSigning.steps.selectParticipants] +continue = "Fortsett til signaturinnstillinger" +count = "{{count}} deltakere valgt" +label = "Velg deltakere" +placeholder = "Velg deltakere som skal signere ..." +title = "Velg deltakere" + [getPdfInfo] downloadJson = "Last ned JSON" downloads = "Nedlastinger" @@ -4460,7 +4860,10 @@ zoomOut = "Zoom ut" [viewer] cannotPreviewFile = "Kan ikke forhÃ¥ndsvise fil" +disableColorFilter = "Deaktiver fargefilter" dualPageView = "Dobbelsidevisning" +enableDarkFilter = "Aktiver mørkt filter" +enableSepiaFilter = "Aktiver sepiafilter" firstPage = "Første side" lastPage = "Siste side" nextPage = "Neste side" @@ -4470,6 +4873,22 @@ singlePageView = "Enkeltsidevisning" unknownFile = "Ukjent fil" zoomIn = "Zoom inn" zoomOut = "Zoom ut" +resetZoom = "Tilbakestill zoom" + +[viewer.nonPdf] +fileTypeBadge = "{{type}}-fil" +convertToPdf = "Konverter til PDF" +loading = "Laster ..." +emptyFile = "Tom fil" +csvStats = "{{rows}} rader · {{columns}} kolonner · {{size}}" +sortedBy = "Sortert etter: {{column}}" +columnDefault = "Kolonne {{index}}" +htmlPreviewWarning = "HTML-forhÃ¥ndsvisning — eksterne ressurser kan hende ikke lastes · {{size}}" +htmlPreview = "HTML-forhÃ¥ndsvisning" +invalidJson = "Ugyldig JSON — viser rÃ¥tt innhold" +textStats = "{{lines}} linjer · {{size}}" +lineNumbers = "Linjenumre" +renderMarkdown = "Gjengi Markdown" [viewer.attachments] title = "Vedlegg" @@ -4531,6 +4950,7 @@ toggleAttachments = "Vis/skjul vedlegg" toggleTheme = "Bytt tema" language = "SprÃ¥k" toggleAnnotations = "Vis/skjul merknader" +toggleLayers = "Veksle lag" search = "Søk i PDF" panMode = "Panoreringsmodus" applyRedactionsFirst = "Bruk sladding først" @@ -5407,20 +5827,72 @@ title = "Skriv ut fil" 2 = "Skriv inn skrivernavn" [quickAccess] +access = "Tilgang" +accessAddPerson = "Legg til en person til" +accessBack = "Tilbake" +accessCopyLink = "Kopier lenke" +accessEmail = "E-postadresse" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Fil" +accessGeneral = "Generell tilgang" +accessInviteTitle = "Inviter personer" +accessOwner = "Eier" +accessPanel = "Dokumenttilgang" +accessPeople = "Personer med tilgang" +accessRemove = "Fjern" +accessRestricted = "Begrenset" +accessRestrictedHint = "Bare personer med tilgang kan Ã¥pne" +accessRole = "Rolle" +accessRoleCommenter = "Kommentator" +accessRoleEditor = "Redaktør" +accessRoleViewer = "Leser" +accessSelectedFile = "Valgt fil" +accessSendInvite = "Send invitasjon" +accessTitle = "Dokumenttilgang" +accessYou = "Deg" account = "Konto" +activeSessions = "Aktive økter" +activeTab = "Aktive" activity = "Logg" adminSettings = "Admin Innst." +allSessions = "Alle økter" allTools = "All Tools" automate = "Auto" +back = "Tilbake" +certSign = "Signer med sertifikat" +completedSessions = "Fullførte økter" +completedTab = "Fullført" config = "Oppsett" +createNew = "Opprett ny forespørsel" +createSession = "Opprett signeringsforespørsel" +dueDate = "Frist (valgfritt)" files = "Filer" help = "Hjelp" +noActiveSessions = "Ingen ventende signeringsforespørsler eller aktive økter" +noCompletedSessions = "Ingen fullførte økter" +noFile = "Ingen fil valgt" read = "Les" reader = "Leser" +refresh = "Oppdater" +requestSignatures = "Be om signaturer" +selectSingleFileToRequest = "Velg én PDF-fil for Ã¥ be om signaturer" +selectedFile = "Valgt fil" +selectUsers = "Velg brukere som skal signere" +selectUsersPlaceholder = "Velg deltakere ..." +sendingRequest = "Sender ..." settings = "Innst." showMeAround = "Vis meg rundt" sign = "Signer" +signatureRequests = "Signeringsforespørsler" +signYourself = "Signer selv" +newRequest = "Ny forespørsel" tours = "Omvisninger" +wetSign = "Legg til signatur" +filterMine = "Mine" +filterOverdue = "Forfalt" +filterSigned = "Signert" +filterDeclined = "AvslÃ¥tt" +searchDocuments = "Søk i dokumenter…" [quickAccess.helpMenu] adminTour = "Admin-omvisning" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Stirling-PDF-serveren din er frakoblet og \"{{endpoin expired = "Økten din har utløpt. Vennligst oppdater siden og prøv igjen." refreshPage = "Oppdater Side" +[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" + +[signatureSettings.tooltip] +header = "Innstillinger for signaturutforming" + +[signatureSettings.tooltip.location] +bullet1 = "Eksempler: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Ikke det samme som plassering pÃ¥ siden" +bullet3 = "Kan være pÃ¥krevd i enkelte jurisdiksjoner" +description = "Valgfritt geografisk sted der signaturen ble pÃ¥ført. Lagres i sertifikatmetadata." +title = "Signatursted" + +[signatureSettings.tooltip.logo] +bullet1 = "Vises sammen med signatur og tekst" +bullet2 = "Støtter PNG, JPG-formater" +bullet3 = "Forbedrer profesjonelt uttrykk" +description = "Legg til en firmalogo pÃ¥ synlige signaturer for profilering og ekthet." +title = "Firmalogo" + +[signatureSettings.tooltip.reason] +bullet1 = "Eksempler: \"Godkjenning\", \"Kontrakt\", \"Gjennomgang fullført\"" +bullet2 = "Synlig i PDF-signaturegenskaper" +bullet3 = "Nyttig for sporbarhet og etterlevelse" +description = "Valgfri tekst som forklarer hvorfor dokumentet signeres. Lagres i sertifikatmetadata." +title = "SigneringsÃ¥rsak" + +[signatureSettings.tooltip.visibility] +bullet1 = "Synlig: Signatur vises pÃ¥ PDF med tilpasset utseende" +bullet2 = "Usynlig: Sertifikat innebygd uten visuelt merke" +bullet3 = "Usynlige signaturer gir fortsatt kryptografisk validering" +description = "Styrer om signaturen er synlig pÃ¥ dokumentet eller innebygd usynlig." +title = "Signatursynlighet" + [settings.configuration] advanced = "Avansert" database = "Database" endpoints = "Endepunkter" features = "Funksjoner" +storageSharing = "Fil-lagring og deling" systemSettings = "Systeminnstillinger" title = "Konfigurasjon" @@ -6332,10 +6868,13 @@ title = "Logg inn i Stirling" [setup.selfhosted] link = "eller koble til en selvhostet konto" subtitle = "Oppgi serverlegitimasjonen din" +changeServerLocked = "Organisasjonen din har begrenset denne appen til en bestemt server" switchToLocal = "Bruk lokale verktøy i stedet" title = "Logg inn pÃ¥ server" [setup.selfhosted.unreachable] +changeServer = "Koble til en annen server" +changeServerLocked = "Organisasjonen din har begrenset denne appen til en bestemt server" continueOffline = "Bruk lokale verktøy i stedet" message = "Kunne ikke nÃ¥ {{url}}. Kontroller at serveren kjører og er tilgjengelig." retry = "Prøv igjen" @@ -6529,6 +7068,15 @@ saved = "Lagret" text = "Tekst" title = "Signaturtype" +[signRequest] +declined = "Signeringsforespørsel avslÃ¥tt" +fetchFailed = "Kunne ikke laste signeringsforespørsel" +signed = "Dokument signert" + +[signSession] +createFailed = "Kunne ikke opprette signeringsforespørsel" +created = "Signeringsforespørsel sendt" + [signup] accountCreatedSuccessfully = "Konto opprettet! Du kan nÃ¥ logge inn." alreadyHaveAccount = "Har du allerede en konto? Logg inn" @@ -6807,6 +7355,106 @@ title = "Del PDF etter kapitler" [splitPdfByChapters] tags = "del,kapitler,bokmerker,organiser" +[storageShare] +accessed = "Ã…pnet" +accessDenied = "Du har ikke tilgang til denne delte filen. Be eieren dele den med deg." +accessFailed = "Kan ikke laste aktivitet." +accessDeniedBody = "Du har ikke tilgang til denne filen. Be eieren dele den med deg." +accessDeniedTitle = "Ingen tilgang" +accessLimitedCommenter = "Kommentartilgang kommer snart. Be eieren om redaktørtilgang hvis du trenger Ã¥ laste ned." +accessLimitedTitle = "Begrenset tilgang" +accessLimitedViewer = "Denne lenken er kun for visning. Be eieren om redaktørtilgang hvis du trenger Ã¥ laste ned." +createdAt = "Opprettet" +download = "Last ned" +downloadFailed = "Kan ikke laste ned denne filen." +expiredBody = "Denne delingslenken er ugyldig eller har utløpt." +expiredTitle = "Lenke utløpt" +goToLogin = "GÃ¥ til innlogging" +loadFailed = "Kan ikke Ã¥pne delt fil." +loading = "Laster delingslenke ..." +loginPrompt = "Logg inn for Ã¥ fÃ¥ tilgang til denne delte filen." +loginRequired = "Innlogging kreves" +openInApp = "Ã…pne i Stirling PDF" +ownerLabel = "Eier" +ownerUnknown = "Ukjent" +requiresLogin = "Denne delte filen krever innlogging." +roleCommenter = "Kommentator" +roleEditor = "Redaktør" +roleViewer = "Leser" +shareHeading = "Delt fil" +titleDefault = "Delt fil" +tryAgain = "Prøv igjen senere." +addUser = "Legg til" +commenterHint = "Kommentartilgang kommer snart." +copied = "Lenke kopiert til utklippstavlen" +copy = "Kopier" +copyFailed = "Kopiering mislyktes" +description = "Opprett en delingslenke for denne filen. Innloggede brukere med lenken kan fÃ¥ tilgang." +downloadsCount = "Nedlastinger: {{count}}" +emailWarningBody = "Dette ser ut som en e-postadresse. Hvis denne personen ikke allerede er Stirling PDF-bruker, vil vedkommende ikke kunne fÃ¥ tilgang til filen." +emailWarningConfirm = "Del likevel" +emailWarningTitle = "E-postadresse" +errorTitle = "Deling mislyktes" +failure = "Kunne ikke generere en delingslenke. Prøv igjen." +fileLabel = "Fil" +generate = "Generer lenke" +generated = "Delingslenke opprettet" +hideActivity = "Skjul aktivitet" +invalidUsername = "Angi et gyldig brukernavn eller e-postadresse." +lastAccessed = "Sist Ã¥pnet" +linkAccessTitle = "Tilgang via delingslenke" +linkLabel = "Delingslenke" +linksDisabled = "Delingslenker er deaktivert." +linksDisabledBody = "Delingslenker er deaktivert av serverinnstillingene dine." +manage = "Administrer deling" +manageDescription = "Opprett og administrer lenker for Ã¥ dele denne filen." +manageLoadFailed = "Kunne ikke laste delingslenker." +manageTitle = "Administrer deling" +noActivity = "Ingen aktivitet ennÃ¥." +noLinks = "Ingen aktive delingslenker ennÃ¥." +noSharedUsers = "Ingen brukere har tilgang ennÃ¥." +removeLink = "Fjern lenke" +removeUser = "Fjern" +revokeFailed = "Kunne ikke fjerne delingslenken." +revoked = "Delingslenke fjernet" +roleLabel = "Rolle" +sharingDisabled = "Deling er deaktivert." +sharingDisabledBody = "Deling har blitt deaktivert av serverinnstillingene dine." +sharedUsersTitle = "Brukere som har tilgang" +title = "Del fil" +unknownUser = "Ukjent bruker" +userAddFailed = "Kan ikke dele med denne brukeren." +userAdded = "Bruker lagt til i delingslisten." +usernameLabel = "Brukernavn eller e-post" +usernamePlaceholder = "Skriv inn et brukernavn eller en e-post" +userRemoveFailed = "Kan ikke fjerne denne brukeren." +userRemoved = "Bruker fjernet fra delingslisten." +viewActivity = "Vis aktivitet" +viewed = "Vist" +viewsCount = "Visninger: {{count}}" +downloaded = "Lastet ned" +bulkDescription = "Opprett én lenke for Ã¥ dele alle valgte filer med pÃ¥loggede brukere." +bulkTitle = "Del valgte filer" +copyLink = "Kopier delingslenke" +fileCount = "{{count}} filer valgt" +ownerOnly = "Bare eieren kan administrere deling." +selectSingleFile = "Velg én fil for Ã¥ administrere deling." + +[storageUpload] +description = "Dette laster opp gjeldende fil til serverlagring for din egen tilgang." +errorTitle = "Opplasting mislyktes" +failure = "Opplasting mislyktes. Kontroller pÃ¥loggingen og lagringsinnstillingene dine." +fileLabel = "Fil" +hint = "Offentlige lenker og tilgangsmoduser styres av serverinnstillingene dine." +success = "Lastet opp til serveren" +title = "Last opp til server" +updateButton = "Oppdater pÃ¥ server" +uploadButton = "Last opp til server" +bulkDescription = "Dette laster opp de valgte filene til serverlagringen din." +bulkTitle = "Last opp valgte filer" +fileCount = "{{count}} filer valgt" +more = " +{{count}} til" + [storage] approximateSize = "Omtrentlig størrelse" fileTooLarge = "Filen er for stor. Maksimal størrelse per fil er" @@ -7153,6 +7801,30 @@ title = "Vis/Rediger PDF" [warning] tooltipTitle = "Advarsel" +[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" + [watermark] completed = "Vannmerke lagt til" desc = "Legg til tekst- eller bildevannmerker i PDF-filer" @@ -7333,6 +8005,7 @@ activeSession = "Aktiv økt" addMembers = "Legg til medlemmer" admin = "Admin" confirmDelete = "Er du sikker pÃ¥ at du vil slette denne brukeren? Denne handlingen kan ikke angres." +confirmUnlock = "Er du sikker pÃ¥ at du vil lÃ¥se opp denne brukerkontoen?" deleteUser = "Slett bruker" deleteUserError = "Kunne ikke slette bruker" deleteUserSuccess = "Bruker slettet" @@ -7341,6 +8014,8 @@ disable = "Deaktiver" disabled = "Deaktivert" editRole = "Rediger rolle" enable = "Aktiver" +locked = "lÃ¥st" +lockedBadge = "LÃ¥st" loading = "Laster personer..." loginRequired = "Aktiver innloggingsmodus først" member = "Medlem" @@ -7350,6 +8025,9 @@ searchMembers = "Søk i medlemmer..." status = "Status" team = "Team" title = "Personer" +unlockAccount = "LÃ¥s opp konto" +unlockUserError = "Kunne ikke lÃ¥se opp brukerkonto" +unlockUserSuccess = "Brukerkonto lÃ¥st opp" user = "Bruker" [workspace.people.actions] diff --git a/frontend/public/locales/pl-PL/translation.toml b/frontend/public/locales/pl-PL/translation.toml index a4492189e6..069244dc90 100644 --- a/frontend/public/locales/pl-PL/translation.toml +++ b/frontend/public/locales/pl-PL/translation.toml @@ -8,6 +8,7 @@ black = "czarny" blue = "niebieski" bored = "Znudzony czekaniem?" cancel = "Anuluj" +confirm = "Potwierdź" changedCredsMessage = "Dane logowanie zostaÅ‚y zmienione." chooseFile = "Wybierz plik" close = "Zamknij" @@ -146,6 +147,7 @@ insufficientCredits = "NiewystarczajÄ…ca liczba kredytów. Wymagane: {{requiredC loadingCredits = "Sprawdzanie kredytów..." loadingProStatus = "Sprawdzanie statusu subskrypcji..." noticeTopUpOrPlan = "Za maÅ‚o kredytów, doÅ‚aduj lub wybierz plan" +accessInvite = "ZaproÅ›" [account] accountSettings = "Ustawienia konta" @@ -1427,6 +1429,34 @@ title = "Przetwarzanie" description = "Maksymalny czas oczekiwania na zadanie przetwarzania przed zgÅ‚oszeniem błędu." label = "Limit przetwarzania (sekundy)" +[admin.settings.storage] +description = "ZarzÄ…dzaj przechowywaniem na serwerze i opcjami udostÄ™pniania." +title = "Przechowywanie plików i udostÄ™pnianie" + +[admin.settings.storage.enabled] +description = "Zezwalaj użytkownikom na przechowywanie plików na serwerze." +label = "Włącz przechowywanie plików na serwerze" + +[admin.settings.storage.sharing.email] +description = "Zezwalaj na udostÄ™pnianie na podstawie adresu e-mail." +label = "Włącz udostÄ™pnianie e-mailem" +mailLink = "Skonfiguruj ustawienia poczty" +mailNote = "Wymaga konfiguracji poczty. " + +[admin.settings.storage.sharing.enabled] +description = "Zezwalaj użytkownikom na udostÄ™pnianie przechowywanych plików." +label = "Włącz udostÄ™pnianie" + +[admin.settings.storage.sharing.links] +description = "Zezwalaj na udostÄ™pnianie za pomocÄ… linków wymagajÄ…cych zalogowania." +frontendUrlLink = "Skonfiguruj w ustawieniach systemu" +frontendUrlNote = "Wymaga Frontend URL. " +label = "Włącz linki udostÄ™pniania" + +[admin.settings.storage.signing.enabled] +description = "Zezwalaj użytkownikom na tworzenie wieloosobowych sesji podpisywania dokumentów. Wymaga włączonego przechowywania plików na serwerze." +label = "Włącz podpisywanie grupowe (Alpha)" + [admin.settings.unsavedChanges] cancel = "Kontynuuj edycjÄ™" discard = "Odrzuć zmiany" @@ -2059,7 +2089,19 @@ numbers = "Liczby/zakresy: 5, 10-20" progressions = "Progresje: 3n, 4n+1" [certSign] +allSigned = "Wszyscy uczestnicy podpisali. Gotowe do finalizacji." +awaitingSignatures = "Oczekiwanie na podpisy" +signatureProgress = "{{signedCount}}/{{totalCount}} podpisów" chooseCertificate = "Wybierz plik certyfikatu" +declined = "Odrzucono" +fetchFailed = "Nie udaÅ‚o siÄ™ wczytać danych podpisu" +finalized = "Sfinalizowano" +notified = "OczekujÄ…ce" +partialNote = "Możesz sfinalizować wczeÅ›niej, korzystajÄ…c z aktualnych podpisów. Niepodpisani uczestnicy zostanÄ… wykluczeni." +pending = "OczekujÄ…ce" +readyToFinalize = "Gotowe do finalizacji" +signed = "Podpisano" +viewed = "WyÅ›wietlono" chooseJksFile = "Wybierz plik JKS" chooseP12File = "Wybierz plik PKCS12" choosePfxFile = "Wybierz plik PFX" @@ -2082,6 +2124,7 @@ title = "Podpisywanie certyfikatem" invisible = "Niewidoczny" stepTitle = "WyglÄ…d podpisu" visible = "Widoczny" +visibility = "Widoczność" [certSign.appearance.options] title = "Szczegóły podpisu" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Sfinalizuj i wczytaj podpisany PDF" +early = "Sfinalizuj z aktualnymi podpisami" + +[certSign.collab.sessionDetail] +addButton = "Dodaj uczestników" +addParticipants = "Dodaj uczestników" +addParticipantsError = "Nie udaÅ‚o siÄ™ dodać uczestników" +backToList = "Wróć do sesji" +deleteConfirm = "Na pewno? Tej operacji nie można cofnąć." +deleteError = "Nie udaÅ‚o siÄ™ usunąć sesji" +deleted = "SesjÄ™ usuniÄ™to" +deleteSession = "UsuÅ„ sesjÄ™" +dueDate = "Termin" +finalizeError = "Nie udaÅ‚o siÄ™ sfinalizować sesji" +loadPdfError = "Nie udaÅ‚o siÄ™ wczytać podpisanego PDF-a" +loadSignedPdf = "Wczytaj podpisany PDF do aktywnych plików" +messageLabel = "Wiadomość" +noAdditionalInfo = "Brak dodatkowych informacji" +owner = "WÅ‚aÅ›ciciel" +participantRemoved = "Uczestnik usuniÄ™ty" +participants = "Uczestnicy" +participantsAdded = "PomyÅ›lnie dodano uczestników" +removeParticipant = "UsuÅ„" +removeParticipantError = "Nie udaÅ‚o siÄ™ usunąć uczestnika" +selectUsers = "Wybierz użytkowników..." +sessionInfo = "Informacje o sesji" +workbenchTitle = "ZarzÄ…dzanie sesjÄ…" + +[certSign.collab.signRequest] +addedToFiles = "Dokument dodany do aktywnych plików" +addSignature = "Dodaj swój podpis" +addToFiles = "Dodaj do aktywnych plików" +advancedSettings = "Ustawienia zaawansowane" +backToList = "Wróć do próśb o podpis" +certificateChoice = "Wybierz certyfikat do podpisu" +changeSignature = "ZmieÅ„ podpis" +clearSignature = "Wyczyść podpis" +completeAndSign = "ZakoÅ„cz i podpisz" +createNewSignature = "Utwórz nowy podpis" +declineButton = "Odrzuć" +decline = "Odrzuć proÅ›bÄ™" +deleteSelected = "UsuÅ„ wybrany podpis" +drawSignature = "Narysuj swój podpis poniżej" +dueDate = "Termin" +fileTooLarge = "Rozmiar pliku musi być mniejszy niż 5MB" +fontFamily = "Krój pisma" +fontSize = "Rozmiar czcionki: {{size}}px" +fontSizePlaceholder = "Rozmiar" +from = "Od" +invalidCertFile = "Wybierz plik certyfikatu P12 lub PFX" +invalidFileType = "Wybierz plik obrazu" +location = "Lokalizacja (opcjonalnie)" +locationPlaceholder = "SkÄ…d podpisujesz?" +message = "Wiadomość" +noCertificate = "Wybierz plik certyfikatu" +noSignatures = "Umieść co najmniej jeden podpis w pliku PDF" +p12File = "Plik certyfikatu P12/PFX" +password = "HasÅ‚o certyfikatu" +passwordPlaceholder = "Wpisz hasÅ‚o..." +penColor = "Kolor pióra" +penSize = "Rozmiar pióra: {{size}}px" +placementActive = "Kliknij PDF, aby umieÅ›cić" +placeSignatureButton = "Umieść podpis na PDF" +reason = "Powód (opcjonalnie)" +reasonPlaceholder = "Dlaczego podpisujesz?" +removeImage = "UsuÅ„ obraz" +removeCertFile = "UsuÅ„ plik" +savedSignatures = "Zapisane podpisy" +selectFile = "Wybierz plik obrazu" +selectSignatureTitle = "Wybierz lub utwórz podpis" +signButton = "Podpisz dokument" +signatureInfo = "Te ustawienia sÄ… konfigurowane przez wÅ‚aÅ›ciciela dokumentu" +signaturePlaced = "Podpis umieszczony na stronie" +signatureSettings = "Ustawienia podpisu" +signatureText = "Tekst podpisu" +signatureTextPlaceholder = "Wpisz swoje imiÄ™ i nazwisko..." +signatureTypeLabel = "Typ podpisu" +signingTitle = "Podpisywanie" +textColor = "Kolor tekstu" +typeSignature = "Wpisz swoje imiÄ™ i nazwisko, aby utworzyć podpis" +uploadCert = "WÅ‚asny certyfikat" +uploadCertDesc = "Użyj wÅ‚asnego certyfikatu P12/PFX" +uploadSignature = "PrzeÅ›lij obraz swojego podpisu" +usePersonalCert = "Certyfikat osobisty" +usePersonalCertDesc = "Automatycznie generowany dla twojego konta" +useServerCert = "Certyfikat organizacji" +useServerCertDesc = "Współdzielony certyfikat organizacji" +workbenchTitle = "ProÅ›ba o podpis" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Wybierz kolor linii" +continue = "Kontynuuj" + +[certSign.collab.signRequest.certModal] +description = "Umieszczono {{count}} podpisów. Wybierz certyfikat, aby zakoÅ„czyć podpisywanie." +sign = "Podpisz dokument" +certValidating = "Weryfikowanie certyfikatu..." +certValidUntil = "Certyfikat ważny do {{date}}" +certInvalid = "NieprawidÅ‚owy certyfikat: {{error}}" +certInvalidFallback = "NieprawidÅ‚owy certyfikat" +certNetworkError = "Nie można zweryfikować certyfikatu" +title = "Skonfiguruj certyfikat" + +[certSign.collab.signRequest.image] +hint = "PrzeÅ›lij obraz PNG lub JPG swojego podpisu" + +[certSign.collab.signRequest.mode] +move = "PrzesuÅ„ podpis" +place = "Umieść podpis" +title = "Tryb podpisywania lub przenoszenia" + +[certSign.collab.signRequest.modeTabs] +draw = "Rysuj" +image = "PrzeÅ›lij" +text = "Wpisz" + +[certSign.collab.signRequest.placeSignature] +message = "Kliknij na PDF, aby umieÅ›cić podpis" +title = "Umieść podpis" + +[certSign.collab.signRequest.preview] +imageAlt = "Wybrany podpis" +missing = "Brak podglÄ…du" +textFallback = "Podpis" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Rysowany podpis" +defaultImageLabel = "PrzesÅ‚any podpis" +defaultLabel = "Podpis" +defaultTextLabel = "Wpisany podpis" +delete = "UsuÅ„ podpis" +none = "Brak zapisanych podpisów" + +[certSign.collab.signRequest.signatureType] +draw = "Rysuj" +type = "Wpisz" +upload = "PrzeÅ›lij" + +[certSign.collab.signRequest.steps] +back = "Wstecz" +cancelPlacement = "Anuluj umieszczanie" +certificate = "Certyfikat" +clickMultipleTimes = "Kliknij na pliku PDF wielokrotnie, aby umieÅ›cić podpisy. PrzeciÄ…gnij dowolny podpis, aby go przenieść lub zmienić rozmiar." +clickToPlace = "Kliknij na pliku PDF w miejscu, w którym chcesz, aby pojawiÅ‚ siÄ™ podpis." +continue = "Kontynuuj do wyboru certyfikatu" +continueToPlacement = "Kontynuuj do umieszczania" +continueToReview = "Kontynuuj do przeglÄ…du" +createSignature = "Utwórz podpis" +invisible = "Niewidoczny" +location = "Lokalizacja:" +multipleSignatures = "Do pliku PDF zostanie zastosowanych {{count}} podpisów" +oneSignature = "1 podpis zostanie zastosowany do pliku PDF" +placeOnPdf = "Umieść na PDF" +reason = "Powód:" +reviewTitle = "Przejrzyj przed podpisaniem" +signaturePlaced = "Podpis umieszczono na stronie {{page}}. Możesz dostosować poÅ‚ożenie, klikajÄ…c ponownie, lub przejść do przeglÄ…du." +visible = "Widoczny" +visibility = "Widoczność:" +yourSignatures = "Twoje podpisy ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Kolor" +fontLabel = "Czcionka" +fontSizeLabel = "Rozmiar" +fontSizePlaceholder = "16" +label = "Tekst podpisu" +modalHint = "Wpisz swoje imiÄ™ i nazwisko, a nastÄ™pnie kliknij Kontynuuj, aby umieÅ›cić je na PDF." +placeholder = "Wpisz swoje imiÄ™ i nazwisko..." + +[certSign.collab.participant] +certValidating = "Weryfikowanie certyfikatu..." +certValid = "✓ Certyfikat ważny" +certValidUntil = " do {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "NieprawidÅ‚owy certyfikat" +certNetworkError = "Nie można zweryfikować certyfikatu" + +[certSign.collab.addParticipants] +add = "Dodaj {{count}} uczestników" +back = "Wstecz" +configureSignatures = "Skonfiguruj ustawienia podpisu" +continue = "Kontynuuj do ustawieÅ„ podpisu" +reasonHelp = "WstÄ™pnie ustaw powód podpisywania dla tych uczestników (opcjonalne, mogÄ… zmienić podczas podpisywania)" +reasonPlaceholder = "np. Akceptacja, PrzeglÄ…d..." +selectUsers = "Wybierz użytkowników" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Dołącz stronÄ™ podsumowania podpisów" +includeSummaryPageHelp = "Na koÅ„cu zostanie dodana strona podsumowania ze wszystkimi metadanymi podpisów. Pola podpisu cyfrowego na poszczególnych stronach zostanÄ… ukryte (podpisy odrÄ™czne bez zmian)." + +[certSign.collab.sessionList] +active = "Aktywne" +finalized = "Sfinalizowane" + +[certSign.collab.signatureSettings] +description = "Skonfiguruj wyglÄ…d podpisów dla wszystkich uczestników" +title = "WyglÄ…d podpisu" + +[certSign.collab.userSelector] +inviteUsers = "Dodaj użytkowników" +loadError = "Nie udaÅ‚o siÄ™ wczytać użytkowników" +noTeam = "Brak zespoÅ‚u" +noUsers = "Nie znaleziono innych użytkowników." +placeholder = "Wybierz użytkowników..." + +[certSign.mobile] +panelActions = "Akcje" +panelDocument = "Dokument" +panelPeople = "Osoby" + +[certSign.sessions] +deleted = "SesjÄ™ usuniÄ™to" +fetchFailed = "Nie udaÅ‚o siÄ™ wczytać szczegółów sesji" +finalized = "SesjÄ™ sfinalizowano" +loaded = "Wczytano podpisany PDF" +pdfNotReady = "PDF nie jest gotowy" +pdfNotReadyDesc = "Podpisany PDF jest generowany. Spróbuj ponownie za chwilÄ™." + +[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" + [changeCreds] changePassword = "Musisz zmienić domyÅ›lne dane logowania" changeUsername = "Zaktualizuj nazwÄ™ użytkownika. Zostaniesz wylogowany po aktualizacji." @@ -3242,6 +3531,46 @@ totalSelected = "Razem wybrane" unsupported = "NieobsÅ‚ugiwane" unzip = "Rozpakuj" uploadError = "Nie udaÅ‚o siÄ™ przesÅ‚ać niektórych plików." +copyCreated = "Kopia zapisana na tym urzÄ…dzeniu." +copyFailed = "Nie udaÅ‚o siÄ™ utworzyć kopii." +leaveShare = "UsuÅ„ z mojej listy" +leaveShareFailed = "Nie udaÅ‚o siÄ™ usunąć udostÄ™pnionego pliku." +leaveShareSuccess = "UsuniÄ™to z listy udostÄ™pnionych." +removeBoth = "UsuÅ„ z obu" +removeFilePrompt = "Ten plik jest zapisany na tym urzÄ…dzeniu i na twoim serwerze. SkÄ…d chcesz go usunąć?" +removeFileTitle = "UsuÅ„ plik" +removeLocalOnly = "Tylko to urzÄ…dzenie" +removeServerFailed = "Nie udaÅ‚o siÄ™ usunąć pliku z serwera." +removeServerOnly = "Tylko serwer" +removeServerOnlyPrompt = "Ten plik jest przechowywany tylko na twoim serwerze. Czy chcesz usunąć go z serwera?" +removeServerSuccess = "UsuniÄ™to z serwera." +removeSharedPrompt = "Ten plik jest z tobÄ… udostÄ™pniony. Możesz usunąć go z tego urzÄ…dzenia lub z listy udostÄ™pnionych." +removeSharedServerOnlyBlockedPrompt = "Ten plik jest z tobÄ… udostÄ™pniony i przechowywany wyłącznie na serwerze." +removeSharedServerOnlyPrompt = "Ten plik jest z tobÄ… udostÄ™pniony i przechowywany wyłącznie na serwerze. Usunąć go z twojej listy?" +changesNotUploaded = "Zmiany nie zostaÅ‚y przesÅ‚ane" +cloudFile = "Plik w chmurze" +filterAll = "Wszystkie" +filterLocal = "Lokalne" +filterSharedByMe = "UdostÄ™pnione przeze mnie" +filterSharedWithMe = "UdostÄ™pnione mi" +lastSynced = "Ostatnia synchronizacja" +localOnly = "Tylko lokalnie" +makeCopy = "Utwórz kopiÄ™" +owner = "WÅ‚aÅ›ciciel" +ownerUnknown = "Nieznany" +share = "UdostÄ™pnij" +shareSelected = "UdostÄ™pnij wybrane" +sharedByYou = "UdostÄ™pnione przez ciebie" +sharedEditNoticeBody = "Nie masz praw edycji do wersji serwerowej tego pliku. Wszelkie zmiany zostanÄ… zapisane jako kopia lokalna." +sharedEditNoticeConfirm = "Rozumiem" +sharedEditNoticeTitle = "Serwerowa kopia tylko do odczytu" +sharedWithYou = "UdostÄ™pnione tobie" +sharing = "UdostÄ™pnianie" +storageState = "Przechowywanie" +synced = "Zsynchronizowano" +updateOnServer = "Zaktualizuj na serwerze" +uploadSelected = "PrzeÅ›lij wybrane" +uploadToServer = "PrzeÅ›lij na serwer" [files] addFiles = "Dodaj pliki" @@ -3367,6 +3696,77 @@ title = "O spÅ‚aszczaniu PDF-ów" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "O podpisywaniu grupowym" + +[groupSigning.tooltip.finalization] +bullet1 = "Wszystkie podpisy sÄ… nakÅ‚adane w ustalonej przez ciebie kolejnoÅ›ci uczestników" +bullet2 = "W razie potrzeby możesz sfinalizować z częściowymi podpisami" +bullet3 = "Po finalizacji sesji nie można modyfikować" +description = "Gdy wszyscy uczestnicy podpiszÄ… (lub zdecydujesz siÄ™ na wczeÅ›niejszÄ… finalizacjÄ™), możesz wygenerować ostateczny podpisany PDF." +title = "Proces finalizacji" + +[groupSigning.tooltip.roles] +bullet1 = "WÅ‚aÅ›ciciel (ty): Tworzy sesjÄ™, konfiguruje domyÅ›lne ustawienia podpisu, finalizuje dokument" +bullet2 = "Uczestnicy: TworzÄ… swój podpis, wybierajÄ… certyfikat, umieszczajÄ… go na PDF" +bullet3 = "Uczestnicy nie mogÄ… modyfikować ustawieÅ„ widocznoÅ›ci, powodu ani lokalizacji podpisu" +description = "Kontrolujesz ustawienia wyglÄ…du podpisu dla wszystkich uczestników." +title = "Role uczestników" + +[groupSigning.tooltip.sequential] +bullet1 = "Pierwszy uczestnik musi podpisać, zanim drugi uzyska dostÄ™p do dokumentu" +bullet2 = "Zapewnia wÅ‚aÅ›ciwÄ… kolejność podpisów dla zgodnoÅ›ci prawnej" +bullet3 = "Możesz zmienić kolejność uczestników, przeciÄ…gajÄ…c ich na liÅ›cie" +description = "Uczestnicy podpisujÄ… dokumenty w ustalonej przez ciebie kolejnoÅ›ci. Każdy otrzymuje powiadomienie, gdy przyjdzie jego kolej." +title = "Sekwencyjne podpisywanie" + +[groupSigning.steps] +back = "Wstecz" +completed = "ZakoÅ„czono" +current = "Bieżący" +stepLabel = "Krok {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Kontynuuj do przeglÄ…du" +invisible = "Podpisy bÄ™dÄ… niewidoczne (tylko metadane)" +locationLabel = "Lokalizacja:" +preview = "PodglÄ…d" +reasonLabel = "Powód:" +title = "Skonfiguruj ustawienia podpisu" +visible = "Podpisy bÄ™dÄ… widoczne na stronie {{page}}" + +[groupSigning.steps.review] +document = "Dokument" +dueDate = "Termin (opcjonalnie)" +dueDatePlaceholder = "Wybierz termin..." +invisible = "Niewidoczne (tylko metadane)" +location = "Lokalizacja:" +logo = "Logo:" +logoHidden = "Bez logo" +logoShown = "WyÅ›wietlane logo Stirling PDF" +participants = "Uczestnicy" +reason = "Powód:" +send = "WyÅ›lij proÅ›by o podpis" +signatureSettings = "Ustawienia podpisu" +title = "Przejrzyj szczegóły sesji" +titleShort = "PrzeglÄ…d i wysyÅ‚ka" +visibility = "Widoczność:" +visible = "Widoczne na stronie {{page}}" +participantCount = "{{count}} uczestników bÄ™dzie podpisywać w kolejnoÅ›ci" + +[groupSigning.steps.selectDocument] +continue = "Kontynuuj do wyboru uczestników" +noFile = "Wybierz jeden plik PDF z aktywnych plików, aby utworzyć sesjÄ™ podpisywania." +selectedFile = "Wybrany dokument" +title = "Wybierz dokument" + +[groupSigning.steps.selectParticipants] +continue = "Kontynuuj do ustawieÅ„ podpisu" +count = "Wybrano {{count}} uczestników" +label = "Wybierz uczestników" +placeholder = "Wybierz uczestników do podpisu..." +title = "Wybierz uczestników" + [getPdfInfo] downloadJson = "Pobierz JSON z zawartoÅ›ciÄ…" downloads = "Pobrania" @@ -4460,7 +4860,10 @@ zoomOut = "Pomniejsz" [viewer] cannotPreviewFile = "Nie można wyÅ›wietlić podglÄ…du pliku" +disableColorFilter = "Wyłącz filtr kolorów" dualPageView = "Widok dwóch stron" +enableDarkFilter = "Włącz ciemny filtr" +enableSepiaFilter = "Włącz filtr sepii" firstPage = "Pierwsza strona" lastPage = "Ostatnia strona" nextPage = "NastÄ™pna strona" @@ -4470,6 +4873,22 @@ singlePageView = "Widok pojedynczej strony" unknownFile = "Nieznany plik" zoomIn = "PowiÄ™ksz" zoomOut = "Pomniejsz" +resetZoom = "Resetuj powiÄ™kszenie" + +[viewer.nonPdf] +fileTypeBadge = "Plik {{type}}" +convertToPdf = "Konwertuj do PDF" +loading = "Wczytywanie..." +emptyFile = "Pusty plik" +csvStats = "{{rows}} wierszy · {{columns}} kolumn · {{size}}" +sortedBy = "Sortowanie wg: {{column}}" +columnDefault = "Kolumna {{index}}" +htmlPreviewWarning = "PodglÄ…d HTML — zasoby zewnÄ™trzne mogÄ… siÄ™ nie zaÅ‚adować · {{size}}" +htmlPreview = "PodglÄ…d HTML" +invalidJson = "NieprawidÅ‚owy JSON — wyÅ›wietlanie surowej zawartoÅ›ci" +textStats = "{{lines}} wierszy · {{size}}" +lineNumbers = "Numery wierszy" +renderMarkdown = "Renderuj markdown" [viewer.attachments] title = "Załączniki" @@ -4531,6 +4950,7 @@ toggleAttachments = "Pokaż/ukryj załączniki" toggleTheme = "Przełącz motyw" language = "JÄ™zyk" toggleAnnotations = "Przełącz widoczność adnotacji" +toggleLayers = "Przełącz warstwy" search = "Szukaj w PDF" panMode = "Tryb przesuwania" applyRedactionsFirst = "Najpierw zastosuj zaczernienia" @@ -5407,20 +5827,72 @@ title = "Drukuj plik" 2 = "Wskaż drukarkÄ™" [quickAccess] +access = "DostÄ™p" +accessAddPerson = "Dodaj kolejnÄ… osobÄ™" +accessBack = "Wstecz" +accessCopyLink = "Kopiuj link" +accessEmail = "Adres e-mail" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Plik" +accessGeneral = "DostÄ™p ogólny" +accessInviteTitle = "ZaproÅ› osoby" +accessOwner = "WÅ‚aÅ›ciciel" +accessPanel = "DostÄ™p do dokumentu" +accessPeople = "Osoby z dostÄ™pem" +accessRemove = "UsuÅ„" +accessRestricted = "Ograniczony" +accessRestrictedHint = "Tylko osoby z dostÄ™pem mogÄ… otworzyć" +accessRole = "Rola" +accessRoleCommenter = "KomentujÄ…cy" +accessRoleEditor = "Edytor" +accessRoleViewer = "PrzeglÄ…dajÄ…cy" +accessSelectedFile = "Wybrany plik" +accessSendInvite = "WyÅ›lij zaproszenie" +accessTitle = "DostÄ™p do dokumentu" +accessYou = "Ty" account = "Konto" +activeSessions = "Aktywne sesje" +activeTab = "Aktywne" activity = "Historia" adminSettings = "Ustaw. admina" +allSessions = "Wszystkie sesje" allTools = "All Tools" automate = "Auto" +back = "Wstecz" +certSign = "Podpis certyfikatem" +completedSessions = "ZakoÅ„czone sesje" +completedTab = "ZakoÅ„czone" config = "Konfig" +createNew = "Utwórz nowÄ… proÅ›bÄ™" +createSession = "Utwórz proÅ›bÄ™ o podpis" +dueDate = "Termin (opcjonalnie)" files = "Pliki" help = "Pomoc" +noActiveSessions = "Brak oczekujÄ…cych próśb o podpis ani aktywnych sesji" +noCompletedSessions = "Brak zakoÅ„czonych sesji" +noFile = "Nie wybrano pliku" read = "Czytaj" reader = "Czytnik" +refresh = "OdÅ›wież" +requestSignatures = "PoproÅ› o podpisy" +selectSingleFileToRequest = "Wybierz jeden plik PDF, aby poprosić o podpisy" +selectedFile = "Wybrany plik" +selectUsers = "Wybierz użytkowników do podpisu" +selectUsersPlaceholder = "Wybierz uczestników..." +sendingRequest = "WysyÅ‚anie..." settings = "Ustaw." showMeAround = "Pokaż mi, jak to dziaÅ‚a" sign = "Podpis" +signatureRequests = "ProÅ›by o podpis" +signYourself = "Podpisz samodzielnie" +newRequest = "Nowa proÅ›ba" tours = "Przewodniki" +wetSign = "Dodaj podpis" +filterMine = "Moje" +filterOverdue = "ZalegÅ‚e" +filterSigned = "Podpisane" +filterDeclined = "Odrzucone" +searchDocuments = "Szukaj dokumentów…" [quickAccess.helpMenu] adminTour = "Przewodnik administratora" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Twój serwer Stirling-PDF jest offline, a \"{{endpoin expired = "Twoja sesja wygasÅ‚a. OdÅ›wież stronÄ™ i spróbuj ponownie." refreshPage = "OdÅ›wież stronÄ™" +[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" + +[signatureSettings.tooltip] +header = "Ustawienia wyglÄ…du podpisu" + +[signatureSettings.tooltip.location] +bullet1 = "PrzykÅ‚ady: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "To nie to samo co poÅ‚ożenie na stronie" +bullet3 = "Może być wymagane w niektórych jurysdykcjach prawnych" +description = "Opcjonalna lokalizacja geograficzna, gdzie zÅ‚ożono podpis. Zapisywana w metadanych certyfikatu." +title = "Lokalizacja podpisu" + +[signatureSettings.tooltip.logo] +bullet1 = "WyÅ›wietlane obok podpisu i tekstu" +bullet2 = "ObsÅ‚uguje formaty PNG, JPG" +bullet3 = "Podnosi profesjonalny wyglÄ…d" +description = "Dodaj logo firmy do widocznych podpisów dla brandingu i autentycznoÅ›ci." +title = "Logo firmy" + +[signatureSettings.tooltip.reason] +bullet1 = "PrzykÅ‚ady: \"Approval\", \"Contract Agreement\", \"Review Complete\"" +bullet2 = "Widoczny we wÅ‚aÅ›ciwoÅ›ciach podpisu PDF" +bullet3 = "Przydatny dla audytu i zgodnoÅ›ci" +description = "Opcjonalny tekst wyjaÅ›niajÄ…cy, dlaczego dokument jest podpisywany. Zapisywany w metadanych certyfikatu." +title = "Powód podpisu" + +[signatureSettings.tooltip.visibility] +bullet1 = "Widoczny: Podpis pojawia siÄ™ na PDF z wÅ‚asnym wyglÄ…dem" +bullet2 = "Niewidoczny: Certyfikat osadzony bez Å›ladu wizualnego" +bullet3 = "Niewidoczne podpisy nadal zapewniajÄ… kryptograficznÄ… walidacjÄ™" +description = "Kontroluje, czy podpis jest widoczny na dokumencie, czy osadzony niewidocznie." +title = "Widoczność podpisu" + [settings.configuration] advanced = "Zaawansowane" database = "Baza danych" endpoints = "Endpointy" features = "Funkcje" +storageSharing = "Przechowywanie plików i udostÄ™pnianie" systemSettings = "Ustawienia systemowe" title = "Konfiguracja" @@ -6332,10 +6868,13 @@ title = "Zaloguj siÄ™ do Stirling" [setup.selfhosted] link = "lub połącz siÄ™ z kontem hostowanym samodzielnie" subtitle = "Wprowadź dane logowania do serwera" +changeServerLocked = "Twoja organizacja ograniczyÅ‚a tÄ™ aplikacjÄ™ do okreÅ›lonego serwera" switchToLocal = "Użyj zamiast tego narzÄ™dzi lokalnych" title = "Zaloguj siÄ™ do serwera" [setup.selfhosted.unreachable] +changeServer = "Połącz z innym serwerem" +changeServerLocked = "Twoja organizacja ograniczyÅ‚a tÄ™ aplikacjÄ™ do okreÅ›lonego serwera" continueOffline = "Użyj zamiast tego narzÄ™dzi lokalnych" message = "Nie można połączyć siÄ™ z {{url}}. Sprawdź, czy serwer dziaÅ‚a i jest dostÄ™pny." retry = "Ponów próbÄ™" @@ -6529,6 +7068,15 @@ saved = "Zapisane" text = "Tekst" title = "Typ podpisu" +[signRequest] +declined = "ProÅ›ba o podpis zostaÅ‚a odrzucona" +fetchFailed = "Nie udaÅ‚o siÄ™ wczytać proÅ›by o podpis" +signed = "Dokument pomyÅ›lnie podpisany" + +[signSession] +createFailed = "Nie udaÅ‚o siÄ™ utworzyć proÅ›by o podpis" +created = "WysÅ‚ano proÅ›bÄ™ o podpis" + [signup] accountCreatedSuccessfully = "Konto zostaÅ‚o utworzone! Teraz możesz siÄ™ zalogować." alreadyHaveAccount = "Masz już konto? Zaloguj siÄ™" @@ -6807,6 +7355,106 @@ title = "Podziel PDF wedÅ‚ug Rozdziałów" [splitPdfByChapters] tags = "podziaÅ‚, rozdziaÅ‚y, zakÅ‚adki, porzÄ…dkowanie, organizacja" +[storageShare] +accessed = "Uzyskano dostÄ™p" +accessDenied = "Nie masz dostÄ™pu do tego udostÄ™pnionego pliku. PoproÅ› wÅ‚aÅ›ciciela o udostÄ™pnienie." +accessFailed = "Nie można wczytać aktywnoÅ›ci." +accessDeniedBody = "Nie masz dostÄ™pu do tego pliku. PoproÅ› wÅ‚aÅ›ciciela o udostÄ™pnienie." +accessDeniedTitle = "Brak dostÄ™pu" +accessLimitedCommenter = "DostÄ™p do komentowania już wkrótce. PoproÅ› wÅ‚aÅ›ciciela o dostÄ™p edytora, jeÅ›li potrzebujesz pobierania." +accessLimitedTitle = "Ograniczony dostÄ™p" +accessLimitedViewer = "Ten link umożliwia tylko przeglÄ…danie. PoproÅ› wÅ‚aÅ›ciciela o dostÄ™p edytora, jeÅ›li potrzebujesz pobierania." +createdAt = "Utworzono" +download = "Pobierz" +downloadFailed = "Nie można pobrać tego pliku." +expiredBody = "Ten link udostÄ™pniania jest nieprawidÅ‚owy lub wygasÅ‚." +expiredTitle = "Link wygasÅ‚" +goToLogin = "Przejdź do logowania" +loadFailed = "Nie można otworzyć udostÄ™pnionego pliku." +loading = "Åadowanie linku udostÄ™pniania..." +loginPrompt = "Zaloguj siÄ™, aby uzyskać dostÄ™p do tego udostÄ™pnionego pliku." +loginRequired = "Wymagane logowanie" +openInApp = "Otwórz w Stirling PDF" +ownerLabel = "WÅ‚aÅ›ciciel" +ownerUnknown = "Nieznany" +requiresLogin = "Ten udostÄ™pniony plik wymaga logowania." +roleCommenter = "KomentujÄ…cy" +roleEditor = "Edytor" +roleViewer = "PrzeglÄ…dajÄ…cy" +shareHeading = "UdostÄ™pniony plik" +titleDefault = "UdostÄ™pniony plik" +tryAgain = "Spróbuj ponownie później." +addUser = "Dodaj" +commenterHint = "Komentowanie już wkrótce." +copied = "Link skopiowany do schowka" +copy = "Kopiuj" +copyFailed = "Kopiowanie nie powiodÅ‚o siÄ™" +description = "Utwórz link udostÄ™pniania dla tego pliku. Zalogowani użytkownicy z linkiem bÄ™dÄ… mieli do niego dostÄ™p." +downloadsCount = "Pobrania: {{count}}" +emailWarningBody = "To wyglÄ…da na adres e-mail. JeÅ›li ta osoba nie jest użytkownikiem Stirling PDF, nie bÄ™dzie mogÅ‚a uzyskać dostÄ™pu do pliku." +emailWarningConfirm = "UdostÄ™pnij mimo to" +emailWarningTitle = "Adres e-mail" +errorTitle = "UdostÄ™pnianie nie powiodÅ‚o siÄ™" +failure = "Nie można wygenerować linku udostÄ™pniania. Spróbuj ponownie." +fileLabel = "Plik" +generate = "Wygeneruj link" +generated = "Wygenerowano link udostÄ™pniania" +hideActivity = "Ukryj aktywność" +invalidUsername = "Wpisz prawidÅ‚owÄ… nazwÄ™ użytkownika lub adres e-mail." +lastAccessed = "Ostatni dostÄ™p" +linkAccessTitle = "DostÄ™p przez link udostÄ™pniania" +linkLabel = "Link udostÄ™pniania" +linksDisabled = "Linki udostÄ™pniania sÄ… wyłączone." +linksDisabledBody = "Linki udostÄ™pniania sÄ… wyłączone przez ustawienia serwera." +manage = "ZarzÄ…dzaj udostÄ™pnianiem" +manageDescription = "Twórz i zarzÄ…dzaj linkami do udostÄ™pniania tego pliku." +manageLoadFailed = "Nie można wczytać linków udostÄ™pniania." +manageTitle = "ZarzÄ…dzaj udostÄ™pnianiem" +noActivity = "Brak aktywnoÅ›ci." +noLinks = "Brak aktywnych linków udostÄ™pniania." +noSharedUsers = "Å»aden użytkownik nie ma jeszcze dostÄ™pu." +removeLink = "UsuÅ„ link" +removeUser = "UsuÅ„" +revokeFailed = "Nie można usunąć linku udostÄ™pniania." +revoked = "UsuniÄ™to link udostÄ™pniania" +roleLabel = "Rola" +sharingDisabled = "UdostÄ™pnianie jest wyłączone." +sharingDisabledBody = "UdostÄ™pnianie zostaÅ‚o wyłączone w ustawieniach serwera." +sharedUsersTitle = "Użytkownicy z dostÄ™pem" +title = "UdostÄ™pnij plik" +unknownUser = "Nieznany użytkownik" +userAddFailed = "Nie można udostÄ™pnić temu użytkownikowi." +userAdded = "Użytkownik dodany do listy udostÄ™pniania." +usernameLabel = "Nazwa użytkownika lub e-mail" +usernamePlaceholder = "Wprowadź nazwÄ™ użytkownika lub e-mail" +userRemoveFailed = "Nie można usunąć tego użytkownika." +userRemoved = "Użytkownik usuniÄ™ty z listy udostÄ™pniania." +viewActivity = "WyÅ›wietl aktywność" +viewed = "WyÅ›wietlono" +viewsCount = "WyÅ›wietlenia: {{count}}" +downloaded = "Pobrano" +bulkDescription = "Utwórz jeden link, aby udostÄ™pnić wszystkie wybrane pliki zalogowanym użytkownikom." +bulkTitle = "UdostÄ™pnij wybrane pliki" +copyLink = "Kopiuj link udostÄ™pniania" +fileCount = "Wybrano {{count}} plików" +ownerOnly = "Tylko wÅ‚aÅ›ciciel może zarzÄ…dzać udostÄ™pnianiem." +selectSingleFile = "Wybierz jeden plik, aby zarzÄ…dzać udostÄ™pnianiem." + +[storageUpload] +description = "To przesyÅ‚a bieżący plik do magazynu serwera, aby uzyskać do niego dostÄ™p." +errorTitle = "PrzesyÅ‚anie nie powiodÅ‚o siÄ™" +failure = "PrzesyÅ‚anie nie powiodÅ‚o siÄ™. Sprawdź swoje dane logowania i ustawienia magazynu." +fileLabel = "Plik" +hint = "Publiczne linki i tryby dostÄ™pu sÄ… kontrolowane przez ustawienia serwera." +success = "PrzesÅ‚ano na serwer" +title = "PrzeÅ›lij na serwer" +updateButton = "Zaktualizuj na serwerze" +uploadButton = "PrzeÅ›lij na serwer" +bulkDescription = "To przesyÅ‚a wybrane pliki do magazynu serwera." +bulkTitle = "PrzeÅ›lij wybrane pliki" +fileCount = "Wybrano {{count}} plików" +more = " +{{count}} wiÄ™cej" + [storage] approximateSize = "Przybliżony rozmiar" fileTooLarge = "Plik jest zbyt duży. Maksymalny rozmiar na plik to" @@ -7153,6 +7801,30 @@ title = "PrzeglÄ…daj/Edytuj PDF" [warning] tooltipTitle = "Ostrzeżenie" +[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" + [watermark] completed = "Dodano znak wodny" desc = "Dodawaj znaki wodne tekstowe lub graficzne do plików PDF" @@ -7333,6 +8005,7 @@ activeSession = "Aktywna sesja" addMembers = "Dodaj czÅ‚onków" admin = "Administrator" confirmDelete = "Czy na pewno chcesz usunąć tego użytkownika? Tej operacji nie można cofnąć." +confirmUnlock = "Czy na pewno chcesz odblokować to konto użytkownika?" deleteUser = "UsuÅ„ użytkownika" deleteUserError = "Nie udaÅ‚o siÄ™ usunąć użytkownika" deleteUserSuccess = "Użytkownik usuniÄ™ty pomyÅ›lnie" @@ -7341,6 +8014,8 @@ disable = "Wyłącz" disabled = "Wyłączony" editRole = "Edytuj rolÄ™" enable = "Włącz" +locked = "zablokowane" +lockedBadge = "Zablokowane" loading = "Wczytywanie osób..." loginRequired = "Najpierw włącz tryb logowania" member = "CzÅ‚onek" @@ -7350,6 +8025,9 @@ searchMembers = "Szukaj czÅ‚onków..." status = "Status" team = "Zespół" title = "Osoby" +unlockAccount = "Odblokuj konto" +unlockUserError = "Nie udaÅ‚o siÄ™ odblokować konta użytkownika" +unlockUserSuccess = "PomyÅ›lnie odblokowano konto użytkownika" user = "Użytkownik" [workspace.people.actions] diff --git a/frontend/public/locales/pt-BR/translation.toml b/frontend/public/locales/pt-BR/translation.toml index de281c0e15..355fcf4112 100644 --- a/frontend/public/locales/pt-BR/translation.toml +++ b/frontend/public/locales/pt-BR/translation.toml @@ -8,6 +8,7 @@ black = "Preto" blue = "Azul" bored = "Entediado? Clique aqui!" cancel = "Cancelar" +confirm = "Confirmar" changedCredsMessage = "Credenciais alteradas!" chooseFile = "Escolher arquivo" close = "Fechar" @@ -146,6 +147,7 @@ insufficientCredits = "Créditos insuficientes. Necessário: {{requiredCredits}} loadingCredits = "Verificando créditos..." loadingProStatus = "Verificando status da assinatura..." noticeTopUpOrPlan = "Créditos insuficientes; recarregue ou atualize para um plano" +accessInvite = "Convidar" [account] accountSettings = "Configurações da Conta" @@ -1427,6 +1429,34 @@ title = "Processamento" description = "Tempo máximo de espera por um trabalho de processamento antes de informar um erro." label = "Tempo limite de processamento (segundos)" +[admin.settings.storage] +description = "Controla as opções de armazenamento e compartilhamento do servidor." +title = "Armazenamento e Compartilhamento de Arquivos" + +[admin.settings.storage.enabled] +description = "Permitir que os usuários armazenem arquivos no servidor." +label = "Ativar armazenamento de arquivos no servidor" + +[admin.settings.storage.sharing.email] +description = "Permitir compartilhamento com endereços de e-mail." +label = "Ativar compartilhamento por e-mail" +mailLink = "Configurar e-mail" +mailNote = "Requer configuração de e-mail. " + +[admin.settings.storage.sharing.enabled] +description = "Permitir que os usuários compartilhem arquivos armazenados." +label = "Ativar compartilhamento" + +[admin.settings.storage.sharing.links] +description = "Permitir compartilhamento via links autenticados." +frontendUrlLink = "Configurar nas configurações do sistema" +frontendUrlNote = "Requer uma URL de Frontend. " +label = "Ativar links de compartilhamento" + +[admin.settings.storage.signing.enabled] +description = "Permitir que os usuários criem sessões de assinatura de documentos com vários participantes. Requer que o armazenamento de arquivos no servidor esteja ativado." +label = "Ativar assinatura em grupo (Alpha)" + [admin.settings.unsavedChanges] cancel = "Continuar editando" discard = "Descartar alterações" @@ -2059,7 +2089,19 @@ numbers = "Números/intervalos: 5, 10-20" progressions = "Progressões: 3n, 4n+1" [certSign] +allSigned = "Todos os participantes assinaram. Pronto para finalizar." +awaitingSignatures = "Aguardando assinaturas" +signatureProgress = "{{signedCount}}/{{totalCount}} assinaturas" chooseCertificate = "Escolher arquivo de certificado" +declined = "Recusado" +fetchFailed = "Falha ao carregar dados de assinatura" +finalized = "Finalizado" +notified = "Pendente" +partialNote = "Você pode finalizar antes com as assinaturas atuais. Participantes que não assinaram serão excluídos." +pending = "Pendente" +readyToFinalize = "Pronto para finalizar" +signed = "Assinado" +viewed = "Visualizado" chooseJksFile = "Escolher arquivo JKS" chooseP12File = "Escolher arquivo PKCS12" choosePfxFile = "Escolher arquivo PFX" @@ -2082,6 +2124,7 @@ title = "Assinatura com Certificado" invisible = "Invisível" stepTitle = "Aparência da assinatura" visible = "Visível" +visibility = "Visibilidade" [certSign.appearance.options] title = "Detalhes da assinatura" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Finalizar e carregar PDF assinado" +early = "Finalizar com as assinaturas atuais" + +[certSign.collab.sessionDetail] +addButton = "Adicionar participantes" +addParticipants = "Adicionar participantes" +addParticipantsError = "Falha ao adicionar participantes" +backToList = "Voltar para sessões" +deleteConfirm = "Tem certeza? Isso não pode ser desfeito." +deleteError = "Falha ao excluir sessão" +deleted = "Sessão excluída" +deleteSession = "Excluir sessão" +dueDate = "Data de vencimento" +finalizeError = "Falha ao finalizar sessão" +loadPdfError = "Falha ao carregar o PDF assinado" +loadSignedPdf = "Carregar PDF assinado em arquivos ativos" +messageLabel = "Mensagem" +noAdditionalInfo = "Sem informações adicionais" +owner = "Proprietário" +participantRemoved = "Participante removido" +participants = "Participantes" +participantsAdded = "Participantes adicionados com sucesso" +removeParticipant = "Remover" +removeParticipantError = "Falha ao remover participante" +selectUsers = "Selecionar usuários..." +sessionInfo = "Informações da sessão" +workbenchTitle = "Gerenciamento da sessão" + +[certSign.collab.signRequest] +addedToFiles = "Documento adicionado aos arquivos ativos" +addSignature = "Adicionar sua assinatura" +addToFiles = "Adicionar aos arquivos ativos" +advancedSettings = "Configurações avançadas" +backToList = "Voltar para solicitações de assinatura" +certificateChoice = "Selecione um certificado para assinar" +changeSignature = "Alterar assinatura" +clearSignature = "Limpar assinatura" +completeAndSign = "Concluir e assinar" +createNewSignature = "Criar nova assinatura" +declineButton = "Recusar" +decline = "Recusar solicitação" +deleteSelected = "Excluir assinatura selecionada" +drawSignature = "Desenhe sua assinatura abaixo" +dueDate = "Data de vencimento" +fileTooLarge = "O tamanho do arquivo deve ser menor que 5MB" +fontFamily = "Família da fonte" +fontSize = "Tamanho da fonte: {{size}}px" +fontSizePlaceholder = "Tamanho" +from = "De" +invalidCertFile = "Selecione um arquivo de certificado P12 ou PFX" +invalidFileType = "Selecione um arquivo de imagem" +location = "Localização (opcional)" +locationPlaceholder = "De onde você está assinando?" +message = "Mensagem" +noCertificate = "Selecione um arquivo de certificado" +noSignatures = "Coloque ao menos uma assinatura no PDF" +p12File = "Arquivo de certificado P12/PFX" +password = "Senha do certificado" +passwordPlaceholder = "Digite a senha..." +penColor = "Cor da caneta" +penSize = "Tamanho da caneta: {{size}}px" +placementActive = "Clique no PDF para posicionar" +placeSignatureButton = "Colocar assinatura no PDF" +reason = "Motivo (opcional)" +reasonPlaceholder = "Por que você está assinando?" +removeImage = "Remover imagem" +removeCertFile = "Remover arquivo" +savedSignatures = "Assinaturas salvas" +selectFile = "Selecionar arquivo de imagem" +selectSignatureTitle = "Selecionar ou criar assinatura" +signButton = "Assinar documento" +signatureInfo = "Essas configurações são definidas pelo proprietário do documento" +signaturePlaced = "Assinatura colocada na página" +signatureSettings = "Configurações de assinatura" +signatureText = "Texto da assinatura" +signatureTextPlaceholder = "Digite seu nome..." +signatureTypeLabel = "Tipo de assinatura" +signingTitle = "Assinatura" +textColor = "Cor do texto" +typeSignature = "Digite seu nome para criar uma assinatura" +uploadCert = "Certificado personalizado" +uploadCertDesc = "Use seu próprio certificado P12/PFX" +uploadSignature = "Enviar imagem da sua assinatura" +usePersonalCert = "Certificado pessoal" +usePersonalCertDesc = "Gerado automaticamente para sua conta" +useServerCert = "Certificado da organização" +useServerCertDesc = "Certificado compartilhado da organização" +workbenchTitle = "Solicitação de assinatura" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Escolher cor do traço" +continue = "Continuar" + +[certSign.collab.signRequest.certModal] +description = "Você colocou {{count}} assinatura(s). Escolha seu certificado para concluir a assinatura." +sign = "Assinar documento" +certValidating = "Validando certificado..." +certValidUntil = "Certificado válido até {{date}}" +certInvalid = "Certificado inválido: {{error}}" +certInvalidFallback = "Certificado inválido" +certNetworkError = "Não foi possível validar o certificado" +title = "Configurar certificado" + +[certSign.collab.signRequest.image] +hint = "Envie uma imagem PNG ou JPG da sua assinatura" + +[certSign.collab.signRequest.mode] +move = "Mover assinatura" +place = "Colocar assinatura" +title = "Modo de assinar ou mover" + +[certSign.collab.signRequest.modeTabs] +draw = "Desenhar" +image = "Enviar" +text = "Digitar" + +[certSign.collab.signRequest.placeSignature] +message = "Clique no PDF para posicionar sua assinatura" +title = "Colocar assinatura" + +[certSign.collab.signRequest.preview] +imageAlt = "Assinatura selecionada" +missing = "Sem prévia" +textFallback = "Assinatura" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Assinatura desenhada" +defaultImageLabel = "Assinatura enviada" +defaultLabel = "Assinatura" +defaultTextLabel = "Assinatura digitada" +delete = "Excluir assinatura" +none = "Nenhuma assinatura salva" + +[certSign.collab.signRequest.signatureType] +draw = "Desenhar" +type = "Digitar" +upload = "Enviar" + +[certSign.collab.signRequest.steps] +back = "Voltar" +cancelPlacement = "Cancelar posicionamento" +certificate = "Certificado" +clickMultipleTimes = "Clique várias vezes no PDF para posicionar assinaturas. Arraste qualquer assinatura para mover ou redimensionar." +clickToPlace = "Clique no PDF onde você deseja que sua assinatura apareça." +continue = "Continuar para seleção de certificado" +continueToPlacement = "Continuar para posicionamento" +continueToReview = "Continuar para revisão" +createSignature = "Criar assinatura" +invisible = "Invisível" +location = "Localização:" +multipleSignatures = "{{count}} assinaturas serão aplicadas ao PDF" +oneSignature = "1 assinatura será aplicada ao PDF" +placeOnPdf = "Colocar no PDF" +reason = "Motivo:" +reviewTitle = "Revisar antes de assinar" +signaturePlaced = "Assinatura colocada na página {{page}}. Você pode ajustar a posição clicando novamente ou continuar para a revisão." +visible = "Visível" +visibility = "Visibilidade:" +yourSignatures = "Suas assinaturas ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Cor" +fontLabel = "Fonte" +fontSizeLabel = "Tamanho" +fontSizePlaceholder = "16" +label = "Texto da assinatura" +modalHint = "Digite seu nome e clique em Continuar para posicioná-lo no PDF." +placeholder = "Digite seu nome..." + +[certSign.collab.participant] +certValidating = "Validando certificado..." +certValid = "✓ Certificado válido" +certValidUntil = " até {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Certificado inválido" +certNetworkError = "Não foi possível validar o certificado" + +[certSign.collab.addParticipants] +add = "Adicionar {{count}} participante(s)" +back = "Voltar" +configureSignatures = "Configurar as configurações de assinatura" +continue = "Continuar para configurações de assinatura" +reasonHelp = "Predefina um motivo de assinatura para estes participantes (opcional; eles podem alterar ao assinar)" +reasonPlaceholder = "ex.: Aprovação, Revisão..." +selectUsers = "Selecionar usuários" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Incluir página de resumo de assinaturas" +includeSummaryPageHelp = "Uma página de resumo será adicionada ao final com todos os metadados de assinatura. As caixas de assinatura do certificado digital nas páginas individuais serão suprimidas (assinaturas manuscritas não são afetadas)." + +[certSign.collab.sessionList] +active = "Ativa" +finalized = "Finalizada" + +[certSign.collab.signatureSettings] +description = "Configurar como as assinaturas aparecerão para todos os participantes" +title = "Aparência da assinatura" + +[certSign.collab.userSelector] +inviteUsers = "Adicionar usuários" +loadError = "Falha ao carregar usuários" +noTeam = "Sem equipe" +noUsers = "Nenhum outro usuário encontrado." +placeholder = "Selecionar usuários..." + +[certSign.mobile] +panelActions = "Ações" +panelDocument = "Documento" +panelPeople = "Pessoas" + +[certSign.sessions] +deleted = "Sessão excluída" +fetchFailed = "Falha ao carregar detalhes da sessão" +finalized = "Sessão finalizada" +loaded = "PDF assinado carregado" +pdfNotReady = "PDF não está pronto" +pdfNotReadyDesc = "O PDF assinado está sendo gerado. Tente novamente em instantes." + +[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" + [changeCreds] changePassword = "Você está usando as credenciais padrões. Por favor, insira uma nova senha" changeUsername = "Atualize seu nome de usuário. Você será desconectado após a atualização." @@ -3242,6 +3531,46 @@ totalSelected = "Total selecionado" unsupported = "Não suportado" unzip = "Descompactar" uploadError = "Falha ao fazer upload de alguns arquivos." +copyCreated = "Cópia salva neste dispositivo." +copyFailed = "Não foi possível criar uma cópia." +leaveShare = "Remover da minha lista" +leaveShareFailed = "Não foi possível remover o arquivo compartilhado." +leaveShareSuccess = "Removido da sua lista de compartilhados." +removeBoth = "Remover de ambos" +removeFilePrompt = "Este arquivo está salvo neste dispositivo e no seu servidor. De onde você deseja removê-lo?" +removeFileTitle = "Remover arquivo" +removeLocalOnly = "Apenas neste dispositivo" +removeServerFailed = "Não foi possível remover o arquivo do servidor." +removeServerOnly = "Apenas no servidor" +removeServerOnlyPrompt = "Este arquivo está armazenado apenas no seu servidor. Deseja removê-lo do servidor?" +removeServerSuccess = "Removido do servidor." +removeSharedPrompt = "Este arquivo foi compartilhado com você. Você pode removê-lo deste dispositivo ou da sua lista de compartilhados." +removeSharedServerOnlyBlockedPrompt = "Este arquivo foi compartilhado com você e está armazenado apenas no servidor." +removeSharedServerOnlyPrompt = "Este arquivo foi compartilhado com você e está armazenado apenas no servidor. Removê-lo da sua lista?" +changesNotUploaded = "Alterações não enviadas" +cloudFile = "Arquivo na nuvem" +filterAll = "Todos" +filterLocal = "Local" +filterSharedByMe = "Compartilhados por mim" +filterSharedWithMe = "Compartilhados comigo" +lastSynced = "Última sincronização" +localOnly = "Apenas local" +makeCopy = "Fazer uma cópia" +owner = "Proprietário" +ownerUnknown = "Desconhecido" +share = "Compartilhar" +shareSelected = "Compartilhar selecionados" +sharedByYou = "Compartilhado por você" +sharedEditNoticeBody = "Você não tem direitos de edição na versão do servidor deste arquivo. Quaisquer edições serão salvas como uma cópia local." +sharedEditNoticeConfirm = "Entendi" +sharedEditNoticeTitle = "Cópia somente leitura no servidor" +sharedWithYou = "Compartilhado com você" +sharing = "Compartilhamento" +storageState = "Armazenamento" +synced = "Sincronizado" +updateOnServer = "Atualizar no servidor" +uploadSelected = "Enviar selecionados" +uploadToServer = "Enviar para o servidor" [files] addFiles = "Adicionar arquivos" @@ -3367,6 +3696,77 @@ title = "Sobre o achatamento de PDFs" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Sobre assinatura em grupo" + +[groupSigning.tooltip.finalization] +bullet1 = "Todas as assinaturas são aplicadas na ordem de participantes que você especificou" +bullet2 = "Você pode finalizar com assinaturas parciais, se necessário" +bullet3 = "Depois de finalizada, a sessão não pode ser modificada" +description = "Quando todos os participantes tiverem assinado (ou você optar por finalizar antes), você pode gerar o PDF final assinado." +title = "Processo de finalização" + +[groupSigning.tooltip.roles] +bullet1 = "Proprietário (você): Cria a sessão, configura padrões de assinatura, finaliza o documento" +bullet2 = "Participantes: Criam sua assinatura, escolhem o certificado, posicionam no PDF" +bullet3 = "Participantes não podem modificar as configurações de visibilidade, motivo ou localização da assinatura" +description = "Você controla as configurações de aparência da assinatura para todos os participantes." +title = "Papéis dos participantes" + +[groupSigning.tooltip.sequential] +bullet1 = "O primeiro participante deve assinar antes que o segundo possa acessar o documento" +bullet2 = "Garante a ordem de assinatura adequada para conformidade legal" +bullet3 = "Você pode reordenar os participantes arrastando-os na lista" +description = "Os participantes assinam os documentos na ordem que você especificar. Cada assinante recebe uma notificação quando é sua vez." +title = "Assinatura sequencial" + +[groupSigning.steps] +back = "Voltar" +completed = "Concluído" +current = "Atual" +stepLabel = "Etapa {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Continuar para revisão" +invisible = "As assinaturas serão invisíveis (apenas metadados)" +locationLabel = "Localização:" +preview = "Prévia" +reasonLabel = "Motivo:" +title = "Configurar configurações de assinatura" +visible = "As assinaturas serão visíveis na página {{page}}" + +[groupSigning.steps.review] +document = "Documento" +dueDate = "Data de vencimento (opcional)" +dueDatePlaceholder = "Selecione a data de vencimento..." +invisible = "Invisível (apenas metadados)" +location = "Localização:" +logo = "Logotipo:" +logoHidden = "Sem logotipo" +logoShown = "Logotipo da Stirling PDF exibido" +participants = "Participantes" +reason = "Motivo:" +send = "Enviar solicitações de assinatura" +signatureSettings = "Configurações de assinatura" +title = "Revisar detalhes da sessão" +titleShort = "Revisar e enviar" +visibility = "Visibilidade:" +visible = "Visível na página {{page}}" +participantCount = "{{count}} participante(s) assinará(ão) em ordem" + +[groupSigning.steps.selectDocument] +continue = "Continuar para seleção de participantes" +noFile = "Selecione um único arquivo PDF dos seus arquivos ativos para criar uma sessão de assinatura." +selectedFile = "Documento selecionado" +title = "Selecionar documento" + +[groupSigning.steps.selectParticipants] +continue = "Continuar para configurações de assinatura" +count = "{{count}} participante(s) selecionado(s)" +label = "Selecionar participantes" +placeholder = "Escolha os participantes para assinar..." +title = "Escolher participantes" + [getPdfInfo] downloadJson = "Baixar JSON" downloads = "Downloads" @@ -4460,7 +4860,10 @@ zoomOut = "Reduzir" [viewer] cannotPreviewFile = "Não é possível visualizar o arquivo" +disableColorFilter = "Desativar filtro de cor" dualPageView = "Visualização de duas páginas" +enableDarkFilter = "Ativar filtro escuro" +enableSepiaFilter = "Ativar filtro sépia" firstPage = "Primeira página" lastPage = "Última página" nextPage = "Próxima página" @@ -4470,6 +4873,22 @@ singlePageView = "Visualização de página única" unknownFile = "Arquivo desconhecido" zoomIn = "Ampliar" zoomOut = "Reduzir" +resetZoom = "Redefinir zoom" + +[viewer.nonPdf] +fileTypeBadge = "Arquivo {{type}}" +convertToPdf = "Converter para PDF" +loading = "Carregando..." +emptyFile = "Arquivo vazio" +csvStats = "{{rows}} linhas · {{columns}} colunas · {{size}}" +sortedBy = "Ordenado por: {{column}}" +columnDefault = "Coluna {{index}}" +htmlPreviewWarning = "Prévia de HTML — recursos externos podem não carregar · {{size}}" +htmlPreview = "Prévia de HTML" +invalidJson = "JSON inválido — exibindo conteúdo bruto" +textStats = "{{lines}} linhas · {{size}}" +lineNumbers = "Números de linha" +renderMarkdown = "Renderizar markdown" [viewer.attachments] title = "Anexos" @@ -4531,6 +4950,7 @@ toggleAttachments = "Alternar anexos" toggleTheme = "Alternar tema" language = "Idioma" toggleAnnotations = "Alternar visibilidade das anotações" +toggleLayers = "Alternar camadas" search = "Pesquisar PDF" panMode = "Modo de panorâmica" applyRedactionsFirst = "Aplicar redações primeiro" @@ -5407,20 +5827,72 @@ title = "Imprimir arquivo" 2 = "Digite o nome da impressora" [quickAccess] +access = "Acesso" +accessAddPerson = "Adicionar outra pessoa" +accessBack = "Voltar" +accessCopyLink = "Copiar link" +accessEmail = "Endereço de e-mail" +accessEmailPlaceholder = "nome@empresa.com" +accessFileLabel = "Arquivo" +accessGeneral = "Acesso geral" +accessInviteTitle = "Convidar pessoas" +accessOwner = "Proprietário" +accessPanel = "Acesso ao documento" +accessPeople = "Pessoas com acesso" +accessRemove = "Remover" +accessRestricted = "Restrito" +accessRestrictedHint = "Apenas pessoas com acesso podem abrir" +accessRole = "Função" +accessRoleCommenter = "Comentarista" +accessRoleEditor = "Editor" +accessRoleViewer = "Visualizador" +accessSelectedFile = "Arquivo selecionado" +accessSendInvite = "Enviar convite" +accessTitle = "Acesso ao documento" +accessYou = "Você" account = "Conta" +activeSessions = "Sessões ativas" +activeTab = "Ativas" activity = "Ativ." adminSettings = "Ajustes admin" +allSessions = "Todas as sessões" allTools = "Ferram." automate = "Automat." +back = "Voltar" +certSign = "Assinatura com certificado" +completedSessions = "Sessões concluídas" +completedTab = "Concluídas" config = "Config" +createNew = "Criar nova solicitação" +createSession = "Criar solicitação de assinatura" +dueDate = "Data de vencimento (opcional)" files = "Arquivos" help = "Ajuda" +noActiveSessions = "Nenhuma solicitação pendente ou sessão ativa" +noCompletedSessions = "Nenhuma sessão concluída" +noFile = "Nenhum arquivo selecionado" read = "Ler" reader = "Leitor" +refresh = "Atualizar" +requestSignatures = "Solicitar assinaturas" +selectSingleFileToRequest = "Selecione um único arquivo PDF para solicitar assinaturas" +selectedFile = "Arquivo selecionado" +selectUsers = "Selecionar usuários para assinar" +selectUsersPlaceholder = "Escolha os participantes..." +sendingRequest = "Enviando..." settings = "Ajustes" showMeAround = "Faça um tour" sign = "Assinar" +signatureRequests = "Solicitações de assinatura" +signYourself = "Assinar você mesmo" +newRequest = "Nova solicitação" tours = "Tours" +wetSign = "Adicionar assinatura" +filterMine = "Meus" +filterOverdue = "Em atraso" +filterSigned = "Assinadas" +filterDeclined = "Recusadas" +searchDocuments = "Pesquisar documentos…" [quickAccess.helpMenu] adminTour = "Tour do administrador" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Seu servidor Stirling-PDF está offline e \"{{endpoin expired = "Sua sessão expirou. Por gentileza atualize a página e tente novamente." refreshPage = "Atualizar Página" +[sessionManagement.tooltip] +header = "Gerenciando sessões de assinatura" + +[sessionManagement.tooltip.addParticipants] +bullet1 = "Novos participantes adicionados ao final 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 é sua vez" +description = "Você pode adicionar mais participantes a uma sessão ativa a qualquer momento antes da finalização." +title = "Adicionando participantes" + +[sessionManagement.tooltip.finalization] +bullet1 = "Finalização completa: Todos os participantes assinaram" +bullet2 = "Finalização parcial: Alguns participantes ainda não assinaram" +bullet3 = "Participantes que não assinaram serão excluídos do documento final" +bullet4 = "Depois de finalizada, você pode carregar o PDF assinado nos arquivos ativos" +description = "A finalização combina todas as assinaturas em um único PDF assinado. Essa ação não pode ser desfeita." +title = "Finalização da sessão" + +[sessionManagement.tooltip.participantRemoval] +bullet1 = "Não é possível remover participantes que já assinaram" +bullet2 = "Participantes removidos não recebem mais notificações" +bullet3 = "A ordem de assinatura é ajustada automaticamente" +description = "Participantes podem ser removidos das sessões antes de assinarem." +title = "Removendo participantes" + +[sessionManagement.tooltip.signatureOrder] +bullet1 = "Cada assinatura é aplicada sequencialmente ao PDF" +bullet2 = "Assinantes posteriores podem ver assinaturas anteriores" +bullet3 = "Crítico para fluxos de aprovação e cadeias de custódia legais" +description = "A ordem que você especifica ao criar a sessão determina quem assina primeiro." +title = "Ordem de assinatura" + +[signatureSettings.tooltip] +header = "Configurações de aparência da assinatura" + +[signatureSettings.tooltip.location] +bullet1 = "Exemplos: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Não é o mesmo que a posição na página" +bullet3 = "Pode ser exigido por certas jurisdições legais" +description = "Localização geográfica opcional onde a assinatura foi aplicada. Armazenada nos metadados do certificado." +title = "Localização da assinatura" + +[signatureSettings.tooltip.logo] +bullet1 = "Exibido junto da assinatura e do texto" +bullet2 = "Compatível com formatos PNG, JPG" +bullet3 = "Melhora a aparência profissional" +description = "Adicione um logotipo da empresa às assinaturas visíveis para branding e autenticidade." +title = "Logotipo da empresa" + +[signatureSettings.tooltip.reason] +bullet1 = "Exemplos: \"Aprovação\", \"Acordo de Contrato\", \"Revisão Concluída\"" +bullet2 = "Visível nas propriedades de assinatura do PDF" +bullet3 = "Útil para trilhas de auditoria e conformidade" +description = "Texto opcional explicando por que o documento está sendo assinado. Armazenado nos metadados do certificado." +title = "Motivo da assinatura" + +[signatureSettings.tooltip.visibility] +bullet1 = "Visível: A assinatura aparece no PDF com aparência personalizada" +bullet2 = "Invisível: Certificado incorporado sem marca visual" +bullet3 = "Assinaturas invisíveis ainda fornecem validação criptográfica" +description = "Controla se a assinatura é visível no documento ou incorporada de forma invisível." +title = "Visibilidade da assinatura" + [settings.configuration] advanced = "Avançado" database = "Banco de dados" endpoints = "Endpoints" features = "Recursos" +storageSharing = "Armazenamento e compartilhamento de arquivos" systemSettings = "Configurações do sistema" title = "Configuração" @@ -6332,10 +6868,13 @@ title = "Entrar no Stirling" [setup.selfhosted] link = "ou conecte-se a uma conta auto-hospedada" subtitle = "Informe suas credenciais do servidor" +changeServerLocked = "Sua organização restringiu este aplicativo a um servidor específico" switchToLocal = "Usar ferramentas locais em vez disso" title = "Entrar no servidor" [setup.selfhosted.unreachable] +changeServer = "Conectar a um servidor diferente" +changeServerLocked = "Sua organização restringiu este aplicativo a um servidor específico" continueOffline = "Usar ferramentas locais em vez disso" message = "Não foi possível acessar {{url}}. Verifique se o servidor está em execução e acessível." retry = "Tentar novamente" @@ -6529,6 +7068,15 @@ saved = "Salvas" text = "Texto" title = "Tipo de assinatura" +[signRequest] +declined = "Solicitação de assinatura recusada" +fetchFailed = "Falha ao carregar solicitação de assinatura" +signed = "Documento assinado com sucesso" + +[signSession] +createFailed = "Falha ao criar solicitação de assinatura" +created = "Solicitação de assinatura enviada" + [signup] accountCreatedSuccessfully = "Conta criada com sucesso! Agora você pode entrar." alreadyHaveAccount = "Já tem uma conta? Entre" @@ -6807,6 +7355,106 @@ title = "Divide PDF por Capítulos" [splitPdfByChapters] tags = "dividir,capítulos,favoritos,organizar" +[storageShare] +accessed = "Acessado" +accessDenied = "Você não tem acesso a este arquivo compartilhado. Peça ao proprietário para compartilhá-lo com você." +accessFailed = "Não foi possível carregar a atividade." +accessDeniedBody = "Você não tem acesso a este arquivo. Peça ao proprietário para compartilhá-lo com você." +accessDeniedTitle = "Sem acesso" +accessLimitedCommenter = "O acesso de comentarista estará disponível em breve. Peça ao proprietário acesso de editor se precisar baixar." +accessLimitedTitle = "Acesso limitado" +accessLimitedViewer = "Este link é somente visualização. Peça ao proprietário acesso de editor se precisar baixar." +createdAt = "Criado" +download = "Baixar" +downloadFailed = "Não foi possível baixar este arquivo." +expiredBody = "Este link de compartilhamento é inválido ou expirou." +expiredTitle = "Link expirado" +goToLogin = "Ir para login" +loadFailed = "Não foi possível abrir o arquivo compartilhado." +loading = "Carregando link de compartilhamento..." +loginPrompt = "Faça login para acessar este arquivo compartilhado." +loginRequired = "Login necessário" +openInApp = "Abrir no Stirling PDF" +ownerLabel = "Proprietário" +ownerUnknown = "Desconhecido" +requiresLogin = "Este arquivo compartilhado requer login." +roleCommenter = "Comentarista" +roleEditor = "Editor" +roleViewer = "Visualizador" +shareHeading = "Arquivo compartilhado" +titleDefault = "Arquivo compartilhado" +tryAgain = "Tente novamente mais tarde." +addUser = "Adicionar" +commenterHint = "Comentários em breve." +copied = "Link copiado para a área de transferência" +copy = "Copiar" +copyFailed = "Falha ao copiar" +description = "Crie um link de compartilhamento para este arquivo. Usuários logados com o link podem acessá-lo." +downloadsCount = "Downloads: {{count}}" +emailWarningBody = "Isso parece um endereço de e-mail. Se essa pessoa ainda não for usuária do Stirling PDF, ela não conseguirá acessar o arquivo." +emailWarningConfirm = "Compartilhar mesmo assim" +emailWarningTitle = "Endereço de e-mail" +errorTitle = "Falha no compartilhamento" +failure = "Não foi possível gerar um link de compartilhamento. Tente novamente." +fileLabel = "Arquivo" +generate = "Gerar link" +generated = "Link de compartilhamento gerado" +hideActivity = "Ocultar atividade" +invalidUsername = "Insira um nome de usuário ou endereço de e-mail válido." +lastAccessed = "Último acesso" +linkAccessTitle = "Acesso via link de compartilhamento" +linkLabel = "Link de compartilhamento" +linksDisabled = "Links de compartilhamento desativados." +linksDisabledBody = "Links de compartilhamento estão desativados pelas configurações do seu servidor." +manage = "Gerenciar compartilhamento" +manageDescription = "Criar e gerenciar links para compartilhar este arquivo." +manageLoadFailed = "Não foi possível carregar os links de compartilhamento." +manageTitle = "Gerenciar compartilhamento" +noActivity = "Ainda sem atividade." +noLinks = "Ainda não há links de compartilhamento ativos." +noSharedUsers = "Nenhum usuário tem acesso ainda." +removeLink = "Remover link" +removeUser = "Remover" +revokeFailed = "Não foi possível remover o link de compartilhamento." +revoked = "Link de compartilhamento removido" +roleLabel = "Função" +sharingDisabled = "O compartilhamento está desativado." +sharingDisabledBody = "O compartilhamento foi desativado pelas configurações do seu servidor." +sharedUsersTitle = "Usuários com acesso" +title = "Compartilhar arquivo" +unknownUser = "Usuário desconhecido" +userAddFailed = "Não foi possível compartilhar com esse usuário." +userAdded = "Usuário adicionado à lista de compartilhamento." +usernameLabel = "Nome de usuário ou e-mail" +usernamePlaceholder = "Insira um nome de usuário ou e-mail" +userRemoveFailed = "Não foi possível remover esse usuário." +userRemoved = "Usuário removido da lista de compartilhamento." +viewActivity = "Ver atividade" +viewed = "Visualizado" +viewsCount = "Visualizações: {{count}}" +downloaded = "Baixado" +bulkDescription = "Crie um único link para compartilhar todos os arquivos selecionados com usuários autenticados." +bulkTitle = "Compartilhar arquivos selecionados" +copyLink = "Copiar link de compartilhamento" +fileCount = "{{count}} arquivos selecionados" +ownerOnly = "Apenas o proprietário pode gerenciar o compartilhamento." +selectSingleFile = "Selecione um único arquivo para gerenciar o compartilhamento." + +[storageUpload] +description = "Isso envia o arquivo atual para o armazenamento do servidor para o seu próprio acesso." +errorTitle = "Falha no envio" +failure = "Falha no envio. Verifique seu login e as configurações de armazenamento." +fileLabel = "Arquivo" +hint = "Links públicos e modos de acesso são controlados pelas configurações do seu servidor." +success = "Enviado para o servidor" +title = "Enviar para o servidor" +updateButton = "Atualizar no servidor" +uploadButton = "Enviar para o servidor" +bulkDescription = "Isso envia os arquivos selecionados para o armazenamento do seu servidor." +bulkTitle = "Enviar arquivos selecionados" +fileCount = "{{count}} arquivos selecionados" +more = " +{{count}} mais" + [storage] approximateSize = "Tamanho aproximado" fileTooLarge = "Arquivo muito grande. Tamanho máximo por arquivo é" @@ -7153,6 +7801,30 @@ title = "Ver/Editar PDF" [warning] tooltipTitle = "Aviso" +[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 redesenhe até ficar satisfeito" +bullet3 = "Funciona em dispositivos de toque (tablets, celulares)" +description = "Crie uma assinatura manuscrita usando o mouse ou a tela sensível ao toque. Ideal para assinaturas pessoais e autênticas." +title = "Desenhar assinatura" + +[wetSignature.tooltip.type] +bullet1 = "Escolha entre várias fontes" +bullet2 = "Personalize o tamanho e a cor do texto" +bullet3 = "Perfeito para assinaturas padronizadas" +description = "Gere uma assinatura a partir de texto digitado. Rápido e consistente, adequado para documentos comerciais." +title = "Digitar assinatura" + +[wetSignature.tooltip.upload] +bullet1 = "Compatível com PNG, JPG e outros formatos de imagem" +bullet2 = "Fundos transparentes são recomendados para melhores resultados" +bullet3 = "A imagem será redimensionada para caber na área da assinatura" +description = "Envie uma imagem de assinatura pré-criada. Ideal se você tiver uma assinatura digitalizada ou o logotipo da empresa." +title = "Enviar imagem da assinatura" + [watermark] completed = "Marca d'água adicionada" desc = "Adicione marcas d'água de texto ou imagem a arquivos PDF" @@ -7333,6 +8005,7 @@ activeSession = "Sessão ativa" addMembers = "Adicionar membros" admin = "Admin" confirmDelete = "Tem certeza de que deseja excluir este usuário? Esta ação não pode ser desfeita." +confirmUnlock = "Tem certeza de que deseja desbloquear esta conta de usuário?" deleteUser = "Excluir usuário" deleteUserError = "Falha ao excluir usuário" deleteUserSuccess = "Usuário excluído com sucesso" @@ -7341,6 +8014,8 @@ disable = "Desativar" disabled = "Desativado" editRole = "Editar função" enable = "Ativar" +locked = "bloqueado" +lockedBadge = "Bloqueado" loading = "Carregando pessoas..." loginRequired = "Ative o modo de login primeiro" member = "Membro" @@ -7350,6 +8025,9 @@ searchMembers = "Pesquisar membros..." status = "Status" team = "Equipe" title = "Pessoas" +unlockAccount = "Desbloquear conta" +unlockUserError = "Falha ao desbloquear a conta do usuário" +unlockUserSuccess = "Conta do usuário desbloqueada com sucesso" user = "Usuário" [workspace.people.actions] diff --git a/frontend/public/locales/pt-PT/translation.toml b/frontend/public/locales/pt-PT/translation.toml index 9d59c54cff..80f6be720d 100644 --- a/frontend/public/locales/pt-PT/translation.toml +++ b/frontend/public/locales/pt-PT/translation.toml @@ -8,6 +8,7 @@ black = "Preto" blue = "Azul" bored = "Entediado à espera?" cancel = "Cancelar" +confirm = "Confirmar" changedCredsMessage = "Credenciais alteradas!" chooseFile = "Escolher ficheiro" close = "Fechar" @@ -146,6 +147,7 @@ insufficientCredits = "Créditos insuficientes. Necessários: {{requiredCredits} loadingCredits = "A verificar créditos..." loadingProStatus = "A verificar o estado da subscrição..." noticeTopUpOrPlan = "Créditos insuficientes, recarregue ou atualize para um plano" +accessInvite = "Convidar" [account] accountSettings = "Definições de Conta" @@ -1427,6 +1429,34 @@ title = "Processamento" description = "Tempo máximo de espera por um trabalho de processamento antes de reportar um erro." label = "Tempo limite de processamento (segundos)" +[admin.settings.storage] +description = "Controlar o armazenamento no servidor e as opções de partilha." +title = "Armazenamento de Ficheiros e Partilha" + +[admin.settings.storage.enabled] +description = "Permitir que os utilizadores armazenem ficheiros no servidor." +label = "Ativar armazenamento de ficheiros no servidor" + +[admin.settings.storage.sharing.email] +description = "Permitir partilha com endereços de e-mail." +label = "Ativar partilha por e-mail" +mailLink = "Configurar definições de e-mail" +mailNote = "Requer configuração de e-mail. " + +[admin.settings.storage.sharing.enabled] +description = "Permitir que os utilizadores partilhem ficheiros armazenados." +label = "Ativar partilha" + +[admin.settings.storage.sharing.links] +description = "Permitir partilha através de ligações autenticadas." +frontendUrlLink = "Configurar nas Definições do Sistema" +frontendUrlNote = "Requer um URL do Frontend. " +label = "Ativar ligações de partilha" + +[admin.settings.storage.signing.enabled] +description = "Permitir que os utilizadores criem sessões de assinatura com vários participantes. Requer o armazenamento de ficheiros no servidor ativado." +label = "Ativar assinatura em grupo (Alpha)" + [admin.settings.unsavedChanges] cancel = "Continuar a editar" discard = "Descartar alterações" @@ -2059,7 +2089,19 @@ numbers = "Números/intervalos: 5, 10-20" progressions = "Progressões: 3n, 4n+1" [certSign] +allSigned = "Todos os participantes assinaram. Pronto para finalizar." +awaitingSignatures = "A aguardar assinaturas" +signatureProgress = "{{signedCount}}/{{totalCount}} assinaturas" chooseCertificate = "Escolher ficheiro de certificado" +declined = "Recusado" +fetchFailed = "Não foi possível carregar os dados de assinatura" +finalized = "Finalizado" +notified = "Pendente" +partialNote = "Pode finalizar antecipadamente com as assinaturas atuais. Os participantes não assinados serão excluídos." +pending = "Pendente" +readyToFinalize = "Pronto para finalizar" +signed = "Assinado" +viewed = "Visto" chooseJksFile = "Escolher ficheiro JKS" chooseP12File = "Escolher ficheiro PKCS12" choosePfxFile = "Escolher ficheiro PFX" @@ -2082,6 +2124,7 @@ title = "Assinatura de Certificado" invisible = "Invisível" stepTitle = "Aparência da Assinatura" visible = "Visível" +visibility = "Visibilidade" [certSign.appearance.options] title = "Detalhes da Assinatura" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Finalizar e carregar o PDF assinado" +early = "Finalizar com as assinaturas atuais" + +[certSign.collab.sessionDetail] +addButton = "Adicionar participantes" +addParticipants = "Adicionar participantes" +addParticipantsError = "Não foi possível adicionar participantes" +backToList = "Voltar às sessões" +deleteConfirm = "Tem a certeza? Esta ação não pode ser anulada." +deleteError = "Não foi possível eliminar a sessão" +deleted = "Sessão eliminada" +deleteSession = "Eliminar sessão" +dueDate = "Data limite" +finalizeError = "Não foi possível finalizar a sessão" +loadPdfError = "Não foi possível carregar o PDF assinado" +loadSignedPdf = "Carregar o PDF assinado nos ficheiros ativos" +messageLabel = "Mensagem" +noAdditionalInfo = "Sem informações adicionais" +owner = "Proprietário" +participantRemoved = "Participante removido" +participants = "Participantes" +participantsAdded = "Participantes adicionados com sucesso" +removeParticipant = "Remover" +removeParticipantError = "Não foi possível remover o participante" +selectUsers = "Selecionar utilizadores..." +sessionInfo = "Informações da sessão" +workbenchTitle = "Gestão da sessão" + +[certSign.collab.signRequest] +addedToFiles = "Documento adicionado aos ficheiros ativos" +addSignature = "Adicionar a sua assinatura" +addToFiles = "Adicionar aos ficheiros ativos" +advancedSettings = "Definições avançadas" +backToList = "Voltar aos pedidos de assinatura" +certificateChoice = "Selecione um certificado para assinar" +changeSignature = "Alterar assinatura" +clearSignature = "Limpar assinatura" +completeAndSign = "Concluir e Assinar" +createNewSignature = "Criar nova assinatura" +declineButton = "Recusar" +decline = "Recusar pedido" +deleteSelected = "Eliminar assinatura selecionada" +drawSignature = "Desenhe a sua assinatura abaixo" +dueDate = "Data limite" +fileTooLarge = "O tamanho do ficheiro deve ser inferior a 5 MB" +fontFamily = "Tipo de letra" +fontSize = "Tamanho do tipo de letra: {{size}}px" +fontSizePlaceholder = "Tamanho" +from = "De" +invalidCertFile = "Por favor, selecione um ficheiro de certificado P12 ou PFX" +invalidFileType = "Por favor, selecione um ficheiro de imagem" +location = "Localização (opcional)" +locationPlaceholder = "De onde está a assinar?" +message = "Mensagem" +noCertificate = "Por favor, selecione um ficheiro de certificado" +noSignatures = "Coloque pelo menos uma assinatura no PDF" +p12File = "Ficheiro de Certificado P12/PFX" +password = "Palavra-passe do certificado" +passwordPlaceholder = "Introduza a palavra-passe..." +penColor = "Cor da caneta" +penSize = "Tamanho da caneta: {{size}}px" +placementActive = "Clique no PDF para colocar" +placeSignatureButton = "Colocar assinatura no PDF" +reason = "Motivo (opcional)" +reasonPlaceholder = "Porque está a assinar?" +removeImage = "Remover imagem" +removeCertFile = "Remover ficheiro" +savedSignatures = "Assinaturas guardadas" +selectFile = "Selecionar ficheiro de imagem" +selectSignatureTitle = "Selecionar ou Criar Assinatura" +signButton = "Assinar documento" +signatureInfo = "Estas definições são configuradas pelo proprietário do documento" +signaturePlaced = "Assinatura colocada na página" +signatureSettings = "Definições da assinatura" +signatureText = "Texto da assinatura" +signatureTextPlaceholder = "Introduza o seu nome..." +signatureTypeLabel = "Tipo de assinatura" +signingTitle = "Assinatura" +textColor = "Cor do texto" +typeSignature = "Escreva o seu nome para criar uma assinatura" +uploadCert = "Certificado personalizado" +uploadCertDesc = "Utilize o seu próprio certificado P12/PFX" +uploadSignature = "Carregar a imagem da sua assinatura" +usePersonalCert = "Certificado pessoal" +usePersonalCertDesc = "Gerado automaticamente para a sua conta" +useServerCert = "Certificado da organização" +useServerCertDesc = "Certificado partilhado da organização" +workbenchTitle = "Pedido de assinatura" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Escolher cor do traço" +continue = "Continuar" + +[certSign.collab.signRequest.certModal] +description = "Colocou {{count}} assinatura(s). Escolha o seu certificado para concluir a assinatura." +sign = "Assinar documento" +certValidating = "A validar certificado..." +certValidUntil = "Certificado válido até {{date}}" +certInvalid = "Certificado inválido: {{error}}" +certInvalidFallback = "Certificado inválido" +certNetworkError = "Não foi possível validar o certificado" +title = "Configurar certificado" + +[certSign.collab.signRequest.image] +hint = "Carregue uma imagem PNG ou JPG da sua assinatura" + +[certSign.collab.signRequest.mode] +move = "Mover assinatura" +place = "Colocar assinatura" +title = "Modo de assinar ou mover" + +[certSign.collab.signRequest.modeTabs] +draw = "Desenhar" +image = "Carregar" +text = "Escrever" + +[certSign.collab.signRequest.placeSignature] +message = "Clique no PDF para colocar a sua assinatura" +title = "Colocar assinatura" + +[certSign.collab.signRequest.preview] +imageAlt = "Assinatura selecionada" +missing = "Sem pré-visualização" +textFallback = "Assinatura" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Assinatura desenhada" +defaultImageLabel = "Assinatura carregada" +defaultLabel = "Assinatura" +defaultTextLabel = "Assinatura escrita" +delete = "Eliminar assinatura" +none = "Nenhuma assinatura guardada" + +[certSign.collab.signRequest.signatureType] +draw = "Desenhar" +type = "Escrever" +upload = "Carregar" + +[certSign.collab.signRequest.steps] +back = "Voltar" +cancelPlacement = "Cancelar colocação" +certificate = "Certificado" +clickMultipleTimes = "Clique várias vezes no PDF para colocar assinaturas. Arraste qualquer assinatura para a mover ou redimensionar." +clickToPlace = "Clique no PDF onde pretende que a sua assinatura apareça." +continue = "Continuar para seleção de certificado" +continueToPlacement = "Continuar para colocação" +continueToReview = "Continuar para revisão" +createSignature = "Criar assinatura" +invisible = "Invisível" +location = "Localização:" +multipleSignatures = "{{count}} assinaturas serão aplicadas ao PDF" +oneSignature = "1 assinatura será aplicada ao PDF" +placeOnPdf = "Colocar no PDF" +reason = "Motivo:" +reviewTitle = "Rever antes de assinar" +signaturePlaced = "Assinatura colocada na página {{page}}. Pode ajustar a posição clicando novamente ou continuar para a revisão." +visible = "Visível" +visibility = "Visibilidade:" +yourSignatures = "As suas assinaturas ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Cor" +fontLabel = "Tipo de letra" +fontSizeLabel = "Tamanho" +fontSizePlaceholder = "16" +label = "Texto da assinatura" +modalHint = "Introduza o seu nome e clique em Continuar para o colocar no PDF." +placeholder = "Introduza o seu nome..." + +[certSign.collab.participant] +certValidating = "A validar certificado..." +certValid = "✓ Certificado válido" +certValidUntil = " até {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Certificado inválido" +certNetworkError = "Não foi possível validar o certificado" + +[certSign.collab.addParticipants] +add = "Adicionar {{count}} participante(s)" +back = "Voltar" +configureSignatures = "Configurar definições de assinatura" +continue = "Continuar para as definições de assinatura" +reasonHelp = "Pré-definir um motivo de assinatura para estes participantes (opcional, podem alterar ao assinar)" +reasonPlaceholder = "ex.: Aprovação, Revisão..." +selectUsers = "Selecionar utilizadores" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Incluir página de resumo de assinaturas" +includeSummaryPageHelp = "Será adicionada no fim uma página de resumo com todos os metadados das assinaturas. As caixas de assinatura de certificado digital nas páginas individuais serão suprimidas (assinaturas manuscritas não são afetadas)." + +[certSign.collab.sessionList] +active = "Ativo" +finalized = "Finalizado" + +[certSign.collab.signatureSettings] +description = "Configurar como as assinaturas irão aparecer para todos os participantes" +title = "Aparência da assinatura" + +[certSign.collab.userSelector] +inviteUsers = "Adicionar utilizadores" +loadError = "Não foi possível carregar utilizadores" +noTeam = "Sem equipa" +noUsers = "Nenhum outro utilizador encontrado." +placeholder = "Selecionar utilizadores..." + +[certSign.mobile] +panelActions = "Ações" +panelDocument = "Documento" +panelPeople = "Pessoas" + +[certSign.sessions] +deleted = "Sessão eliminada" +fetchFailed = "Não foi possível carregar os detalhes da sessão" +finalized = "Sessão finalizada" +loaded = "PDF assinado carregado" +pdfNotReady = "PDF não pronto" +pdfNotReadyDesc = "O PDF assinado está a ser gerado. Tente novamente dentro de momentos." + +[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" + [changeCreds] changePassword = "Está a usar credenciais de login padrão. Por favor insira uma nova palavra-passe" changeUsername = "Atualize o seu nome de utilizador. Será terminada a sua sessão após a atualização." @@ -3242,6 +3531,46 @@ totalSelected = "Total selecionado" unsupported = "Não suportado" unzip = "Descompactar" uploadError = "Falha ao carregar alguns ficheiros." +copyCreated = "Cópia guardada neste dispositivo." +copyFailed = "Não foi possível criar uma cópia." +leaveShare = "Remover da minha lista" +leaveShareFailed = "Não foi possível remover o ficheiro partilhado." +leaveShareSuccess = "Removido da sua lista de partilhas." +removeBoth = "Remover de ambos" +removeFilePrompt = "Este ficheiro está guardado neste dispositivo e no seu servidor. De onde pretende removê-lo?" +removeFileTitle = "Remover ficheiro" +removeLocalOnly = "Apenas deste dispositivo" +removeServerFailed = "Não foi possível remover o ficheiro do servidor." +removeServerOnly = "Apenas do servidor" +removeServerOnlyPrompt = "Este ficheiro está armazenado apenas no seu servidor. Pretende removê-lo do servidor?" +removeServerSuccess = "Removido do servidor." +removeSharedPrompt = "Este ficheiro foi partilhado consigo. Pode removê-lo deste dispositivo ou da sua lista de partilhas." +removeSharedServerOnlyBlockedPrompt = "Este ficheiro foi partilhado consigo e está armazenado apenas no servidor." +removeSharedServerOnlyPrompt = "Este ficheiro foi partilhado consigo e está armazenado apenas no servidor. Removê-lo da sua lista?" +changesNotUploaded = "Alterações não carregadas" +cloudFile = "Ficheiro na nuvem" +filterAll = "Todos" +filterLocal = "Local" +filterSharedByMe = "Partilhado por mim" +filterSharedWithMe = "Partilhado comigo" +lastSynced = "Última sincronização" +localOnly = "Apenas local" +makeCopy = "Criar uma cópia" +owner = "Proprietário" +ownerUnknown = "Desconhecido" +share = "Partilhar" +shareSelected = "Partilhar selecionados" +sharedByYou = "Partilhado por si" +sharedEditNoticeBody = "Não tem permissões de edição para a versão no servidor deste ficheiro. Quaisquer edições serão guardadas como uma cópia local." +sharedEditNoticeConfirm = "Percebi" +sharedEditNoticeTitle = "Cópia no servidor só de leitura" +sharedWithYou = "Partilhado consigo" +sharing = "Partilha" +storageState = "Armazenamento" +synced = "Sincronizado" +updateOnServer = "Atualizar no Servidor" +uploadSelected = "Carregar selecionados" +uploadToServer = "Carregar para o Servidor" [files] addFiles = "Adicionar ficheiros" @@ -3367,6 +3696,77 @@ title = "Sobre o Aplanamento de PDFs" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Sobre a Assinatura em Grupo" + +[groupSigning.tooltip.finalization] +bullet1 = "Todas as assinaturas são aplicadas pela ordem de participantes que especificou" +bullet2 = "Pode finalizar com assinaturas parciais, se necessário" +bullet3 = "Depois de finalizada, a sessão não pode ser modificada" +description = "Quando todos os participantes assinarem (ou optar por finalizar antecipadamente), pode gerar o PDF final assinado." +title = "Processo de finalização" + +[groupSigning.tooltip.roles] +bullet1 = "Proprietário (você): cria a sessão, configura predefinições de assinatura, finaliza o documento" +bullet2 = "Participantes: criam a sua assinatura, escolhem o certificado, colocam no PDF" +bullet3 = "Os participantes não podem modificar as definições de visibilidade, motivo ou localização da assinatura" +description = "Controla as definições de aparência da assinatura para todos os participantes." +title = "Papéis dos participantes" + +[groupSigning.tooltip.sequential] +bullet1 = "O primeiro participante tem de assinar antes de o segundo poder aceder ao documento" +bullet2 = "Garante a ordem de assinatura adequada para conformidade legal" +bullet3 = "Pode reordenar participantes arrastando-os na lista" +description = "Os participantes assinam os documentos pela ordem que especificar. Cada signatário recebe uma notificação quando é a sua vez." +title = "Assinatura sequencial" + +[groupSigning.steps] +back = "Voltar" +completed = "Concluído" +current = "Atual" +stepLabel = "Passo {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Continuar para revisão" +invisible = "As assinaturas serão invisíveis (apenas metadados)" +locationLabel = "Localização:" +preview = "Pré-visualização" +reasonLabel = "Motivo:" +title = "Configurar definições de assinatura" +visible = "As assinaturas serão visíveis na página {{page}}" + +[groupSigning.steps.review] +document = "Documento" +dueDate = "Data limite (opcional)" +dueDatePlaceholder = "Selecionar data limite..." +invisible = "Invisível (apenas metadados)" +location = "Localização:" +logo = "Logótipo:" +logoHidden = "Sem logótipo" +logoShown = "Logótipo do Stirling PDF mostrado" +participants = "Participantes" +reason = "Motivo:" +send = "Enviar pedidos de assinatura" +signatureSettings = "Definições da assinatura" +title = "Rever detalhes da sessão" +titleShort = "Rever e Enviar" +visibility = "Visibilidade:" +visible = "Visível na página {{page}}" +participantCount = "{{count}} participante(s) assinará(ão) por ordem" + +[groupSigning.steps.selectDocument] +continue = "Continuar para seleção de participantes" +noFile = "Selecione um único ficheiro PDF dos seus ficheiros ativos para criar uma sessão de assinatura." +selectedFile = "Documento selecionado" +title = "Selecionar documento" + +[groupSigning.steps.selectParticipants] +continue = "Continuar para as definições de assinatura" +count = "{{count}} participante(s) selecionado(s)" +label = "Selecionar participantes" +placeholder = "Escolher participantes para assinar..." +title = "Escolher participantes" + [getPdfInfo] downloadJson = "Transferir JSON" downloads = "Transferências" @@ -4460,7 +4860,10 @@ zoomOut = "Reduzir" [viewer] cannotPreviewFile = "Não é possível pré-visualizar o ficheiro" +disableColorFilter = "Desativar filtro de cor" dualPageView = "Vista de duas páginas" +enableDarkFilter = "Ativar filtro escuro" +enableSepiaFilter = "Ativar filtro sépia" firstPage = "Primeira página" lastPage = "Última página" nextPage = "Página seguinte" @@ -4470,6 +4873,22 @@ singlePageView = "Vista de página única" unknownFile = "Ficheiro desconhecido" zoomIn = "Ampliar" zoomOut = "Reduzir" +resetZoom = "Repor zoom" + +[viewer.nonPdf] +fileTypeBadge = "Ficheiro {{type}}" +convertToPdf = "Converter para PDF" +loading = "A carregar..." +emptyFile = "Ficheiro vazio" +csvStats = "{{rows}} linhas · {{columns}} colunas · {{size}}" +sortedBy = "Ordenado por: {{column}}" +columnDefault = "Coluna {{index}}" +htmlPreviewWarning = "Pré-visualização HTML — recursos externos podem não ser carregados · {{size}}" +htmlPreview = "Pré-visualização HTML" +invalidJson = "JSON inválido — a mostrar conteúdo em bruto" +textStats = "{{lines}} linhas · {{size}}" +lineNumbers = "Números de linha" +renderMarkdown = "Renderizar Markdown" [viewer.attachments] title = "Anexos" @@ -4531,6 +4950,7 @@ toggleAttachments = "Alternar anexos" toggleTheme = "Alternar tema" language = "Idioma" toggleAnnotations = "Alternar visibilidade das anotações" +toggleLayers = "Alternar camadas" search = "Pesquisar PDF" panMode = "Modo de deslocamento" applyRedactionsFirst = "Aplique as ocultações primeiro" @@ -5407,20 +5827,72 @@ title = "Imprimir Ficheiro" 2 = "Introduza Nome da Impressora" [quickAccess] +access = "Acesso" +accessAddPerson = "Adicionar outra pessoa" +accessBack = "Voltar" +accessCopyLink = "Copiar ligação" +accessEmail = "Endereço de e-mail" +accessEmailPlaceholder = "nome@empresa.com" +accessFileLabel = "Ficheiro" +accessGeneral = "Acesso geral" +accessInviteTitle = "Convidar pessoas" +accessOwner = "Proprietário" +accessPanel = "Acesso ao documento" +accessPeople = "Pessoas com acesso" +accessRemove = "Remover" +accessRestricted = "Restrito" +accessRestrictedHint = "Apenas as pessoas com acesso podem abrir" +accessRole = "Função" +accessRoleCommenter = "Comentador" +accessRoleEditor = "Editor" +accessRoleViewer = "Leitor" +accessSelectedFile = "Ficheiro selecionado" +accessSendInvite = "Enviar convite" +accessTitle = "Acesso ao documento" +accessYou = "Eu" account = "Conta" +activeSessions = "Sessões ativas" +activeTab = "Ativas" activity = "Registo" adminSettings = "Ajustes admin" +allSessions = "Todas as sessões" allTools = "All Tools" automate = "Auto" +back = "Voltar" +certSign = "Assinar com certificado" +completedSessions = "Sessões concluídas" +completedTab = "Concluídas" config = "Config." +createNew = "Criar novo pedido" +createSession = "Criar pedido de assinatura" +dueDate = "Data limite (opcional)" files = "Fich." help = "Ajuda" +noActiveSessions = "Sem pedidos de assinatura pendentes ou sessões ativas" +noCompletedSessions = "Sem sessões concluídas" +noFile = "Nenhum ficheiro selecionado" read = "Ler" reader = "Leitor" +refresh = "Atualizar" +requestSignatures = "Pedir assinaturas" +selectSingleFileToRequest = "Selecione um único ficheiro PDF para pedir assinaturas" +selectedFile = "Ficheiro selecionado" +selectUsers = "Selecionar utilizadores para assinar" +selectUsersPlaceholder = "Escolher participantes..." +sendingRequest = "A enviar..." settings = "Ajustes" showMeAround = "Faz-me uma visita guiada" sign = "Assinar" +signatureRequests = "Pedidos de assinatura" +signYourself = "Assinar você mesmo" +newRequest = "Novo pedido" tours = "Visitas guiadas" +wetSign = "Adicionar assinatura" +filterMine = "Meus" +filterOverdue = "Em atraso" +filterSigned = "Assinado" +filterDeclined = "Recusado" +searchDocuments = "Procurar documentos…" [quickAccess.helpMenu] adminTour = "Visita guiada de administração" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "O seu servidor Stirling-PDF está offline e \"{{endpo expired = "A sua sessão expirou. Por favor atualize a página e tente novamente." refreshPage = "Atualizar Página" +[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" + +[signatureSettings.tooltip] +header = "Definições de aparência da assinatura" + +[signatureSettings.tooltip.location] +bullet1 = "Exemplos: \"Nova Iorque, EUA\", \"Escritório de Londres\", \"Remoto\"" +bullet2 = "Não é o mesmo que a posição na página" +bullet3 = "Pode ser exigido por certas jurisdições legais" +description = "Localização geográfica opcional onde a assinatura foi aplicada. Armazenada nos metadados do certificado." +title = "Localização da assinatura" + +[signatureSettings.tooltip.logo] +bullet1 = "Apresentado junto à assinatura e texto" +bullet2 = "Suporta formatos PNG, JPG" +bullet3 = "Melhora a aparência profissional" +description = "Adicione um logótipo da empresa às assinaturas visíveis para reforçar a marca e autenticidade." +title = "Logótipo da empresa" + +[signatureSettings.tooltip.reason] +bullet1 = "Exemplos: \"Aprovação\", \"Acordo de Contrato\", \"Revisão Concluída\"" +bullet2 = "Visível nas propriedades da assinatura do PDF" +bullet3 = "Útil para rastreabilidade e conformidade" +description = "Texto opcional que explica porque o documento está a ser assinado. Armazenado nos metadados do certificado." +title = "Motivo da assinatura" + +[signatureSettings.tooltip.visibility] +bullet1 = "Visível: A assinatura aparece no PDF com aparência personalizada" +bullet2 = "Invisível: Certificado incorporado sem marca visual" +bullet3 = "Assinaturas invisíveis continuam a fornecer validação criptográfica" +description = "Controla se a assinatura é visível no documento ou incorporada de forma invisível." +title = "Visibilidade da assinatura" + [settings.configuration] advanced = "Avançado" database = "Base de dados" endpoints = "Endpoints" features = "Funcionalidades" +storageSharing = "Armazenamento de Ficheiros e Partilha" systemSettings = "Definições do sistema" title = "Configuração" @@ -6332,10 +6868,13 @@ title = "Iniciar sessão no Stirling" [setup.selfhosted] link = "ou ligue-se a uma conta autoalojada" subtitle = "Introduza as credenciais do seu servidor" +changeServerLocked = "A sua organização restringiu esta aplicação a um servidor específico" switchToLocal = "Usar ferramentas locais em vez disso" title = "Iniciar sessão no servidor" [setup.selfhosted.unreachable] +changeServer = "Ligar a um servidor diferente" +changeServerLocked = "A sua organização restringiu esta aplicação a um servidor específico" continueOffline = "Usar ferramentas locais em vez disso" message = "Não foi possível alcançar {{url}}. Verifique que o servidor está em execução e acessível." retry = "Tentar novamente" @@ -6529,6 +7068,15 @@ saved = "Guardadas" text = "Texto" title = "Tipo de Assinatura" +[signRequest] +declined = "Pedido de assinatura recusado" +fetchFailed = "Não foi possível carregar o pedido de assinatura" +signed = "Documento assinado com sucesso" + +[signSession] +createFailed = "Não foi possível criar o pedido de assinatura" +created = "Pedido de assinatura enviado" + [signup] accountCreatedSuccessfully = "Conta criada com sucesso! Já pode iniciar sessão." alreadyHaveAccount = "Já tem uma conta? Inicie sessão" @@ -6807,6 +7355,106 @@ title = "Dividir PDF por Capítulos" [splitPdfByChapters] tags = "dividir,capítulos,marcadores,organizar" +[storageShare] +accessed = "Acedido" +accessDenied = "Não tem acesso a este ficheiro partilhado. Peça ao proprietário que o partilhe consigo." +accessFailed = "Não foi possível carregar a atividade." +accessDeniedBody = "Não tem acesso a este ficheiro. Peça ao proprietário que o partilhe consigo." +accessDeniedTitle = "Sem acesso" +accessLimitedCommenter = "O acesso para comentários chegará em breve. Peça acesso de editor se precisar de transferir." +accessLimitedTitle = "Acesso limitado" +accessLimitedViewer = "Esta ligação é apenas para visualização. Peça acesso de editor se precisar de transferir." +createdAt = "Criado" +download = "Transferir" +downloadFailed = "Não foi possível transferir este ficheiro." +expiredBody = "Esta ligação de partilha é inválida ou expirou." +expiredTitle = "Ligação expirada" +goToLogin = "Ir para início de sessão" +loadFailed = "Não foi possível abrir o ficheiro partilhado." +loading = "A carregar ligação de partilha..." +loginPrompt = "Inicie sessão para aceder a este ficheiro partilhado." +loginRequired = "É necessário iniciar sessão" +openInApp = "Abrir no Stirling PDF" +ownerLabel = "Proprietário" +ownerUnknown = "Desconhecido" +requiresLogin = "Este ficheiro partilhado requer início de sessão." +roleCommenter = "Comentador" +roleEditor = "Editor" +roleViewer = "Leitor" +shareHeading = "Ficheiro partilhado" +titleDefault = "Ficheiro partilhado" +tryAgain = "Por favor, tente novamente mais tarde." +addUser = "Adicionar" +commenterHint = "Os comentários chegarão em breve." +copied = "Ligação copiada para a área de transferência" +copy = "Copiar" +copyFailed = "Não foi possível copiar" +description = "Crie uma ligação de partilha para este ficheiro. Utilizadores com sessão iniciada e com a ligação podem aceder-lhe." +downloadsCount = "Transferências: {{count}}" +emailWarningBody = "Isto parece um endereço de e-mail. Se esta pessoa não for já utilizadora do Stirling PDF, não poderá aceder ao ficheiro." +emailWarningConfirm = "Partilhar mesmo assim" +emailWarningTitle = "Endereço de e-mail" +errorTitle = "Falha na partilha" +failure = "Não foi possível gerar uma ligação de partilha. Por favor, tente novamente." +fileLabel = "Ficheiro" +generate = "Gerar ligação" +generated = "Ligação de partilha gerada" +hideActivity = "Ocultar atividade" +invalidUsername = "Introduza um nome de utilizador ou endereço de e-mail válido." +lastAccessed = "Último acesso" +linkAccessTitle = "Acesso da ligação de partilha" +linkLabel = "Ligação de partilha" +linksDisabled = "As ligações de partilha estão desativadas." +linksDisabledBody = "As ligações de partilha estão desativadas pelas definições do seu servidor." +manage = "Gerir partilha" +manageDescription = "Crie e gere ligações para partilhar este ficheiro." +manageLoadFailed = "Não foi possível carregar as ligações de partilha." +manageTitle = "Gerir partilha" +noActivity = "Ainda sem atividade." +noLinks = "Ainda não existem ligações de partilha ativas." +noSharedUsers = "Ainda não há utilizadores com acesso." +removeLink = "Remover ligação" +removeUser = "Remover" +revokeFailed = "Não foi possível remover a ligação de partilha." +revoked = "Ligação de partilha removida" +roleLabel = "Função" +sharingDisabled = "A partilha está desativada." +sharingDisabledBody = "A partilha foi desativada pelas definições do seu servidor." +sharedUsersTitle = "Utilizadores com acesso partilhado" +title = "Partilhar ficheiro" +unknownUser = "Utilizador desconhecido" +userAddFailed = "Não foi possível partilhar com esse utilizador." +userAdded = "Utilizador adicionado à lista de partilha." +usernameLabel = "Nome de utilizador ou e-mail" +usernamePlaceholder = "Introduza um nome de utilizador ou e-mail" +userRemoveFailed = "Não foi possível remover esse utilizador." +userRemoved = "Utilizador removido da lista de partilha." +viewActivity = "Ver atividade" +viewed = "Visto" +viewsCount = "Visualizações: {{count}}" +downloaded = "Transferido" +bulkDescription = "Crie uma única ligação para partilhar todos os ficheiros selecionados com utilizadores com sessão iniciada." +bulkTitle = "Partilhar ficheiros selecionados" +copyLink = "Copiar ligação de partilha" +fileCount = "{{count}} ficheiros selecionados" +ownerOnly = "Apenas o proprietário pode gerir a partilha." +selectSingleFile = "Selecione um único ficheiro para gerir a partilha." + +[storageUpload] +description = "Isto carrega o ficheiro atual para o armazenamento do servidor para o seu próprio acesso." +errorTitle = "Falha no carregamento" +failure = "O carregamento falhou. Verifique o seu início de sessão e as definições de armazenamento." +fileLabel = "Ficheiro" +hint = "As ligações públicas e os modos de acesso são controlados pelas definições do seu servidor." +success = "Carregado para o servidor" +title = "Carregar para o servidor" +updateButton = "Atualizar no servidor" +uploadButton = "Carregar para o servidor" +bulkDescription = "Isto carrega os ficheiros selecionados para o armazenamento do seu servidor." +bulkTitle = "Carregar ficheiros selecionados" +fileCount = "{{count}} ficheiros selecionados" +more = " +{{count}} mais" + [storage] approximateSize = "Tamanho aproximado" fileTooLarge = "Ficheiro demasiado grande. O tamanho máximo por ficheiro é" @@ -7153,6 +7801,30 @@ title = "Ver/Editar PDF" [warning] tooltipTitle = "Aviso" +[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" + [watermark] completed = "Marca de água adicionada" desc = "Adicionar marcas de água de texto ou imagem a ficheiros PDF" @@ -7333,6 +8005,7 @@ activeSession = "Sessão ativa" addMembers = "Adicionar membros" admin = "Administrador" confirmDelete = "Tem a certeza de que pretende eliminar este utilizador? Esta ação não pode ser anulada." +confirmUnlock = "Tem a certeza de que pretende desbloquear esta conta de utilizador?" deleteUser = "Eliminar utilizador" deleteUserError = "Falha ao eliminar utilizador" deleteUserSuccess = "Utilizador eliminado com sucesso" @@ -7341,6 +8014,8 @@ disable = "Desativar" disabled = "Desativado" editRole = "Editar função" enable = "Ativar" +locked = "bloqueado" +lockedBadge = "Bloqueado" loading = "A carregar pessoas..." loginRequired = "Ative primeiro o modo de login" member = "Membro" @@ -7350,6 +8025,9 @@ searchMembers = "Procurar membros..." status = "Estado" team = "Equipa" title = "Pessoas" +unlockAccount = "Desbloquear conta" +unlockUserError = "Falha ao desbloquear a conta de utilizador" +unlockUserSuccess = "Conta de utilizador desbloqueada com sucesso" user = "Utilizador" [workspace.people.actions] diff --git a/frontend/public/locales/ro-RO/translation.toml b/frontend/public/locales/ro-RO/translation.toml index 8d7f5c68f8..f42beadf85 100644 --- a/frontend/public/locales/ro-RO/translation.toml +++ b/frontend/public/locales/ro-RO/translation.toml @@ -8,6 +8,7 @@ black = "Negru" blue = "Albastru" bored = "Plictisit aÈ™teptând?" cancel = "Anulare" +confirm = "ConfirmaÈ›i" changedCredsMessage = "CredenÈ›ialele au fost schimbate!" chooseFile = "AlegeÈ›i fiÈ™ier" close = "ÃŽnchide" @@ -146,6 +147,7 @@ insufficientCredits = "Credite insuficiente. Necesare: {{requiredCredits}}, Disp loadingCredits = "Se verifică creditele..." loadingProStatus = "Se verifică starea abonamentului..." noticeTopUpOrPlan = "Credite insuficiente, reîncărcaÈ›i sau treceÈ›i la un plan" +accessInvite = "InvitaÈ›i" [account] accountSettings = "Setări Cont" @@ -1427,6 +1429,34 @@ title = "Procesare" description = "Timpul maxim de aÈ™teptare pentru o sarcină de procesare înainte de raportarea unei erori." label = "Timeout procesare (secunde)" +[admin.settings.storage] +description = "ControlaÈ›i opÈ›iunile de stocare pe server È™i de partajare." +title = "Stocare fiÈ™iere È™i partajare" + +[admin.settings.storage.enabled] +description = "Permite utilizatorilor să stocheze fiÈ™iere pe server." +label = "ActivaÈ›i stocarea de fiÈ™iere pe server" + +[admin.settings.storage.sharing.email] +description = "Permite partajarea cu adrese de e-mail." +label = "ActivaÈ›i partajarea prin e-mail" +mailLink = "ConfiguraÈ›i setările de e-mail" +mailNote = "Necesită configurare de e-mail. " + +[admin.settings.storage.sharing.enabled] +description = "Permite utilizatorilor să partajeze fiÈ™ierele stocate." +label = "ActivaÈ›i partajarea" + +[admin.settings.storage.sharing.links] +description = "Permite partajarea prin linkuri pentru utilizatori autentificaÈ›i." +frontendUrlLink = "ConfiguraÈ›i în setările sistemului" +frontendUrlNote = "Necesită un Frontend URL. " +label = "ActivaÈ›i linkurile de partajare" + +[admin.settings.storage.signing.enabled] +description = "Permite utilizatorilor să creeze sesiuni de semnare a documentelor cu mai mulÈ›i participanÈ›i. Necesită activarea stocării fiÈ™ierelor pe server." +label = "ActivaÈ›i semnarea în grup (Alpha)" + [admin.settings.unsavedChanges] cancel = "Continuă editarea" discard = "Renunță la modificări" @@ -2059,7 +2089,19 @@ numbers = "Numere/intervale: 5, 10-20" progressions = "Progresii: 3n, 4n+1" [certSign] +allSigned = "ToÈ›i participanÈ›ii au semnat. Gata de finalizare." +awaitingSignatures = "ÃŽn aÈ™teptarea semnăturilor" +signatureProgress = "{{signedCount}}/{{totalCount}} semnături" chooseCertificate = "AlegeÈ›i fiÈ™ierul certificatului" +declined = "Refuzat" +fetchFailed = "Nu s-au putut încărca datele de semnare" +finalized = "Finalizat" +notified = "ÃŽn aÈ™teptare" +partialNote = "PuteÈ›i finaliza mai devreme cu semnăturile curente. ParticipanÈ›ii fără semnătură vor fi excluÈ™i." +pending = "ÃŽn aÈ™teptare" +readyToFinalize = "Gata de finalizare" +signed = "Semnat" +viewed = "Vizualizat" chooseJksFile = "AlegeÈ›i fiÈ™ierul JKS" chooseP12File = "AlegeÈ›i fiÈ™ierul PKCS12" choosePfxFile = "AlegeÈ›i fiÈ™ierul PFX" @@ -2082,6 +2124,7 @@ title = "Semnare certificat" invisible = "Invizibil" stepTitle = "Aspectul semnăturii" visible = "Vizibil" +visibility = "Vizibilitate" [certSign.appearance.options] title = "Detalii semnătură" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "FinalizaÈ›i È™i încărcaÈ›i PDF-ul semnat" +early = "FinalizaÈ›i cu semnăturile curente" + +[certSign.collab.sessionDetail] +addButton = "AdăugaÈ›i participanÈ›i" +addParticipants = "AdăugaÈ›i participanÈ›i" +addParticipantsError = "Nu s-au putut adăuga participanÈ›ii" +backToList = "ÃŽnapoi la sesiuni" +deleteConfirm = "SunteÈ›i sigur(ă)? Aceasta nu poate fi anulată." +deleteError = "Nu s-a putut È™terge sesiunea" +deleted = "Sesiune È™tearsă" +deleteSession = "ȘtergeÈ›i sesiunea" +dueDate = "Data limită" +finalizeError = "Nu s-a putut finaliza sesiunea" +loadPdfError = "Nu s-a putut încărca PDF-ul semnat" +loadSignedPdf = "ÃŽncărcaÈ›i PDF-ul semnat în fiÈ™ierele active" +messageLabel = "Mesaj" +noAdditionalInfo = "Nicio informaÈ›ie suplimentară" +owner = "Proprietar" +participantRemoved = "Participant eliminat" +participants = "ParticipanÈ›i" +participantsAdded = "ParticipanÈ›i adăugaÈ›i cu succes" +removeParticipant = "EliminaÈ›i" +removeParticipantError = "Nu s-a putut elimina participantul" +selectUsers = "SelectaÈ›i utilizatori..." +sessionInfo = "InformaÈ›ii sesiune" +workbenchTitle = "Administrare sesiuni" + +[certSign.collab.signRequest] +addedToFiles = "Document adăugat la fiÈ™ierele active" +addSignature = "AdăugaÈ›i semnătura dvs." +addToFiles = "AdăugaÈ›i la fiÈ™ierele active" +advancedSettings = "Setări avansate" +backToList = "ÃŽnapoi la cereri de semnare" +certificateChoice = "SelectaÈ›i un certificat pentru semnare" +changeSignature = "SchimbaÈ›i semnătura" +clearSignature = "ȘtergeÈ›i semnătura" +completeAndSign = "FinalizaÈ›i È™i semnaÈ›i" +createNewSignature = "CreaÈ›i semnătură nouă" +declineButton = "RefuzaÈ›i" +decline = "RefuzaÈ›i cererea" +deleteSelected = "ȘtergeÈ›i semnătura selectată" +drawSignature = "DesenaÈ›i-vă semnătura mai jos" +dueDate = "Data limită" +fileTooLarge = "Dimensiunea fiÈ™ierului trebuie să fie mai mică de 5MB" +fontFamily = "Familie de fonturi" +fontSize = "Mărimea fontului: {{size}}px" +fontSizePlaceholder = "Mărime" +from = "De la" +invalidCertFile = "SelectaÈ›i un fiÈ™ier certificat P12 sau PFX" +invalidFileType = "SelectaÈ›i un fiÈ™ier imagine" +location = "LocaÈ›ie (OpÈ›ional)" +locationPlaceholder = "De unde semnaÈ›i?" +message = "Mesaj" +noCertificate = "SelectaÈ›i un fiÈ™ier certificat" +noSignatures = "PlasaÈ›i cel puÈ›in o semnătură pe PDF" +p12File = "FiÈ™ier certificat P12/PFX" +password = "Parola certificatului" +passwordPlaceholder = "IntroduceÈ›i parola..." +penColor = "Culoarea stiloului" +penSize = "Grosimea stiloului: {{size}}px" +placementActive = "FaceÈ›i clic pe PDF pentru a plasa" +placeSignatureButton = "PlasaÈ›i semnătura pe PDF" +reason = "Motiv (OpÈ›ional)" +reasonPlaceholder = "De ce semnaÈ›i?" +removeImage = "EliminaÈ›i imaginea" +removeCertFile = "EliminaÈ›i fiÈ™ierul" +savedSignatures = "Semnături salvate" +selectFile = "SelectaÈ›i fiÈ™ier imagine" +selectSignatureTitle = "SelectaÈ›i sau creaÈ›i o semnătură" +signButton = "SemnaÈ›i documentul" +signatureInfo = "Aceste setări sunt configurate de proprietarul documentului" +signaturePlaced = "Semnătură plasată pe pagină" +signatureSettings = "Setări semnătură" +signatureText = "Textul semnăturii" +signatureTextPlaceholder = "IntroduceÈ›i numele dvs...." +signatureTypeLabel = "Tipul semnăturii" +signingTitle = "Semnare" +textColor = "Culoarea textului" +typeSignature = "TastaÈ›i-vă numele pentru a crea o semnătură" +uploadCert = "Certificat personalizat" +uploadCertDesc = "UtilizaÈ›i propriul certificat P12/PFX" +uploadSignature = "ÃŽncărcaÈ›i imaginea semnăturii" +usePersonalCert = "Certificat personal" +usePersonalCertDesc = "Generat automat pentru contul dvs." +useServerCert = "Certificat al organizaÈ›iei" +useServerCertDesc = "Certificat partajat al organizaÈ›iei" +workbenchTitle = "Cerere de semnare" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "AlegeÈ›i culoarea traseului" +continue = "ContinuaÈ›i" + +[certSign.collab.signRequest.certModal] +description = "AÈ›i plasat {{count}} semnătură(i). AlegeÈ›i certificatul pentru a finaliza semnarea." +sign = "SemnaÈ›i documentul" +certValidating = "Se validează certificatul..." +certValidUntil = "Certificat valabil până la {{date}}" +certInvalid = "Certificat invalid: {{error}}" +certInvalidFallback = "Certificat invalid" +certNetworkError = "Nu s-a putut valida certificatul" +title = "ConfiguraÈ›i certificatul" + +[certSign.collab.signRequest.image] +hint = "ÃŽncărcaÈ›i o imagine PNG sau JPG a semnăturii dvs." + +[certSign.collab.signRequest.mode] +move = "MutaÈ›i semnătura" +place = "PlasaÈ›i semnătura" +title = "Mod semnare sau mutare" + +[certSign.collab.signRequest.modeTabs] +draw = "Desen" +image = "ÃŽncărcare" +text = "Text" + +[certSign.collab.signRequest.placeSignature] +message = "FaceÈ›i clic pe PDF pentru a plasa semnătura" +title = "PlasaÈ›i semnătura" + +[certSign.collab.signRequest.preview] +imageAlt = "Semnătura selectată" +missing = "Fără previzualizare" +textFallback = "Semnătură" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Semnătură desenată" +defaultImageLabel = "Semnătură încărcată" +defaultLabel = "Semnătură" +defaultTextLabel = "Semnătură tastată" +delete = "ȘtergeÈ›i semnătura" +none = "Nicio semnătură salvată" + +[certSign.collab.signRequest.signatureType] +draw = "Desen" +type = "Text" +upload = "ÃŽncărcare" + +[certSign.collab.signRequest.steps] +back = "ÃŽnapoi" +cancelPlacement = "AnulaÈ›i plasarea" +certificate = "Certificat" +clickMultipleTimes = "FaceÈ›i clic de mai multe ori pe PDF pentru a plasa semnături. TrageÈ›i orice semnătură pentru a o muta sau redimensiona." +clickToPlace = "FaceÈ›i clic pe PDF unde doriÈ›i să apară semnătura." +continue = "ContinuaÈ›i la selectarea certificatului" +continueToPlacement = "ContinuaÈ›i la plasare" +continueToReview = "ContinuaÈ›i la revizuire" +createSignature = "CreaÈ›i semnătură" +invisible = "Invizibil" +location = "LocaÈ›ie:" +multipleSignatures = "{{count}} semnături vor fi aplicate pe PDF" +oneSignature = "1 semnătură va fi aplicată pe PDF" +placeOnPdf = "PlasaÈ›i pe PDF" +reason = "Motiv:" +reviewTitle = "Revizuire înainte de semnare" +signaturePlaced = "Semnătură plasată pe pagina {{page}}. PuteÈ›i ajusta poziÈ›ia făcând clic din nou sau continua la revizuire." +visible = "Vizibil" +visibility = "Vizibilitate:" +yourSignatures = "Semnăturile dvs. ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Culoare" +fontLabel = "Font" +fontSizeLabel = "Mărime" +fontSizePlaceholder = "16" +label = "Textul semnăturii" +modalHint = "IntroduceÈ›i numele dvs., apoi faceÈ›i clic pe ContinuaÈ›i pentru a-l plasa pe PDF." +placeholder = "IntroduceÈ›i numele dvs...." + +[certSign.collab.participant] +certValidating = "Se validează certificatul..." +certValid = "✓ Certificat valid" +certValidUntil = " până la {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Certificat invalid" +certNetworkError = "Nu s-a putut valida certificatul" + +[certSign.collab.addParticipants] +add = "AdăugaÈ›i {{count}} participant(È›i)" +back = "ÃŽnapoi" +configureSignatures = "ConfiguraÈ›i setările semnăturii" +continue = "ContinuaÈ›i la setările semnăturii" +reasonHelp = "PredefiniÈ›i un motiv de semnare pentru aceÈ™ti participanÈ›i (opÈ›ional, îl pot modifica la semnare)" +reasonPlaceholder = "de ex. Aprobare, Revizuire..." +selectUsers = "SelectaÈ›i utilizatori" + +[certSign.collab.sessionCreation] +includeSummaryPage = "IncludeÈ›i pagina de sumar a semnăturilor" +includeSummaryPageHelp = "O pagină de sumar va fi adăugată la final cu toate metadatele semnăturilor. CăsuÈ›ele de semnătură cu certificat digital de pe paginile individuale vor fi suprimate (semnăturile olografe nu sunt afectate)." + +[certSign.collab.sessionList] +active = "Active" +finalized = "Finalizate" + +[certSign.collab.signatureSettings] +description = "ConfiguraÈ›i modul în care vor apărea semnăturile pentru toÈ›i participanÈ›ii" +title = "Aspectul semnăturii" + +[certSign.collab.userSelector] +inviteUsers = "AdăugaÈ›i utilizatori" +loadError = "Nu s-au putut încărca utilizatorii" +noTeam = "Fără echipă" +noUsers = "Nu s-au găsit alÈ›i utilizatori." +placeholder = "SelectaÈ›i utilizatori..." + +[certSign.mobile] +panelActions = "AcÈ›iuni" +panelDocument = "Document" +panelPeople = "Persoane" + +[certSign.sessions] +deleted = "Sesiune È™tearsă" +fetchFailed = "Nu s-au putut încărca detaliile sesiunii" +finalized = "Sesiune finalizată" +loaded = "PDF semnat încărcat" +pdfNotReady = "PDF-ul nu este gata" +pdfNotReadyDesc = "PDF-ul semnat este în curs de generare. ÃŽncercaÈ›i din nou peste puÈ›in timp." + +[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" + [changeCreds] changePassword = "Utilizezi credenÈ›iale de conectare implicite. Te rugăm să introduci o nouă parolă" changeUsername = "ActualizaÈ›i numele de utilizator. VeÈ›i fi deconectat(ă) după actualizare." @@ -3242,6 +3531,46 @@ totalSelected = "Total selectate" unsupported = "Nesuportat" unzip = "DezarhivaÈ›i" uploadError = "ÃŽncărcarea unor fiÈ™iere a eÈ™uat." +copyCreated = "Copia a fost salvată pe acest dispozitiv." +copyFailed = "Nu s-a putut crea o copie." +leaveShare = "EliminaÈ›i din lista mea" +leaveShareFailed = "Nu s-a putut elimina fiÈ™ierul partajat." +leaveShareSuccess = "Eliminat din lista dvs. de partajate." +removeBoth = "EliminaÈ›i din ambele" +removeFilePrompt = "Acest fiÈ™ier este salvat pe acest dispozitiv È™i pe serverul dvs. De unde doriÈ›i să îl eliminaÈ›i?" +removeFileTitle = "EliminaÈ›i fiÈ™ierul" +removeLocalOnly = "Doar de pe acest dispozitiv" +removeServerFailed = "Nu s-a putut elimina fiÈ™ierul de pe server." +removeServerOnly = "Doar de pe server" +removeServerOnlyPrompt = "Acest fiÈ™ier este stocat doar pe serverul dvs. DoriÈ›i să îl eliminaÈ›i de pe server?" +removeServerSuccess = "Eliminat de pe server." +removeSharedPrompt = "Acest fiÈ™ier este partajat cu dvs. ÃŽl puteÈ›i elimina de pe acest dispozitiv sau din lista dvs. de partajate." +removeSharedServerOnlyBlockedPrompt = "Acest fiÈ™ier este partajat cu dvs. È™i este stocat doar pe server." +removeSharedServerOnlyPrompt = "Acest fiÈ™ier este partajat cu dvs. È™i este stocat doar pe server. ÃŽl eliminaÈ›i din lista dvs.?" +changesNotUploaded = "Modificări neîncărcate" +cloudFile = "FiÈ™ier în cloud" +filterAll = "Toate" +filterLocal = "Locale" +filterSharedByMe = "Partajate de mine" +filterSharedWithMe = "Partajate cu mine" +lastSynced = "Ultima sincronizare" +localOnly = "Doar local" +makeCopy = "CreaÈ›i o copie" +owner = "Proprietar" +ownerUnknown = "Necunoscut" +share = "PartajaÈ›i" +shareSelected = "PartajaÈ›i elementele selectate" +sharedByYou = "Partajat de dvs." +sharedEditNoticeBody = "Nu aveÈ›i drepturi de editare asupra versiunii de pe server a acestui fiÈ™ier. Orice modificări pe care le faceÈ›i vor fi salvate ca o copie locală." +sharedEditNoticeConfirm = "Am înÈ›eles" +sharedEditNoticeTitle = "Copie pe server doar pentru citire" +sharedWithYou = "Partajat cu dvs." +sharing = "Partajare" +storageState = "Stocare" +synced = "Sincronizat" +updateOnServer = "ActualizaÈ›i pe server" +uploadSelected = "ÃŽncărcaÈ›i elementele selectate" +uploadToServer = "ÃŽncărcaÈ›i pe server" [files] addFiles = "AdăugaÈ›i fiÈ™iere" @@ -3367,6 +3696,77 @@ title = "Despre aplatizarea PDF-urilor" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Despre semnarea în grup" + +[groupSigning.tooltip.finalization] +bullet1 = "Toate semnăturile sunt aplicate în ordinea participanÈ›ilor specificată de dvs." +bullet2 = "PuteÈ›i finaliza cu semnături parÈ›iale, dacă este necesar" +bullet3 = "După finalizare, sesiunea nu mai poate fi modificată" +description = "După ce toÈ›i participanÈ›ii au semnat (sau alegeÈ›i să finalizaÈ›i mai devreme), puteÈ›i genera PDF-ul final semnat." +title = "Procesul de finalizare" + +[groupSigning.tooltip.roles] +bullet1 = "Proprietar (dvs.): Creează sesiunea, configurează valorile implicite ale semnăturii, finalizează documentul" +bullet2 = "ParticipanÈ›i: ÃŽÈ™i creează semnătura, aleg certificatul, plasează pe PDF" +bullet3 = "ParticipanÈ›ii nu pot modifica setările de vizibilitate, motiv sau locaÈ›ie ale semnăturii" +description = "ControlaÈ›i setările aspectului semnăturii pentru toÈ›i participanÈ›ii." +title = "Rolurile participanÈ›ilor" + +[groupSigning.tooltip.sequential] +bullet1 = "Primul participant trebuie să semneze înainte ca al doilea să poată accesa documentul" +bullet2 = "Asigură ordinea corectă de semnare pentru conformitate legală" +bullet3 = "PuteÈ›i reordona participanÈ›ii trăgându-i în listă" +description = "ParticipanÈ›ii semnează documentele în ordinea pe care o specificaÈ›i. Fiecare semnatar primeÈ™te o notificare când îi vine rândul." +title = "Semnare secvenÈ›ială" + +[groupSigning.steps] +back = "ÃŽnapoi" +completed = "Finalizat" +current = "Curent" +stepLabel = "Pasul {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "ContinuaÈ›i la revizuire" +invisible = "Semnăturile vor fi invizibile (doar metadata)" +locationLabel = "LocaÈ›ie:" +preview = "Previzualizare" +reasonLabel = "Motiv:" +title = "ConfiguraÈ›i setările semnăturii" +visible = "Semnăturile vor fi vizibile pe pagina {{page}}" + +[groupSigning.steps.review] +document = "Document" +dueDate = "Data limită (opÈ›ional)" +dueDatePlaceholder = "SelectaÈ›i data limită..." +invisible = "Invizibil (doar metadata)" +location = "LocaÈ›ie:" +logo = "Logo:" +logoHidden = "Fără logo" +logoShown = "Logo Stirling PDF afiÈ™at" +participants = "ParticipanÈ›i" +reason = "Motiv:" +send = "TrimiteÈ›i cereri de semnare" +signatureSettings = "Setările semnăturii" +title = "RevizuiÈ›i detaliile sesiunii" +titleShort = "Revizuire È™i trimitere" +visibility = "Vizibilitate:" +visible = "Vizibil pe pagina {{page}}" +participantCount = "{{count}} participant(È›i) vor semna în ordine" + +[groupSigning.steps.selectDocument] +continue = "ContinuaÈ›i la selectarea participanÈ›ilor" +noFile = "SelectaÈ›i un singur fiÈ™ier PDF din fiÈ™ierele active pentru a crea o sesiune de semnare." +selectedFile = "Document selectat" +title = "SelectaÈ›i documentul" + +[groupSigning.steps.selectParticipants] +continue = "ContinuaÈ›i la setările semnăturii" +count = "{{count}} participant(È›i) selectaÈ›i" +label = "SelectaÈ›i participanÈ›ii" +placeholder = "AlegeÈ›i participanÈ›i pentru semnare..." +title = "AlegeÈ›i participanÈ›ii" + [getPdfInfo] downloadJson = "Descarcă JSON" downloads = "Descărcări" @@ -4460,7 +4860,10 @@ zoomOut = "MicÈ™oraÈ›i" [viewer] cannotPreviewFile = "Nu se poate previzualiza fiÈ™ierul" +disableColorFilter = "DezactivaÈ›i filtrul de culoare" dualPageView = "Vizualizare cu două pagini" +enableDarkFilter = "ActivaÈ›i filtrul întunecat" +enableSepiaFilter = "ActivaÈ›i filtrul sepia" firstPage = "Prima pagină" lastPage = "Ultima pagină" nextPage = "Pagina următoare" @@ -4470,6 +4873,22 @@ singlePageView = "Vizualizare cu o singură pagină" unknownFile = "FiÈ™ier necunoscut" zoomIn = "MăriÈ›i" zoomOut = "MicÈ™oraÈ›i" +resetZoom = "ResetaÈ›i zoomul" + +[viewer.nonPdf] +fileTypeBadge = "FiÈ™ier {{type}}" +convertToPdf = "ConvertiÈ›i în PDF" +loading = "Se încarcă..." +emptyFile = "FiÈ™ier gol" +csvStats = "{{rows}} rânduri · {{columns}} coloane · {{size}}" +sortedBy = "Sortat după: {{column}}" +columnDefault = "Coloana {{index}}" +htmlPreviewWarning = "Previzualizare HTML — resursele externe s-ar putea să nu se încarce · {{size}}" +htmlPreview = "Previzualizare HTML" +invalidJson = "JSON invalid — se afiÈ™ează conÈ›inutul brut" +textStats = "{{lines}} linii · {{size}}" +lineNumbers = "Numere de linie" +renderMarkdown = "RedaÈ›i markdown" [viewer.attachments] title = "AtaÈ™amente" @@ -4531,6 +4950,7 @@ toggleAttachments = "AfiÈ™ează/ascunde ataÈ™amentele" toggleTheme = "ComutaÈ›i tema" language = "Limbă" toggleAnnotations = "ComutaÈ›i vizibilitatea adnotărilor" +toggleLayers = "ComutaÈ›i straturile" search = "CăutaÈ›i în PDF" panMode = "Mod panoramare" applyRedactionsFirst = "AplicaÈ›i mai întâi redactările" @@ -5407,20 +5827,72 @@ title = "TipăreÈ™te FiÈ™ier" 2 = "Introdu Numele Imprimantei" [quickAccess] +access = "Acces" +accessAddPerson = "AdăugaÈ›i încă o persoană" +accessBack = "ÃŽnapoi" +accessCopyLink = "CopiaÈ›i linkul" +accessEmail = "Adresă de e-mail" +accessEmailPlaceholder = "nume@companie.com" +accessFileLabel = "FiÈ™ier" +accessGeneral = "Acces general" +accessInviteTitle = "InvitaÈ›i persoane" +accessOwner = "Proprietar" +accessPanel = "Acces la document" +accessPeople = "Persoane cu acces" +accessRemove = "EliminaÈ›i" +accessRestricted = "RestricÈ›ionat" +accessRestrictedHint = "Numai persoanele cu acces pot deschide" +accessRole = "Rol" +accessRoleCommenter = "Comentator" +accessRoleEditor = "Editor" +accessRoleViewer = "Vizualizator" +accessSelectedFile = "FiÈ™ier selectat" +accessSendInvite = "TrimiteÈ›i invitaÈ›ie" +accessTitle = "Acces la document" +accessYou = "Dvs." account = "Cont" +activeSessions = "Sesiuni active" +activeTab = "Active" activity = "Jurnal" adminSettings = "Setări admin" +allSessions = "Toate sesiunile" allTools = "All Tools" automate = "Auto" +back = "ÃŽnapoi" +certSign = "Semnare cu certificat" +completedSessions = "Sesiuni finalizate" +completedTab = "Finalizate" config = "Config" +createNew = "CreaÈ›i cerere nouă" +createSession = "CreaÈ›i cerere de semnare" +dueDate = "Data limită (opÈ›ional)" files = "FiÈ™iere" help = "Ajutor" +noActiveSessions = "Nicio cerere de semnare în aÈ™teptare sau sesiune activă" +noCompletedSessions = "Nicio sesiune finalizată" +noFile = "Niciun fiÈ™ier selectat" read = "Citire" reader = "Cititor" +refresh = "ReîmprospătaÈ›i" +requestSignatures = "SolicitaÈ›i semnături" +selectSingleFileToRequest = "SelectaÈ›i un singur fiÈ™ier PDF pentru a solicita semnături" +selectedFile = "FiÈ™ier selectat" +selectUsers = "SelectaÈ›i utilizatori pentru semnare" +selectUsersPlaceholder = "AlegeÈ›i participanÈ›i..." +sendingRequest = "Se trimite..." settings = "Setări" showMeAround = "Ghidează-mă" sign = "Semnează" +signatureRequests = "Cereri de semnare" +signYourself = "SemnaÈ›i personal" +newRequest = "Cerere nouă" tours = "Tururi" +wetSign = "AdăugaÈ›i semnătură" +filterMine = "Ale mele" +filterOverdue = "ÃŽntârziate" +filterSigned = "Semnate" +filterDeclined = "Refuzate" +searchDocuments = "CăutaÈ›i documente…" [quickAccess.helpMenu] adminTour = "Turul de administrare" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Serverul dvs. Stirling-PDF este offline, iar \"{{endp expired = "Sesiunea dvs. a expirat. ReîmprospătaÈ›i pagina È™i încercaÈ›i din nou." refreshPage = "ReîmprospătaÈ›i pagina" +[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" + +[signatureSettings.tooltip] +header = "Setări de aspect ale semnăturii" + +[signatureSettings.tooltip.location] +bullet1 = "Exemple: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Nu este acelaÈ™i lucru cu poziÈ›ia pe pagină" +bullet3 = "Poate fi necesară în anumite jurisdicÈ›ii legale" +description = "LocaÈ›ie geografică opÈ›ională unde a fost aplicată semnătura. Stocată în metadata certificatului." +title = "LocaÈ›ia semnăturii" + +[signatureSettings.tooltip.logo] +bullet1 = "AfiÈ™at alături de semnătură È™i text" +bullet2 = "Acceptă formatele PNG, JPG" +bullet3 = "ÃŽmbunătățeÈ™te aspectul profesional" +description = "AdăugaÈ›i un logo al companiei la semnăturile vizibile pentru branding È™i autenticitate." +title = "Logo companie" + +[signatureSettings.tooltip.reason] +bullet1 = "Exemple: \"Aprobare\", \"Acord contractual\", \"Revizuire finalizată\"" +bullet2 = "Vizibil în proprietățile semnăturii PDF" +bullet3 = "Util pentru trasee de audit È™i conformitate" +description = "Text opÈ›ional care explică de ce este semnat documentul. Stocat în metadata certificatului." +title = "Motivul semnăturii" + +[signatureSettings.tooltip.visibility] +bullet1 = "Vizibil: Semnătura apare pe PDF cu aspect personalizat" +bullet2 = "Invizibil: Certificatul este încorporat fără marcaj vizual" +bullet3 = "Semnăturile invizibile oferă în continuare validare criptografică" +description = "Controlează dacă semnătura este vizibilă pe document sau încorporată invizibil." +title = "Vizibilitatea semnăturii" + [settings.configuration] advanced = "Avansat" database = "Bază de date" endpoints = "Endpoint-uri" features = "FuncÈ›ii" +storageSharing = "Stocare fiÈ™iere È™i partajare" systemSettings = "Setări sistem" title = "ConfiguraÈ›ie" @@ -6332,10 +6868,13 @@ title = "Autentifică-te în Stirling" [setup.selfhosted] link = "sau conectaÈ›i-vă la un cont self-hosted" subtitle = "Introdu acreditările serverului tău" +changeServerLocked = "OrganizaÈ›ia dvs. a restricÈ›ionat această aplicaÈ›ie la un anumit server" switchToLocal = "UtilizaÈ›i instrumentele locale în schimb" title = "Autentifică-te pe server" [setup.selfhosted.unreachable] +changeServer = "ConectaÈ›i-vă la un alt server" +changeServerLocked = "OrganizaÈ›ia dvs. a restricÈ›ionat această aplicaÈ›ie la un anumit server" continueOffline = "UtilizaÈ›i instrumentele locale în schimb" message = "Nu s-a putut accesa {{url}}. VerificaÈ›i că serverul rulează È™i este accesibil." retry = "ReîncercaÈ›i" @@ -6529,6 +7068,15 @@ saved = "Salvate" text = "Text" title = "Tip de semnătură" +[signRequest] +declined = "Cererea de semnare a fost refuzată" +fetchFailed = "Nu s-a putut încărca cererea de semnare" +signed = "Document semnat cu succes" + +[signSession] +createFailed = "Nu s-a putut crea cererea de semnare" +created = "Cererea de semnare a fost trimisă" + [signup] accountCreatedSuccessfully = "Cont creat cu succes! Acum vă puteÈ›i autentifica." alreadyHaveAccount = "AveÈ›i deja un cont? AutentificaÈ›i-vă" @@ -6807,6 +7355,106 @@ title = "ÃŽmparte PDF după capitole" [splitPdfByChapters] tags = "împarte,capitole,semne de carte,organizează" +[storageShare] +accessed = "Accesat" +accessDenied = "Nu aveÈ›i acces la acest fiÈ™ier partajat. CereÈ›i proprietarului să îl partajeze cu dvs." +accessFailed = "Nu se poate încărca activitatea." +accessDeniedBody = "Nu aveÈ›i acces la acest fiÈ™ier. CereÈ›i proprietarului să îl partajeze cu dvs." +accessDeniedTitle = "Fără acces" +accessLimitedCommenter = "Accesul de comentare va fi disponibil în curând. CereÈ›i proprietarului acces de editor dacă aveÈ›i nevoie să descărcaÈ›i." +accessLimitedTitle = "Acces limitat" +accessLimitedViewer = "Acest link este doar pentru vizualizare. CereÈ›i proprietarului acces de editor dacă aveÈ›i nevoie să descărcaÈ›i." +createdAt = "Creat" +download = "DescărcaÈ›i" +downloadFailed = "Nu se poate descărca acest fiÈ™ier." +expiredBody = "Acest link de partajare este invalid sau a expirat." +expiredTitle = "Link expirat" +goToLogin = "MergeÈ›i la autentificare" +loadFailed = "Nu se poate deschide fiÈ™ierul partajat." +loading = "Se încarcă linkul de partajare..." +loginPrompt = "AutentificaÈ›i-vă pentru a accesa acest fiÈ™ier partajat." +loginRequired = "Autentificare necesară" +openInApp = "Deschide în Stirling PDF" +ownerLabel = "Proprietar" +ownerUnknown = "Necunoscut" +requiresLogin = "Acest fiÈ™ier partajat necesită autentificare." +roleCommenter = "Comentator" +roleEditor = "Editor" +roleViewer = "Vizualizator" +shareHeading = "FiÈ™ier partajat" +titleDefault = "FiÈ™ier partajat" +tryAgain = "Vă rugăm să încercaÈ›i din nou mai târziu." +addUser = "AdăugaÈ›i" +commenterHint = "FuncÈ›ia de comentare va fi disponibilă în curând." +copied = "Link copiat în clipboard" +copy = "CopiaÈ›i" +copyFailed = "Copiere eÈ™uată" +description = "CreaÈ›i un link de partajare pentru acest fiÈ™ier. Utilizatorii autentificaÈ›i cu linkul îl pot accesa." +downloadsCount = "Descărcări: {{count}}" +emailWarningBody = "Aceasta pare a fi o adresă de e-mail. Dacă această persoană nu este deja utilizator Stirling PDF, nu va putea accesa fiÈ™ierul." +emailWarningConfirm = "PartajaÈ›i oricum" +emailWarningTitle = "Adresă de e-mail" +errorTitle = "Partajarea a eÈ™uat" +failure = "Nu s-a putut genera un link de partajare. ÃŽncercaÈ›i din nou." +fileLabel = "FiÈ™ier" +generate = "GeneraÈ›i link" +generated = "Link de partajare generat" +hideActivity = "AscundeÈ›i activitatea" +invalidUsername = "IntroduceÈ›i un nume de utilizator sau o adresă de e-mail validă." +lastAccessed = "Accesat ultima dată" +linkAccessTitle = "Acces prin link de partajare" +linkLabel = "Link de partajare" +linksDisabled = "Linkurile de partajare sunt dezactivate." +linksDisabledBody = "Linkurile de partajare sunt dezactivate în setările serverului dvs." +manage = "GestionaÈ›i partajarea" +manageDescription = "CreaÈ›i È™i gestionaÈ›i linkuri pentru a partaja acest fiÈ™ier." +manageLoadFailed = "Nu se pot încărca linkurile de partajare." +manageTitle = "GestionaÈ›i partajarea" +noActivity = "Nicio activitate încă." +noLinks = "ÃŽncă nu există linkuri de partajare active." +noSharedUsers = "ÃŽncă nu au acces utilizatori." +removeLink = "EliminaÈ›i linkul" +removeUser = "EliminaÈ›i" +revokeFailed = "Nu s-a putut elimina linkul de partajare." +revoked = "Linkul de partajare a fost eliminat" +roleLabel = "Rol" +sharingDisabled = "Partajarea este dezactivată." +sharingDisabledBody = "Partajarea a fost dezactivată de setările serverului." +sharedUsersTitle = "Utilizatori cu care este partajat" +title = "PartajaÈ›i fiÈ™ierul" +unknownUser = "Utilizator necunoscut" +userAddFailed = "Nu se poate partaja cu acel utilizator." +userAdded = "Utilizator adăugat la lista de partajare." +usernameLabel = "Nume de utilizator sau e-mail" +usernamePlaceholder = "IntroduceÈ›i un nume de utilizator sau un e-mail" +userRemoveFailed = "Nu se poate elimina acel utilizator." +userRemoved = "Utilizator eliminat din lista de partajare." +viewActivity = "VizualizaÈ›i activitatea" +viewed = "Vizualizat" +viewsCount = "Vizualizări: {{count}}" +downloaded = "Descărcat" +bulkDescription = "CreaÈ›i un singur link pentru a partaja toate fiÈ™ierele selectate cu utilizatorii autentificaÈ›i." +bulkTitle = "PartajaÈ›i fiÈ™ierele selectate" +copyLink = "CopiaÈ›i linkul de partajare" +fileCount = "{{count}} fiÈ™iere selectate" +ownerOnly = "Doar proprietarul poate gestiona partajarea." +selectSingleFile = "SelectaÈ›i un singur fiÈ™ier pentru a gestiona partajarea." + +[storageUpload] +description = "Aceasta încarcă fiÈ™ierul curent în spaÈ›iul de stocare al serverului pentru accesul dvs." +errorTitle = "ÃŽncărcare eÈ™uată" +failure = "ÃŽncărcare eÈ™uată. VerificaÈ›i autentificarea È™i setările de stocare." +fileLabel = "FiÈ™ier" +hint = "Linkurile publice È™i modurile de acces sunt controlate de setările serverului." +success = "ÃŽncărcat pe server" +title = "ÃŽncărcaÈ›i pe server" +updateButton = "ActualizaÈ›i pe server" +uploadButton = "ÃŽncărcaÈ›i pe server" +bulkDescription = "Aceasta încarcă fiÈ™ierele selectate în spaÈ›iul de stocare al serverului dvs." +bulkTitle = "ÃŽncărcaÈ›i fiÈ™ierele selectate" +fileCount = "{{count}} fiÈ™iere selectate" +more = " +{{count}} în plus" + [storage] approximateSize = "Dimensiune aproximativă" fileTooLarge = "FiÈ™ier prea mare. Dimensiunea maximă per fiÈ™ier este" @@ -7153,6 +7801,30 @@ title = "VizualizaÈ›i/EditaÈ›i PDF" [warning] tooltipTitle = "Avertisment" +[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" + [watermark] completed = "Filigran adăugat" desc = "Adaugă filigrane text sau imagine în fiÈ™iere PDF" @@ -7333,6 +8005,7 @@ activeSession = "Sesiune activă" addMembers = "AdăugaÈ›i membri" admin = "Administrator" confirmDelete = "Sigur doriÈ›i să È™tergeÈ›i acest utilizator? Această acÈ›iune nu poate fi anulată." +confirmUnlock = "Sigur doriÈ›i să deblocaÈ›i acest cont de utilizator?" deleteUser = "ȘtergeÈ›i utilizatorul" deleteUserError = "Ștergerea utilizatorului a eÈ™uat" deleteUserSuccess = "Utilizator È™ters cu succes" @@ -7341,6 +8014,8 @@ disable = "Dezactivează" disabled = "Dezactivat" editRole = "EditaÈ›i rolul" enable = "Activează" +locked = "blocat" +lockedBadge = "Blocat" loading = "Se încarcă membrii..." loginRequired = "Activează mai întâi modul de autentificare" member = "Membru" @@ -7350,6 +8025,9 @@ searchMembers = "CăutaÈ›i membri..." status = "Stare" team = "Echipă" title = "Membri" +unlockAccount = "DeblocaÈ›i contul" +unlockUserError = "Deblocarea contului de utilizator a eÈ™uat" +unlockUserSuccess = "Contul de utilizator a fost deblocat cu succes" user = "Utilizator" [workspace.people.actions] diff --git a/frontend/public/locales/ru-RU/translation.toml b/frontend/public/locales/ru-RU/translation.toml index 7342f0b83f..5fe49e4a86 100644 --- a/frontend/public/locales/ru-RU/translation.toml +++ b/frontend/public/locales/ru-RU/translation.toml @@ -8,6 +8,7 @@ black = "Черный" blue = "Синий" bored = "Скучно ждать?" cancel = "Отмена" +confirm = "Подтвердить" changedCredsMessage = "Учетные данные изменены!" chooseFile = "Выбрать файл" close = "Закрыть" @@ -146,6 +147,7 @@ insufficientCredits = "ÐедоÑтаточно кредитов. Требует loadingCredits = "Проверка кредитов..." loadingProStatus = "Проверка ÑтатуÑа подпиÑки..." noticeTopUpOrPlan = "ÐедоÑтаточно кредитов, пополните Ð±Ð°Ð»Ð°Ð½Ñ Ð¸Ð»Ð¸ перейдите на тариф" +accessInvite = "ПриглаÑить" [account] accountSettings = "ÐаÑтройки аккаунта" @@ -1427,6 +1429,34 @@ title = "Обработка" description = "МакÑимальное Ð²Ñ€ÐµÐ¼Ñ Ð¾Ð¶Ð¸Ð´Ð°Ð½Ð¸Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð·Ð°Ð´Ð°Ñ‡Ð¸ до ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¾Ð± ошибке." label = "Таймаут обработки (Ñекунды)" +[admin.settings.storage] +description = "УправлÑйте хранением на Ñервере и параметрами общего доÑтупа." +title = "Хранение файлов и общий доÑтуп" + +[admin.settings.storage.enabled] +description = "Разрешить пользователÑм хранить файлы на Ñервере." +label = "Включить хранение файлов на Ñервере" + +[admin.settings.storage.sharing.email] +description = "Разрешить общий доÑтуп по адреÑам Ñлектронной почты." +label = "Включить общий доÑтуп по Ñлектронной почте" +mailLink = "ÐаÑтроить параметры почты" +mailNote = "ТребуетÑÑ Ð½Ð°Ñтройка почты. " + +[admin.settings.storage.sharing.enabled] +description = "Разрешить пользователÑм делитьÑÑ Ñохранёнными файлами." +label = "Включить общий доÑтуп" + +[admin.settings.storage.sharing.links] +description = "Разрешить общий доÑтуп через ÑÑылки Ð´Ð»Ñ Ð²Ð¾ÑˆÐµÐ´ÑˆÐ¸Ñ… пользователей." +frontendUrlLink = "ÐаÑтроить в ÑиÑтемных наÑтройках" +frontendUrlNote = "ТребуетÑÑ Frontend URL. " +label = "Включить ÑÑылки общего доÑтупа" + +[admin.settings.storage.signing.enabled] +description = "Разрешить пользователÑм Ñоздавать ÑеÑÑии подпиÑÐ°Ð½Ð¸Ñ Ð´Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð° Ñ Ð½ÐµÑколькими учаÑтниками. ТребуетÑÑ Ð²ÐºÐ»ÑŽÑ‡Ð¸Ñ‚ÑŒ хранение файлов на Ñервере." +label = "Включить групповое подпиÑание (Alpha)" + [admin.settings.unsavedChanges] cancel = "Продолжить редактирование" discard = "Отменить изменениÑ" @@ -2059,7 +2089,19 @@ numbers = "ЧиÑла/диапазоны: 5, 10–20" progressions = "ПрогреÑÑии: 3n, 4n+1" [certSign] +allSigned = "Ð’Ñе учаÑтники подпиÑали. Готово к завершению." +awaitingSignatures = "Ожидание подпиÑей" +signatureProgress = "{{signedCount}}/{{totalCount}} подпиÑей" chooseCertificate = "Выберите файл Ñертификата" +declined = "Отклонено" +fetchFailed = "Ðе удалоÑÑŒ загрузить данные о подпиÑании" +finalized = "Завершено" +notified = "Ожидает" +partialNote = "Ð’Ñ‹ можете завершить раньше Ñ Ñ‚ÐµÐºÑƒÑ‰Ð¸Ð¼Ð¸ подпиÑÑми. ÐеподпиÑавшие учаÑтники будут иÑключены." +pending = "Ожидает" +readyToFinalize = "Готово к завершению" +signed = "ПодпиÑано" +viewed = "ПроÑмотрено" chooseJksFile = "Выберите файл JKS" chooseP12File = "Выберите файл PKCS12" choosePfxFile = "Выберите файл PFX" @@ -2082,6 +2124,7 @@ title = "ПодпиÑание Ñертификатом" invisible = "ÐевидимаÑ" stepTitle = "Вид подпиÑи" visible = "ВидимаÑ" +visibility = "ВидимоÑть" [certSign.appearance.options] title = "Ð¡Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ð¾ подпиÑи" @@ -2188,6 +2231,252 @@ bullet4 = "Можно иÑпользовать пользовательÑкие text = "При проверке подпиÑей инÑтрумент Ñообщает, дейÑтвительны ли они, кто подпиÑал документ, когда он был подпиÑан и менÑлÑÑ Ð»Ð¸ поÑле подпиÑаниÑ." title = "Проверка подпиÑей" +[certSign.collab.finalize] +button = "Завершить и загрузить подпиÑанный PDF" +early = "Завершить Ñ Ñ‚ÐµÐºÑƒÑ‰Ð¸Ð¼Ð¸ подпиÑÑми" + +[certSign.collab.sessionDetail] +addButton = "Добавить учаÑтников" +addParticipants = "Добавить учаÑтников" +addParticipantsError = "Ðе удалоÑÑŒ добавить учаÑтников" +backToList = "Ðазад к ÑеÑÑиÑм" +deleteConfirm = "Ð’Ñ‹ уверены? ДейÑтвие необратимо." +deleteError = "Ðе удалоÑÑŒ удалить ÑеÑÑию" +deleted = "СеÑÑÐ¸Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ð°" +deleteSession = "Удалить ÑеÑÑию" +dueDate = "Срок" +finalizeError = "Ðе удалоÑÑŒ завершить ÑеÑÑию" +loadPdfError = "Ðе удалоÑÑŒ загрузить подпиÑанный PDF" +loadSignedPdf = "Загрузить подпиÑанный PDF в активные файлы" +messageLabel = "Сообщение" +noAdditionalInfo = "Ðет дополнительной информации" +owner = "Владелец" +participantRemoved = "УчаÑтник удалён" +participants = "УчаÑтники" +participantsAdded = "УчаÑтники уÑпешно добавлены" +removeParticipant = "Удалить" +removeParticipantError = "Ðе удалоÑÑŒ удалить учаÑтника" +selectUsers = "Выберите пользователей..." +sessionInfo = "Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ ÑеÑÑии" +workbenchTitle = "Управление ÑеÑÑией" + +[certSign.collab.signRequest] +addedToFiles = "Документ добавлен в активные файлы" +addSignature = "Добавьте Ñвою подпиÑÑŒ" +addToFiles = "Добавить в активные файлы" +advancedSettings = "Дополнительные наÑтройки" +backToList = "Ðазад к запроÑам подпиÑи" +certificateChoice = "Выберите Ñертификат Ð´Ð»Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñи" +changeSignature = "Изменить подпиÑÑŒ" +clearSignature = "ОчиÑтить подпиÑÑŒ" +completeAndSign = "Завершить и подпиÑать" +createNewSignature = "Создать новую подпиÑÑŒ" +declineButton = "Отклонить" +decline = "Отклонить запроÑ" +deleteSelected = "Удалить выбранную подпиÑÑŒ" +drawSignature = "ÐариÑуйте Ñвою подпиÑÑŒ ниже" +dueDate = "Срок" +fileTooLarge = "Размер файла должен быть меньше 5 МБ" +fontFamily = "Шрифт" +fontSize = "Размер шрифта: {{size}}px" +fontSizePlaceholder = "Размер" +from = "От" +invalidCertFile = "Выберите файл Ñертификата P12 или PFX" +invalidFileType = "Выберите файл изображениÑ" +location = "МеÑтоположение (необÑзательно)" +locationPlaceholder = "Откуда вы подпиÑываете?" +message = "Сообщение" +noCertificate = "Выберите файл Ñертификата" +noSignatures = "РазмеÑтите Ñ…Ð¾Ñ‚Ñ Ð±Ñ‹ одну подпиÑÑŒ на PDF" +p12File = "Файл Ñертификата P12/PFX" +password = "Пароль Ñертификата" +passwordPlaceholder = "Введите пароль..." +penColor = "Цвет пера" +penSize = "Толщина пера: {{size}}px" +placementActive = "Щёлкните по PDF, чтобы размеÑтить" +placeSignatureButton = "РазмеÑтить подпиÑÑŒ на PDF" +reason = "Причина (необÑзательно)" +reasonPlaceholder = "Зачем вы подпиÑываете?" +removeImage = "Удалить изображение" +removeCertFile = "Удалить файл" +savedSignatures = "Сохранённые подпиÑи" +selectFile = "Выбрать файл изображениÑ" +selectSignatureTitle = "Выбрать или Ñоздать подпиÑÑŒ" +signButton = "ПодпиÑать документ" +signatureInfo = "Эти наÑтройки задаёт владелец документа" +signaturePlaced = "ПодпиÑÑŒ размещена на Ñтранице" +signatureSettings = "ÐаÑтройки подпиÑи" +signatureText = "ТекÑÑ‚ подпиÑи" +signatureTextPlaceholder = "Введите ваше имÑ..." +signatureTypeLabel = "Тип подпиÑи" +signingTitle = "ПодпиÑание" +textColor = "Цвет текÑта" +typeSignature = "Введите Ñвоё имÑ, чтобы Ñоздать подпиÑÑŒ" +uploadCert = "СобÑтвенный Ñертификат" +uploadCertDesc = "ИÑпользуйте Ñвой Ñертификат P12/PFX" +uploadSignature = "Загрузите изображение подпиÑи" +usePersonalCert = "Личный Ñертификат" +usePersonalCertDesc = "ÐвтоматичеÑки Ñгенерирован Ð´Ð»Ñ Ð²Ð°ÑˆÐµÐ¹ учётной запиÑи" +useServerCert = "Сертификат организации" +useServerCertDesc = "Общий Ñертификат организации" +workbenchTitle = "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñи" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Выберите цвет штриха" +continue = "Продолжить" + +[certSign.collab.signRequest.certModal] +description = "Ð’Ñ‹ размеÑтили {{count}} подпиÑÑŒ(и). Выберите Ñертификат Ð´Ð»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ð¿Ð¾Ð´Ð¿Ð¸ÑаниÑ." +sign = "ПодпиÑать документ" +certValidating = "Проверка Ñертификата..." +certValidUntil = "Сертификат дейÑтвителен до {{date}}" +certInvalid = "ÐедейÑтвительный Ñертификат: {{error}}" +certInvalidFallback = "ÐедейÑтвительный Ñертификат" +certNetworkError = "Ðе удалоÑÑŒ подтвердить Ñертификат" +title = "ÐаÑтройка Ñертификата" + +[certSign.collab.signRequest.image] +hint = "Загрузите изображение PNG или JPG Ñ Ð²Ð°ÑˆÐµÐ¹ подпиÑью" + +[certSign.collab.signRequest.mode] +move = "ПеремеÑтить подпиÑÑŒ" +place = "РазмеÑтить подпиÑÑŒ" +title = "Режим Ñ€Ð°Ð·Ð¼ÐµÑ‰ÐµÐ½Ð¸Ñ Ð¸Ð»Ð¸ перемещениÑ" + +[certSign.collab.signRequest.modeTabs] +draw = "РиÑовать" +image = "Загрузить" +text = "ВвеÑти" + +[certSign.collab.signRequest.placeSignature] +message = "Щёлкните по PDF, чтобы размеÑтить Ñвою подпиÑÑŒ" +title = "РазмеÑтить подпиÑÑŒ" + +[certSign.collab.signRequest.preview] +imageAlt = "Ð’Ñ‹Ð±Ñ€Ð°Ð½Ð½Ð°Ñ Ð¿Ð¾Ð´Ð¿Ð¸ÑÑŒ" +missing = "Ðет предпроÑмотра" +textFallback = "ПодпиÑÑŒ" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "РиÑÐ¾Ð²Ð°Ð½Ð½Ð°Ñ Ð¿Ð¾Ð´Ð¿Ð¸ÑÑŒ" +defaultImageLabel = "Ð—Ð°Ð³Ñ€ÑƒÐ¶ÐµÐ½Ð½Ð°Ñ Ð¿Ð¾Ð´Ð¿Ð¸ÑÑŒ" +defaultLabel = "ПодпиÑÑŒ" +defaultTextLabel = "Ð’Ð²ÐµÐ´Ñ‘Ð½Ð½Ð°Ñ Ð¿Ð¾Ð´Ð¿Ð¸ÑÑŒ" +delete = "Удалить подпиÑÑŒ" +none = "Сохранённых подпиÑей нет" + +[certSign.collab.signRequest.signatureType] +draw = "РиÑовать" +type = "ВвеÑти" +upload = "Загрузить" + +[certSign.collab.signRequest.steps] +back = "Ðазад" +cancelPlacement = "Отменить размещение" +certificate = "Сертификат" +clickMultipleTimes = "Щёлкните по PDF неÑколько раз, чтобы размеÑтить подпиÑи. ПеретаÑкивайте подпиÑÑŒ Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ¼ÐµÑ‰ÐµÐ½Ð¸Ñ Ð¸Ð»Ð¸ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ñ€Ð°Ð·Ð¼ÐµÑ€Ð°." +clickToPlace = "Щёлкните по PDF, где должна поÑвитьÑÑ Ð²Ð°ÑˆÐ° подпиÑÑŒ." +continue = "Перейти к выбору Ñертификата" +continueToPlacement = "Перейти к размещению" +continueToReview = "Перейти к проверке" +createSignature = "Создать подпиÑÑŒ" +invisible = "ÐевидимаÑ" +location = "МеÑтоположение:" +multipleSignatures = "{{count}} подпиÑÑŒ(и) будут применены к PDF" +oneSignature = "1 подпиÑÑŒ будет применена к PDF" +placeOnPdf = "РазмеÑтить на PDF" +reason = "Причина:" +reviewTitle = "Проверка перед подпиÑанием" +signaturePlaced = "ПодпиÑÑŒ размещена на Ñтранице {{page}}. Ð’Ñ‹ можете Ñкорректировать позицию, щёлкнув ещё раз, или продолжить к проверке." +visible = "ВидимаÑ" +visibility = "ВидимоÑть:" +yourSignatures = "Ваши подпиÑи ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Цвет" +fontLabel = "Шрифт" +fontSizeLabel = "Размер" +fontSizePlaceholder = "16" +label = "ТекÑÑ‚ подпиÑи" +modalHint = "Введите Ñвоё имÑ, затем нажмите «Продолжить», чтобы размеÑтить его на PDF." +placeholder = "Введите ваше имÑ..." + +[certSign.collab.participant] +certValidating = "Проверка Ñертификата..." +certValid = "✓ Сертификат дейÑтвителен" +certValidUntil = " до {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "ÐедейÑтвительный Ñертификат" +certNetworkError = "Ðе удалоÑÑŒ подтвердить Ñертификат" + +[certSign.collab.addParticipants] +add = "Добавить {{count}} учаÑтник(ов)" +back = "Ðазад" +configureSignatures = "ÐаÑтроить параметры подпиÑи" +continue = "Перейти к наÑтройкам подпиÑи" +reasonHelp = "ПредуÑтановить причину подпиÑÐ°Ð½Ð¸Ñ Ð´Ð»Ñ Ñтих учаÑтников (необÑзательно; её можно изменить при подпиÑании)" +reasonPlaceholder = "например, «Утверждение», «Проверка»..." +selectUsers = "Выбрать пользователей" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Добавить Ñтраницу Ñводки подпиÑей" +includeSummaryPageHelp = "Ð’ конце будет добавлена Ñтраница Ñо вÑеми метаданными подпиÑи. ÐŸÐ¾Ð»Ñ Ñ†Ð¸Ñ„Ñ€Ð¾Ð²Ð¾Ð¹ подпиÑи на отдельных Ñтраницах будут Ñкрыты (рукопиÑные подпиÑи не затронуты)." + +[certSign.collab.sessionList] +active = "Ðктивные" +finalized = "Завершённые" + +[certSign.collab.signatureSettings] +description = "ÐаÑтройте, как будут выглÑдеть подпиÑи Ð´Ð»Ñ Ð²Ñех учаÑтников" +title = "Внешний вид подпиÑи" + +[certSign.collab.userSelector] +inviteUsers = "Добавить пользователей" +loadError = "Ðе удалоÑÑŒ загрузить пользователей" +noTeam = "Без команды" +noUsers = "Другие пользователи не найдены." +placeholder = "Выберите пользователей..." + +[certSign.mobile] +panelActions = "ДейÑтвиÑ" +panelDocument = "Документ" +panelPeople = "Люди" + +[certSign.sessions] +deleted = "СеÑÑÐ¸Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ð°" +fetchFailed = "Ðе удалоÑÑŒ загрузить данные ÑеÑÑии" +finalized = "СеÑÑÐ¸Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð°" +loaded = "ПодпиÑанный PDF загружен" +pdfNotReady = "PDF не готов" +pdfNotReadyDesc = "ПодпиÑанный PDF генерируетÑÑ. Повторите попытку позже." + +[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" + [changeCreds] changePassword = "Ð’Ñ‹ иÑпользуете Ñтандартные учетные данные Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ð°. ПожалуйÑта, введите новый пароль" changeUsername = "Обновите Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ. ПоÑле Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð²Ñ‹ будете выведены из ÑиÑтемы." @@ -3242,6 +3531,46 @@ totalSelected = "Ð’Ñего выбрано" unsupported = "Ðе поддерживаетÑÑ" unzip = "РаÑпаковать" uploadError = "Ðе удалоÑÑŒ загрузить некоторые файлы." +copyCreated = "ÐšÐ¾Ð¿Ð¸Ñ Ñохранена на Ñтом уÑтройÑтве." +copyFailed = "Ðе удалоÑÑŒ Ñоздать копию." +leaveShare = "Убрать из моего ÑпиÑка" +leaveShareFailed = "Ðе удалоÑÑŒ удалить общий файл." +leaveShareSuccess = "Удалено из вашего ÑпиÑка общих файлов." +removeBoth = "Удалить из обоих" +removeFilePrompt = "Этот файл Ñохранён на Ñтом уÑтройÑтве и на вашем Ñервере. Где вы хотите его удалить?" +removeFileTitle = "Удалить файл" +removeLocalOnly = "Только на Ñтом уÑтройÑтве" +removeServerFailed = "Ðе удалоÑÑŒ удалить файл Ñ Ñервера." +removeServerOnly = "Только на Ñервере" +removeServerOnlyPrompt = "Этот файл хранитÑÑ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ на вашем Ñервере. Удалить его Ñ Ñервера?" +removeServerSuccess = "Удалено Ñ Ñервера." +removeSharedPrompt = "Этот файл предоÑтавлен вам. Ð’Ñ‹ можете удалить его Ñ Ñтого уÑтройÑтва или из ÑпиÑка общих файлов." +removeSharedServerOnlyBlockedPrompt = "Этот файл предоÑтавлен вам и хранитÑÑ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ на Ñервере." +removeSharedServerOnlyPrompt = "Этот файл предоÑтавлен вам и хранитÑÑ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ на Ñервере. Удалить его из вашего ÑпиÑка?" +changesNotUploaded = "Ð˜Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð½Ðµ загружены" +cloudFile = "Файл в облаке" +filterAll = "Ð’Ñе" +filterLocal = "Локальные" +filterSharedByMe = "Мною предоÑтавленные" +filterSharedWithMe = "ПредоÑтавлены мне" +lastSynced = "ПоÑледнÑÑ ÑинхронизациÑ" +localOnly = "Только локально" +makeCopy = "Сделать копию" +owner = "Владелец" +ownerUnknown = "ÐеизвеÑтно" +share = "ПоделитьÑÑ" +shareSelected = "ПоделитьÑÑ Ð²Ñ‹Ð±Ñ€Ð°Ð½Ð½Ñ‹Ð¼Ð¸" +sharedByYou = "ПредоÑтавлены вами" +sharedEditNoticeBody = "У Ð²Ð°Ñ Ð½ÐµÑ‚ прав на редактирование Ñерверной верÑии Ñтого файла. Любые Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð±ÑƒÐ´ÑƒÑ‚ Ñохранены как Ð»Ð¾ÐºÐ°Ð»ÑŒÐ½Ð°Ñ ÐºÐ¾Ð¿Ð¸Ñ." +sharedEditNoticeConfirm = "ПонÑтно" +sharedEditNoticeTitle = "Ð¡ÐµÑ€Ð²ÐµÑ€Ð½Ð°Ñ ÐºÐ¾Ð¿Ð¸Ñ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ" +sharedWithYou = "ПредоÑтавлены вам" +sharing = "Общий доÑтуп" +storageState = "Хранилище" +synced = "Синхронизировано" +updateOnServer = "Обновить на Ñервере" +uploadSelected = "Загрузить выбранные" +uploadToServer = "Загрузить на Ñервер" [files] addFiles = "Добавить файлы" @@ -3367,6 +3696,77 @@ title = "Об уплощении PDF" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "О групповом подпиÑании" + +[groupSigning.tooltip.finalization] +bullet1 = "Ð’Ñе подпиÑи применÑÑŽÑ‚ÑÑ Ð² указанном вами порÑдке учаÑтников" +bullet2 = "При необходимоÑти можно завершить Ñ Ð½ÐµÐ¿Ð¾Ð»Ð½Ñ‹Ð¼ набором подпиÑей" +bullet3 = "ПоÑле Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ ÑеÑÑию Ð½ÐµÐ»ÑŒÐ·Ñ Ð¸Ð·Ð¼ÐµÐ½Ð¸Ñ‚ÑŒ" +description = "Когда вÑе учаÑтники подпишут (или вы решите завершить раньше), вы Ñможете Ñгенерировать финальный подпиÑанный PDF." +title = "ПроцеÑÑ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ" + +[groupSigning.tooltip.roles] +bullet1 = "Владелец (вы): Ñоздаёт ÑеÑÑию, наÑтраивает Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ Ð¿Ð¾ умолчанию, завершает документ" +bullet2 = "УчаÑтники: Ñоздают подпиÑÑŒ, выбирают Ñертификат, размещают на PDF" +bullet3 = "УчаÑтники не могут изменÑть видимоÑть, причину или меÑтоположение подпиÑи" +description = "Ð’Ñ‹ управлÑете наÑтройками внешнего вида подпиÑи Ð´Ð»Ñ Ð²Ñех учаÑтников." +title = "Роли учаÑтников" + +[groupSigning.tooltip.sequential] +bullet1 = "Первый учаÑтник должен подпиÑать, прежде чем второй получит доÑтуп к документу" +bullet2 = "ОбеÑпечивает корректный порÑдок подпиÑÐ°Ð½Ð¸Ñ Ð´Ð»Ñ ÑŽÑ€Ð¸Ð´Ð¸Ñ‡ÐµÑкой ÑоответÑтвиÑ" +bullet3 = "Ð’Ñ‹ можете менÑть порÑдок учаÑтников, перетаÑÐºÐ¸Ð²Ð°Ñ Ð¸Ñ… в ÑпиÑке" +description = "УчаÑтники подпиÑывают документы в указанном вами порÑдке. Каждый подпиÑант получает уведомление, когда наÑтупает его очередь." +title = "ПоÑледовательное подпиÑание" + +[groupSigning.steps] +back = "Ðазад" +completed = "Завершено" +current = "Текущий" +stepLabel = "Шаг {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Перейти к проверке" +invisible = "ПодпиÑи будут невидимыми (только метаданные)" +locationLabel = "МеÑтоположение:" +preview = "ПредпроÑмотр" +reasonLabel = "Причина:" +title = "ÐаÑтроить параметры подпиÑи" +visible = "ПодпиÑи будут видимы на Ñтранице {{page}}" + +[groupSigning.steps.review] +document = "Документ" +dueDate = "Срок (необÑзательно)" +dueDatePlaceholder = "Выберите Ñрок..." +invisible = "ÐÐµÐ²Ð¸Ð´Ð¸Ð¼Ð°Ñ (только метаданные)" +location = "МеÑтоположение:" +logo = "Логотип:" +logoHidden = "Без логотипа" +logoShown = "Логотип Stirling PDF отображаетÑÑ" +participants = "УчаÑтники" +reason = "Причина:" +send = "Отправить запроÑÑ‹ на подпиÑÑŒ" +signatureSettings = "ÐаÑтройки подпиÑи" +title = "Проверка Ñведений о ÑеÑÑии" +titleShort = "Проверка и отправка" +visibility = "ВидимоÑть:" +visible = "Видима на Ñтранице {{page}}" +participantCount = "{{count}} учаÑтник(ов) будет подпиÑывать по порÑдку" + +[groupSigning.steps.selectDocument] +continue = "Перейти к выбору учаÑтников" +noFile = "Выберите один PDF-файл из активных файлов, чтобы Ñоздать ÑеÑÑию подпиÑаниÑ." +selectedFile = "Выбранный документ" +title = "Выберите документ" + +[groupSigning.steps.selectParticipants] +continue = "Перейти к наÑтройкам подпиÑи" +count = "Выбрано учаÑтник(ов): {{count}}" +label = "Выберите учаÑтников" +placeholder = "Выберите учаÑтников Ð´Ð»Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñи..." +title = "Выбор учаÑтников" + [getPdfInfo] downloadJson = "Скачать JSON" downloads = "Загрузки" @@ -4460,7 +4860,10 @@ zoomOut = "Уменьшить" [viewer] cannotPreviewFile = "Ðе удаётÑÑ Ð¿Ñ€Ð¾Ñмотреть файл" +disableColorFilter = "Отключить цветовой фильтр" dualPageView = "ДвухÑтраничный вид" +enableDarkFilter = "Включить тёмный фильтр" +enableSepiaFilter = "Включить фильтр ÑепиÑ" firstPage = "ÐŸÐµÑ€Ð²Ð°Ñ Ñтраница" lastPage = "ПоÑледнÑÑ Ñтраница" nextPage = "Ð¡Ð»ÐµÐ´ÑƒÑŽÑ‰Ð°Ñ Ñтраница" @@ -4470,6 +4873,22 @@ singlePageView = "ОдноÑтраничный вид" unknownFile = "ÐеизвеÑтный файл" zoomIn = "Увеличить" zoomOut = "Уменьшить" +resetZoom = "СброÑить маÑштаб" + +[viewer.nonPdf] +fileTypeBadge = "Файл {{type}}" +convertToPdf = "Преобразовать в PDF" +loading = "Загрузка..." +emptyFile = "ПуÑтой файл" +csvStats = "{{rows}} Ñтрок · {{columns}} Ñтолбцов · {{size}}" +sortedBy = "Сортировка по: {{column}}" +columnDefault = "Столбец {{index}}" +htmlPreviewWarning = "ПредпроÑмотр HTML — внешние реÑурÑÑ‹ могут не загрузитьÑÑ Â· {{size}}" +htmlPreview = "ПредпроÑмотр HTML" +invalidJson = "Ðекорректный JSON — показано иÑходное Ñодержимое" +textStats = "{{lines}} Ñтрок · {{size}}" +lineNumbers = "Ðомера Ñтрок" +renderMarkdown = "Отобразить markdown" [viewer.attachments] title = "ВложениÑ" @@ -4531,6 +4950,7 @@ toggleAttachments = "Показать/Ñкрыть вложениÑ" toggleTheme = "Переключить тему" language = "Язык" toggleAnnotations = "Показать/Ñкрыть аннотации" +toggleLayers = "Переключить Ñлои" search = "ПоиÑк по PDF" panMode = "Режим панорамированиÑ" applyRedactionsFirst = "Сначала примените зачернениÑ" @@ -5407,20 +5827,72 @@ title = "Печать файла" 2 = "Введите Ð¸Ð¼Ñ Ð¿Ñ€Ð¸Ð½Ñ‚ÐµÑ€Ð°" [quickAccess] +access = "ДоÑтуп" +accessAddPerson = "Добавить ещё человека" +accessBack = "Ðазад" +accessCopyLink = "Копировать ÑÑылку" +accessEmail = "ÐÐ´Ñ€ÐµÑ Ñлектронной почты" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Файл" +accessGeneral = "Общий доÑтуп" +accessInviteTitle = "ПриглаÑить людей" +accessOwner = "Владелец" +accessPanel = "ДоÑтуп к документу" +accessPeople = "Люди Ñ Ð´Ð¾Ñтупом" +accessRemove = "Удалить" +accessRestricted = "Ограничен" +accessRestrictedHint = "Только пользователи Ñ Ð´Ð¾Ñтупом могут открыть" +accessRole = "Роль" +accessRoleCommenter = "Комментатор" +accessRoleEditor = "Редактор" +accessRoleViewer = "ПроÑмотр" +accessSelectedFile = "Выбранный файл" +accessSendInvite = "Отправить приглашение" +accessTitle = "ДоÑтуп к документу" +accessYou = "Ð’Ñ‹" account = "Ð£Ñ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ" +activeSessions = "Ðктивные ÑеÑÑии" +activeTab = "Ðктивные" activity = "Журнал" adminSettings = "Ðдмин. наÑтр." +allSessions = "Ð’Ñе ÑеÑÑии" allTools = "ИнÑтр." automate = "Ðвто" +back = "Ðазад" +certSign = "ПодпиÑать Ñертификатом" +completedSessions = "Завершённые ÑеÑÑии" +completedTab = "Завершённые" config = "Конфиг" +createNew = "Создать новый запроÑ" +createSession = "Создать Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñи" +dueDate = "Срок (необÑзательно)" files = "Файлы" help = "Справка" +noActiveSessions = "Ðет ожидающих запроÑов подпиÑи или активных ÑеÑÑий" +noCompletedSessions = "Ðет завершённых ÑеÑÑий" +noFile = "Файл не выбран" read = "Чтение" reader = "Читалка" +refresh = "Обновить" +requestSignatures = "ЗапроÑить подпиÑи" +selectSingleFileToRequest = "Выберите один PDF-файл, чтобы запроÑить подпиÑи" +selectedFile = "Выбранный файл" +selectUsers = "Выберите пользователей Ð´Ð»Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñи" +selectUsersPlaceholder = "Выберите учаÑтников..." +sendingRequest = "Отправка..." settings = "Опции" showMeAround = "Проведите по интерфейÑу" sign = "ПодпиÑÑŒ" +signatureRequests = "ЗапроÑÑ‹ подпиÑи" +signYourself = "ПодпиÑать Ñамому" +newRequest = "Ðовый запроÑ" tours = "Туры" +wetSign = "Добавить подпиÑÑŒ" +filterMine = "Мои" +filterOverdue = "ПроÑрочено" +filterSigned = "ПодпиÑано" +filterDeclined = "Отклонено" +searchDocuments = "ПоиÑк документов…" [quickAccess.helpMenu] adminTour = "Обзор админиÑтрированиÑ" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Ваш Ñервер Stirling-PDF офлайн, и \"{ expired = "Ваша ÑеÑÑÐ¸Ñ Ð¸Ñтекла. ПожалуйÑта, обновите Ñтраницу и попробуйте Ñнова." refreshPage = "Обновить Ñтраницу" +[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 = "ПорÑдок подпиÑей" + +[signatureSettings.tooltip] +header = "ÐаÑтройки внешнего вида подпиÑи" + +[signatureSettings.tooltip.location] +bullet1 = "Примеры: «New York, USA», «London Office», «Remote»" +bullet2 = "Ðе то же Ñамое, что Ð¿Ð¾Ð·Ð¸Ñ†Ð¸Ñ Ð½Ð° Ñтранице" +bullet3 = "Может требоватьÑÑ Ð² некоторых юриÑдикциÑÑ…" +description = "ÐеобÑзательное географичеÑкое меÑтоположение, где была поÑтавлена подпиÑÑŒ. ХранитÑÑ Ð² метаданных Ñертификата." +title = "МеÑтоположение подпиÑи" + +[signatureSettings.tooltip.logo] +bullet1 = "ОтображаетÑÑ Ñ€Ñдом Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñью и текÑтом" +bullet2 = "ПоддерживаютÑÑ Ñ„Ð¾Ñ€Ð¼Ð°Ñ‚Ñ‹ PNG, JPG" +bullet3 = "Повышает профеÑÑиональный вид" +description = "Добавьте логотип компании к видимым подпиÑÑм Ð´Ð»Ñ Ð±Ñ€ÐµÐ½Ð´Ð¸Ð½Ð³Ð° и Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ Ð¿Ð¾Ð´Ð»Ð¸Ð½Ð½Ð¾Ñти." +title = "Логотип компании" + +[signatureSettings.tooltip.reason] +bullet1 = "Примеры: «Утверждение», «Заключение контракта», «Проверка завершена»" +bullet2 = "Видна в ÑвойÑтвах подпиÑи PDF" +bullet3 = "Полезна Ð´Ð»Ñ Ð°ÑƒÐ´Ð¸Ñ‚Ð° и ÑоответÑÑ‚Ð²Ð¸Ñ Ñ‚Ñ€ÐµÐ±Ð¾Ð²Ð°Ð½Ð¸Ñм" +description = "ÐеобÑзательный текÑÑ‚, объÑÑнÑющий, почему документ подпиÑываетÑÑ. ХранитÑÑ Ð² метаданных Ñертификата." +title = "Причина подпиÑи" + +[signatureSettings.tooltip.visibility] +bullet1 = "ВидимаÑ: подпиÑÑŒ поÑвлÑетÑÑ Ð² PDF Ñ Ð½Ð°Ñтраиваемым видом" +bullet2 = "ÐевидимаÑ: Ñертификат вÑтраиваетÑÑ Ð±ÐµÐ· визуальной метки" +bullet3 = "Ðевидимые подпиÑи вÑÑ‘ равно обеÑпечивают криптографичеÑкую проверку" +description = "ОпределÑет, будет ли подпиÑÑŒ видна в документе или вÑтроена невидимо." +title = "ВидимоÑть подпиÑи" + [settings.configuration] advanced = "Дополнительно" database = "База данных" endpoints = "Конечные точки" features = "Функции" +storageSharing = "Хранение файлов и общий доÑтуп" systemSettings = "СиÑтемные наÑтройки" title = "КонфигурациÑ" @@ -6332,10 +6868,13 @@ title = "Вход в Stirling" [setup.selfhosted] link = "или подключитеÑÑŒ к ÑамохоÑтируемой учётной запиÑи" subtitle = "Введите учётные данные Ñервера" +changeServerLocked = "Ваша Ð¾Ñ€Ð³Ð°Ð½Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡Ð¸Ð»Ð° Ñто приложение определённым Ñервером" switchToLocal = "ВмеÑто Ñтого иÑпользовать локальные инÑтрументы" title = "Вход на Ñервер" [setup.selfhosted.unreachable] +changeServer = "ПодключитьÑÑ Ðº другому Ñерверу" +changeServerLocked = "Ваша Ð¾Ñ€Ð³Ð°Ð½Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡Ð¸Ð»Ð° Ñто приложение определённым Ñервером" continueOffline = "ВмеÑто Ñтого иÑпользовать локальные инÑтрументы" message = "Ðе удалоÑÑŒ ÑвÑзатьÑÑ Ñ {{url}}. Проверьте, что Ñервер запущен и доÑтупен." retry = "Повторить попытку" @@ -6529,6 +7068,15 @@ saved = "Сохранённое" text = "ТекÑÑ‚" title = "Тип подпиÑи" +[signRequest] +declined = "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñи отклонён" +fetchFailed = "Ðе удалоÑÑŒ загрузить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñи" +signed = "Документ уÑпешно подпиÑан" + +[signSession] +createFailed = "Ðе удалоÑÑŒ Ñоздать Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñи" +created = "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñи отправлен" + [signup] accountCreatedSuccessfully = "Ðккаунт уÑпешно Ñоздан! Теперь вы можете войти." alreadyHaveAccount = "Уже еÑть аккаунт? Войдите" @@ -6807,6 +7355,106 @@ title = "Разделить PDF по главам" [splitPdfByChapters] tags = "разделение,главы,закладки,организациÑ" +[storageShare] +accessed = "Открыто" +accessDenied = "У Ð²Ð°Ñ Ð½ÐµÑ‚ доÑтупа к Ñтому общему файлу. ПопроÑите владельца поделитьÑÑ Ð¸Ð¼ Ñ Ð²Ð°Ð¼Ð¸." +accessFailed = "Ðе удалоÑÑŒ загрузить активноÑть." +accessDeniedBody = "У Ð²Ð°Ñ Ð½ÐµÑ‚ доÑтупа к Ñтому файлу. ПопроÑите владельца поделитьÑÑ Ð¸Ð¼ Ñ Ð²Ð°Ð¼Ð¸." +accessDeniedTitle = "Ðет доÑтупа" +accessLimitedCommenter = "ДоÑтуп Ð´Ð»Ñ ÐºÐ¾Ð¼Ð¼ÐµÐ½Ñ‚Ð°Ñ€Ð¸ÐµÐ² Ñкоро поÑвитÑÑ. ПопроÑите у владельца доÑтуп редактора, еÑли нужно Ñкачать." +accessLimitedTitle = "Ограниченный доÑтуп" +accessLimitedViewer = "Эта ÑÑылка только Ð´Ð»Ñ Ð¿Ñ€Ð¾Ñмотра. ПопроÑите у владельца доÑтуп редактора, еÑли нужно Ñкачать." +createdAt = "Создано" +download = "Скачать" +downloadFailed = "Ðе удалоÑÑŒ Ñкачать Ñтот файл." +expiredBody = "Эта ÑÑылка общего доÑтупа недейÑтвительна или Ñрок её дейÑÑ‚Ð²Ð¸Ñ Ð¸Ñтёк." +expiredTitle = "СÑылка недейÑтвительна" +goToLogin = "Перейти ко входу" +loadFailed = "Ðе удалоÑÑŒ открыть общий файл." +loading = "Загрузка ÑÑылки общего доÑтупа..." +loginPrompt = "Войдите, чтобы получить доÑтуп к Ñтому общему файлу." +loginRequired = "ТребуетÑÑ Ð²Ñ…Ð¾Ð´" +openInApp = "Открыть в Stirling PDF" +ownerLabel = "Владелец" +ownerUnknown = "ÐеизвеÑтно" +requiresLogin = "Ð”Ð»Ñ Ñтого общего файла требуетÑÑ Ð²Ñ…Ð¾Ð´." +roleCommenter = "Комментатор" +roleEditor = "Редактор" +roleViewer = "ПроÑмотр" +shareHeading = "Общий файл" +titleDefault = "Общий файл" +tryAgain = "Повторите попытку позже." +addUser = "Добавить" +commenterHint = "Комментирование Ñкоро поÑвитÑÑ." +copied = "СÑылка Ñкопирована в буфер обмена" +copy = "Копировать" +copyFailed = "Ðе удалоÑÑŒ Ñкопировать" +description = "Создать ÑÑылку общего доÑтупа к Ñтому файлу. Вошедшие пользователи Ñо ÑÑылкой Ñмогут получить доÑтуп." +downloadsCount = "Загрузки: {{count}}" +emailWarningBody = "Похоже на Ð°Ð´Ñ€ÐµÑ Ñлектронной почты. ЕÑли Ñтот человек ещё не ÑвлÑетÑÑ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»ÐµÐ¼ Stirling PDF, он не Ñможет получить доÑтуп к файлу." +emailWarningConfirm = "Ð’ÑÑ‘ равно поделитьÑÑ" +emailWarningTitle = "ÐÐ´Ñ€ÐµÑ Ñлектронной почты" +errorTitle = "Ðе удалоÑÑŒ поделитьÑÑ" +failure = "Ðе удалоÑÑŒ Ñгенерировать ÑÑылку общего доÑтупа. Повторите попытку." +fileLabel = "Файл" +generate = "Сгенерировать ÑÑылку" +generated = "СÑылка общего доÑтупа Ñоздана" +hideActivity = "Скрыть активноÑть" +invalidUsername = "Введите корректное Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸Ð»Ð¸ Ð°Ð´Ñ€ÐµÑ Ñлектронной почты." +lastAccessed = "ПоÑледний доÑтуп" +linkAccessTitle = "ДоÑтуп по ÑÑылке общего доÑтупа" +linkLabel = "СÑылка общего доÑтупа" +linksDisabled = "СÑылки общего доÑтупа отключены." +linksDisabledBody = "СÑылки общего доÑтупа отключены наÑтройками вашего Ñервера." +manage = "Управление общим доÑтупом" +manageDescription = "Создавайте и управлÑйте ÑÑылками Ð´Ð»Ñ Ð¾Ð±Ñ‰ÐµÐ³Ð¾ доÑтупа к Ñтому файлу." +manageLoadFailed = "Ðе удалоÑÑŒ загрузить ÑÑылки общего доÑтупа." +manageTitle = "Управление общим доÑтупом" +noActivity = "Пока нет активноÑти." +noLinks = "Ðктивных ÑÑылок общего доÑтупа пока нет." +noSharedUsers = "Пока нет пользователей Ñ Ð´Ð¾Ñтупом." +removeLink = "Удалить ÑÑылку" +removeUser = "Удалить" +revokeFailed = "Ðе удалоÑÑŒ удалить ÑÑылку общего доÑтупа." +revoked = "СÑылка общего доÑтупа удалена" +roleLabel = "Роль" +sharingDisabled = "Общий доÑтуп отключен." +sharingDisabledBody = "Общий доÑтуп отключен наÑтройками вашего Ñервера." +sharedUsersTitle = "Пользователи Ñ Ð´Ð¾Ñтупом" +title = "Общий доÑтуп к файлу" +unknownUser = "ÐеизвеÑтный пользователь" +userAddFailed = "Ðе удалоÑÑŒ предоÑтавить общий доÑтуп Ñтому пользователю." +userAdded = "Пользователь добавлен в ÑпиÑок доÑтупа." +usernameLabel = "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸Ð»Ð¸ ÑÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð°Ñ Ð¿Ð¾Ñ‡Ñ‚Ð°" +usernamePlaceholder = "Введите Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸Ð»Ð¸ Ñлектронную почту" +userRemoveFailed = "Ðе удалоÑÑŒ удалить Ñтого пользователÑ." +userRemoved = "Пользователь удален из ÑпиÑка доÑтупа." +viewActivity = "ПроÑмотреть активноÑть" +viewed = "ПроÑмотрено" +viewsCount = "ПроÑмотры: {{count}}" +downloaded = "Скачано" +bulkDescription = "Создать одну ÑÑылку Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð¾ÑÑ‚Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¾Ð±Ñ‰ÐµÐ³Ð¾ доÑтупа ко вÑем выбранным файлам авторизованным пользователÑм." +bulkTitle = "Общий доÑтуп к выбранным файлам" +copyLink = "Скопировать ÑÑылку общего доÑтупа" +fileCount = "Выбрано файлов: {{count}}" +ownerOnly = "Только владелец может управлÑть общим доÑтупом." +selectSingleFile = "Выберите один файл Ð´Ð»Ñ ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¾Ð±Ñ‰Ð¸Ð¼ доÑтупом." + +[storageUpload] +description = "Это загружает текущий файл в хранилище Ñервера Ð´Ð»Ñ Ð²Ð°ÑˆÐµÐ³Ð¾ доÑтупа." +errorTitle = "Сбой загрузки" +failure = "Ðе удалоÑÑŒ загрузить. Проверьте Ñвои учетные данные и наÑтройки хранилища." +fileLabel = "Файл" +hint = "Публичные ÑÑылки и режимы доÑтупа управлÑÑŽÑ‚ÑÑ Ð½Ð°Ñтройками вашего Ñервера." +success = "Загружено на Ñервер" +title = "Загрузить на Ñервер" +updateButton = "Обновить на Ñервере" +uploadButton = "Загрузить на Ñервер" +bulkDescription = "Это загружает выбранные файлы в хранилище вашего Ñервера." +bulkTitle = "Загрузить выбранные файлы" +fileCount = "Выбрано файлов: {{count}}" +more = " +{{count}} ещё" + [storage] approximateSize = "Примерный размер" fileTooLarge = "Файл Ñлишком большой. МакÑимальный размер на файл —" @@ -7153,6 +7801,30 @@ title = "Смотреть/Редактировать PDF" [warning] tooltipTitle = "Предупреждение" +[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 = "Загрузить изображение подпиÑи" + [watermark] completed = "ВодÑной знак добавлен" desc = "ДобавлÑйте текÑтовые или графичеÑкие водÑные знаки в PDF-файлы" @@ -7333,6 +8005,7 @@ activeSession = "Ðктивный ÑеанÑ" addMembers = "Добавить учаÑтников" admin = "ÐдминиÑтратор" confirmDelete = "Ð’Ñ‹ уверены, что хотите удалить Ñтого пользователÑ? Это дейÑтвие Ð½ÐµÐ»ÑŒÐ·Ñ Ð¾Ñ‚Ð¼ÐµÐ½Ð¸Ñ‚ÑŒ." +confirmUnlock = "Ð’Ñ‹ уверены, что хотите разблокировать Ñту учетную запиÑÑŒ пользователÑ?" deleteUser = "Удалить пользователÑ" deleteUserError = "Ðе удалоÑÑŒ удалить пользователÑ" deleteUserSuccess = "Пользователь уÑпешно удален" @@ -7341,6 +8014,8 @@ disable = "Отключить" disabled = "Отключен" editRole = "Изменить роль" enable = "Включить" +locked = "заблокирован" +lockedBadge = "Заблокирован" loading = "Загрузка учаÑтников..." loginRequired = "Сначала включите режим входа" member = "УчаÑтник" @@ -7350,6 +8025,9 @@ searchMembers = "ПоиÑк учаÑтников..." status = "СтатуÑ" team = "Команда" title = "Люди" +unlockAccount = "Разблокировать учетную запиÑÑŒ" +unlockUserError = "Ðе удалоÑÑŒ разблокировать учетную запиÑÑŒ пользователÑ" +unlockUserSuccess = "Ð£Ñ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ ÑƒÑпешно разблокирована" user = "Пользователь" [workspace.people.actions] diff --git a/frontend/public/locales/sk-SK/translation.toml b/frontend/public/locales/sk-SK/translation.toml index 859b1f5a4c..9070f34b7f 100644 --- a/frontend/public/locales/sk-SK/translation.toml +++ b/frontend/public/locales/sk-SK/translation.toml @@ -8,6 +8,7 @@ black = "ÄŒierna" blue = "Modrá" bored = "Nudíte sa pri Äakaní?" cancel = "ZruÅ¡iÅ¥" +confirm = "PotvrdiÅ¥" changedCredsMessage = "Údaje zmenené!" chooseFile = "VybraÅ¥ súbor" close = "ZatvoriÅ¥" @@ -146,6 +147,7 @@ insufficientCredits = "Nedostatok kreditov. Potrebné: {{requiredCredits}}, K di loadingCredits = "Kontrolujú sa kredity..." loadingProStatus = "Kontroluje sa stav predplatného..." noticeTopUpOrPlan = "Nedostatok kreditov, dobiÅ¥ alebo prejsÅ¥ na plán" +accessInvite = "PozvaÅ¥" [account] accountSettings = "Nastavenia úÄtu" @@ -1427,6 +1429,34 @@ title = "Spracovanie" description = "Maximálny Äas Äakania na spracovanie úlohy pred nahlásením chyby." label = "ÄŒasový limit spracovania (sekundy)" +[admin.settings.storage] +description = "Spravujte úložisko servera a možnosti zdieľania." +title = "Úložisko súborov a zdieľanie" + +[admin.settings.storage.enabled] +description = "UmožniÅ¥ používateľom ukladaÅ¥ súbory na server." +label = "PovoliÅ¥ serverové úložisko súborov" + +[admin.settings.storage.sharing.email] +description = "UmožniÅ¥ zdieľanie s e‑mailovými adresami." +label = "PovoliÅ¥ zdieľanie e‑mailom" +mailLink = "KonfigurovaÅ¥ nastavenia poÅ¡ty" +mailNote = "Vyžaduje konfiguráciu poÅ¡ty. " + +[admin.settings.storage.sharing.enabled] +description = "UmožniÅ¥ používateľom zdieľaÅ¥ uložené súbory." +label = "PovoliÅ¥ zdieľanie" + +[admin.settings.storage.sharing.links] +description = "UmožniÅ¥ zdieľanie cez odkazy pre prihlásených používateľov." +frontendUrlLink = "NastaviÅ¥ v systémových nastaveniach" +frontendUrlNote = "Vyžaduje Frontend URL. " +label = "PovoliÅ¥ zdieľacie odkazy" + +[admin.settings.storage.signing.enabled] +description = "UmožniÅ¥ používateľom vytváraÅ¥ podpisové relácie s viacerými úÄastníkmi. Vyžaduje povolené serverové úložisko súborov." +label = "PovoliÅ¥ skupinové podpisovanie (Alpha)" + [admin.settings.unsavedChanges] cancel = "PokraÄovaÅ¥ v úpravách" discard = "ZahodiÅ¥ zmeny" @@ -2059,7 +2089,19 @@ numbers = "Čísla/rozsahy: 5, 10-20" progressions = "Postupnosti: 3n, 4n+1" [certSign] +allSigned = "VÅ¡etci úÄastníci podpísali. Pripravené na finalizáciu." +awaitingSignatures = "ÄŒaká sa na podpisy" +signatureProgress = "Podpisy: {{signedCount}}/{{totalCount}}" chooseCertificate = "VybraÅ¥ súbor certifikátu" +declined = "Odmietnuté" +fetchFailed = "Nepodarilo sa naÄítaÅ¥ údaje o podpisovaní" +finalized = "Finalizované" +notified = "Oznámené" +partialNote = "Môžete finalizovaÅ¥ skôr s aktuálnymi podpismi. Nepodpísaní úÄastníci budú vylúÄení." +pending = "ÄŒaká" +readyToFinalize = "Pripravené na finalizáciu" +signed = "Podpísané" +viewed = "Zobrazené" chooseJksFile = "VybraÅ¥ súbor JKS" chooseP12File = "VybraÅ¥ súbor PKCS12" choosePfxFile = "VybraÅ¥ súbor PFX" @@ -2082,6 +2124,7 @@ title = "Podpis certifikátom" invisible = "Neviditeľný" stepTitle = "Vzhľad podpisu" visible = "Viditeľný" +visibility = "ViditeľnosÅ¥" [certSign.appearance.options] title = "Podrobnosti podpisu" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "FinalizovaÅ¥ a naÄítaÅ¥ podpísané PDF" +early = "FinalizovaÅ¥ s aktuálnymi podpismi" + +[certSign.collab.sessionDetail] +addButton = "PridaÅ¥ úÄastníkov" +addParticipants = "PridaÅ¥ úÄastníkov" +addParticipantsError = "Nepodarilo sa pridaÅ¥ úÄastníkov" +backToList = "Späť na relácie" +deleteConfirm = "Ste si istí? Toto nie je možné vrátiÅ¥ späť." +deleteError = "Nepodarilo sa odstrániÅ¥ reláciu" +deleted = "Relácia bola odstránená" +deleteSession = "OdstrániÅ¥ reláciu" +dueDate = "Termín" +finalizeError = "Nepodarilo sa finalizovaÅ¥ reláciu" +loadPdfError = "Nepodarilo sa naÄítaÅ¥ podpísané PDF" +loadSignedPdf = "NaÄítaÅ¥ podpísané PDF do aktívnych súborov" +messageLabel = "Správa" +noAdditionalInfo = "Žiadne dodatoÄné informácie" +owner = "Vlastník" +participantRemoved = "ÚÄastník odstránený" +participants = "ÚÄastníci" +participantsAdded = "ÚÄastníci boli úspeÅ¡ne pridaní" +removeParticipant = "OdstrániÅ¥" +removeParticipantError = "Nepodarilo sa odstrániÅ¥ úÄastníka" +selectUsers = "VybraÅ¥ používateľov..." +sessionInfo = "Informácie o relácii" +workbenchTitle = "Správa relácií" + +[certSign.collab.signRequest] +addedToFiles = "Dokument bol pridaný do aktívnych súborov" +addSignature = "Pridajte svoj podpis" +addToFiles = "PridaÅ¥ do aktívnych súborov" +advancedSettings = "PokroÄilé nastavenia" +backToList = "Späť na žiadosti o podpis" +certificateChoice = "Vyberte certifikát na podpis" +changeSignature = "ZmeniÅ¥ podpis" +clearSignature = "VymazaÅ¥ podpis" +completeAndSign = "DokonÄiÅ¥ a podpísaÅ¥" +createNewSignature = "VytvoriÅ¥ nový podpis" +declineButton = "OdmietnuÅ¥" +decline = "OdmietnuÅ¥ žiadosÅ¥" +deleteSelected = "OdstrániÅ¥ vybraný podpis" +drawSignature = "Nižšie nakreslite svoj podpis" +dueDate = "Termín" +fileTooLarge = "VeľkosÅ¥ súboru musí byÅ¥ menÅ¡ia ako 5 MB" +fontFamily = "Písmo" +fontSize = "VeľkosÅ¥ písma: {{size}}px" +fontSizePlaceholder = "VeľkosÅ¥" +from = "Od" +invalidCertFile = "Vyberte súbor certifikátu P12 alebo PFX" +invalidFileType = "Vyberte obrazový súbor" +location = "Poloha (voliteľné)" +locationPlaceholder = "Odkiaľ podpisujete?" +message = "Správa" +noCertificate = "Vyberte súbor certifikátu" +noSignatures = "Umiestnite aspoň jeden podpis do PDF" +p12File = "Súbor certifikátu P12/PFX" +password = "Heslo certifikátu" +passwordPlaceholder = "Zadajte heslo..." +penColor = "Farba pera" +penSize = "Hrúbka pera: {{size}}px" +placementActive = "Kliknite do PDF na umiestnenie" +placeSignatureButton = "UmiestniÅ¥ podpis do PDF" +reason = "Dôvod (voliteľné)" +reasonPlaceholder = "PreÄo podpisujete?" +removeImage = "OdstrániÅ¥ obrázok" +removeCertFile = "OdstrániÅ¥ súbor" +savedSignatures = "Uložené podpisy" +selectFile = "VybraÅ¥ obrazový súbor" +selectSignatureTitle = "VybraÅ¥ alebo vytvoriÅ¥ podpis" +signButton = "PodpísaÅ¥ dokument" +signatureInfo = "Tieto nastavenia nastavuje vlastník dokumentu" +signaturePlaced = "Podpis umiestnený na stranu" +signatureSettings = "Nastavenia podpisu" +signatureText = "Text podpisu" +signatureTextPlaceholder = "Zadajte svoje meno..." +signatureTypeLabel = "Typ podpisu" +signingTitle = "Podpisovanie" +textColor = "Farba textu" +typeSignature = "Zadajte svoje meno na vytvorenie podpisu" +uploadCert = "Vlastný certifikát" +uploadCertDesc = "Použite svoj certifikát P12/PFX" +uploadSignature = "Nahrajte obrázok svojho podpisu" +usePersonalCert = "Osobný certifikát" +usePersonalCertDesc = "Automaticky generovaný pre váš úÄet" +useServerCert = "OrganizaÄný certifikát" +useServerCertDesc = "Zdieľaný organizaÄný certifikát" +workbenchTitle = "Podpisová žiadosÅ¥" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Vyberte farbu Å¥ahu" +continue = "PokraÄovaÅ¥" + +[certSign.collab.signRequest.certModal] +description = "Umiestnili ste {{count}} podpisov. Vyberte certifikát na dokonÄenie podpisovania." +sign = "PodpísaÅ¥ dokument" +certValidating = "Overuje sa certifikát..." +certValidUntil = "Certifikát platný do {{date}}" +certInvalid = "Neplatný certifikát: {{error}}" +certInvalidFallback = "Neplatný certifikát" +certNetworkError = "Certifikát sa nepodarilo overiÅ¥" +title = "Konfigurácia certifikátu" + +[certSign.collab.signRequest.image] +hint = "Nahrajte obrázok PNG alebo JPG svojho podpisu" + +[certSign.collab.signRequest.mode] +move = "Presunúť podpis" +place = "UmiestniÅ¥ podpis" +title = "Režim podpisu alebo presunu" + +[certSign.collab.signRequest.modeTabs] +draw = "KresliÅ¥" +image = "NahraÅ¥" +text = "Text" + +[certSign.collab.signRequest.placeSignature] +message = "Kliknite na PDF, kam chcete umiestniÅ¥ svoj podpis" +title = "UmiestniÅ¥ podpis" + +[certSign.collab.signRequest.preview] +imageAlt = "Vybraný podpis" +missing = "Žiadny náhľad" +textFallback = "Podpis" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Kreslený podpis" +defaultImageLabel = "Nahraný podpis" +defaultLabel = "Podpis" +defaultTextLabel = "Písaný podpis" +delete = "OdstrániÅ¥ podpis" +none = "Žiadne uložené podpisy" + +[certSign.collab.signRequest.signatureType] +draw = "KresliÅ¥" +type = "Text" +upload = "NahraÅ¥" + +[certSign.collab.signRequest.steps] +back = "Späť" +cancelPlacement = "ZruÅ¡iÅ¥ umiestnenie" +certificate = "Certifikát" +clickMultipleTimes = "Kliknite do PDF viackrát na umiestnenie podpisov. Potiahnutím podpis presuňte alebo zmeňte jeho veľkosÅ¥." +clickToPlace = "Kliknite do PDF tam, kde chcete, aby sa váš podpis zobrazil." +continue = "PokraÄovaÅ¥ na výber certifikátu" +continueToPlacement = "PokraÄovaÅ¥ na umiestnenie" +continueToReview = "PokraÄovaÅ¥ na kontrolu" +createSignature = "VytvoriÅ¥ podpis" +invisible = "Neviditeľné" +location = "Poloha:" +multipleSignatures = "{{count}} podpisov bude pridaných do PDF" +oneSignature = "1 podpis bude pridaný do PDF" +placeOnPdf = "UmiestniÅ¥ do PDF" +reason = "Dôvod:" +reviewTitle = "Kontrola pred podpísaním" +signaturePlaced = "Podpis umiestnený na strane {{page}}. Polohu môžete upraviÅ¥ opätovným kliknutím alebo pokraÄovaÅ¥ na kontrolu." +visible = "Viditeľné" +visibility = "ViditeľnosÅ¥:" +yourSignatures = "VaÅ¡e podpisy ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Farba" +fontLabel = "Písmo" +fontSizeLabel = "VeľkosÅ¥" +fontSizePlaceholder = "16" +label = "Text podpisu" +modalHint = "Zadajte svoje meno a kliknite na PokraÄovaÅ¥, aby ste ho umiestnili do PDF." +placeholder = "Zadajte svoje meno..." + +[certSign.collab.participant] +certValidating = "Overuje sa certifikát..." +certValid = "✓ Platný certifikát" +certValidUntil = " do {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Neplatný certifikát" +certNetworkError = "Certifikát sa nepodarilo overiÅ¥" + +[certSign.collab.addParticipants] +add = "PridaÅ¥ {{count}} úÄastníka/ov" +back = "Späť" +configureSignatures = "NastaviÅ¥ parametre podpisu" +continue = "PokraÄovaÅ¥ na nastavenia podpisu" +reasonHelp = "Prednastavte dôvod podpisu pre týchto úÄastníkov (voliteľné, pri podpisovaní ho môžu zmeniÅ¥)" +reasonPlaceholder = "napr. Schválenie, Kontrola..." +selectUsers = "VybraÅ¥ používateľov" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Zahrnúť súhrnnú stranu podpisov" +includeSummaryPageHelp = "Na koniec bude pridaná súhrnná strana so vÅ¡etkými metadátami podpisov. Rámiky digitálnych certifikátov na jednotlivých stranách budú potlaÄené (vlastnoruÄné podpisy nie sú ovplyvnené)." + +[certSign.collab.sessionList] +active = "Aktívne" +finalized = "Finalizované" + +[certSign.collab.signatureSettings] +description = "Nakonfigurujte, ako sa budú podpisy zobrazovaÅ¥ vÅ¡etkým úÄastníkom" +title = "Vzhľad podpisu" + +[certSign.collab.userSelector] +inviteUsers = "PridaÅ¥ používateľov" +loadError = "Nepodarilo sa naÄítaÅ¥ používateľov" +noTeam = "Bez tímu" +noUsers = "NenaÅ¡li sa žiadni Äalší používatelia." +placeholder = "VybraÅ¥ používateľov..." + +[certSign.mobile] +panelActions = "Akcie" +panelDocument = "Dokument" +panelPeople = "Ľudia" + +[certSign.sessions] +deleted = "Relácia bola odstránená" +fetchFailed = "Nepodarilo sa naÄítaÅ¥ podrobnosti relácie" +finalized = "Relácia bola finalizovaná" +loaded = "Podpísané PDF naÄítané" +pdfNotReady = "PDF nie je pripravené" +pdfNotReadyDesc = "Podpísané PDF sa generuje. Skúste to znova o chvíľu." + +[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" + [changeCreds] changePassword = "Používate predvolené prihlasovacie údaje. Prosím, zadajte nové heslo" changeUsername = "Aktualizujte svoje používateľské meno. Po aktualizácii budete odhlásení." @@ -3242,6 +3531,46 @@ totalSelected = "Celkom vybrané" unsupported = "Nepodporované" unzip = "RozbaliÅ¥" uploadError = "Niektoré súbory sa nepodarilo nahraÅ¥." +copyCreated = "Kópia uložená do tohto zariadenia." +copyFailed = "Kópiu sa nepodarilo vytvoriÅ¥." +leaveShare = "OdstrániÅ¥ z môjho zoznamu" +leaveShareFailed = "Zdieľaný súbor sa nepodarilo odstrániÅ¥." +leaveShareSuccess = "Odstránené z vášho zoznamu zdieľaných." +removeBoth = "OdstrániÅ¥ z oboch" +removeFilePrompt = "Tento súbor je uložený v tomto zariadení aj na vaÅ¡om serveri. Odkiaľ ho chcete odstrániÅ¥?" +removeFileTitle = "OdstrániÅ¥ súbor" +removeLocalOnly = "Len z tohto zariadenia" +removeServerFailed = "Súbor sa nepodarilo odstrániÅ¥ zo servera." +removeServerOnly = "Len zo servera" +removeServerOnlyPrompt = "Tento súbor je uložený len na vaÅ¡om serveri. Chcete ho odstrániÅ¥ zo servera?" +removeServerSuccess = "Odstránené zo servera." +removeSharedPrompt = "Tento súbor je s vami zdieľaný. Môžete ho odstrániÅ¥ z tohto zariadenia alebo zo svojho zdieľaného zoznamu." +removeSharedServerOnlyBlockedPrompt = "Tento súbor je s vami zdieľaný a je uložený len na serveri." +removeSharedServerOnlyPrompt = "Tento súbor je s vami zdieľaný a je uložený len na serveri. OdstrániÅ¥ ho zo zoznamu?" +changesNotUploaded = "Zmeny neboli nahrané" +cloudFile = "Cloudový súbor" +filterAll = "VÅ¡etko" +filterLocal = "Lokálne" +filterSharedByMe = "Zdieľané mnou" +filterSharedWithMe = "Zdieľané so mnou" +lastSynced = "Naposledy synchronizované" +localOnly = "Len lokálne" +makeCopy = "VytvoriÅ¥ kópiu" +owner = "Vlastník" +ownerUnknown = "Neznámy" +share = "ZdieľaÅ¥" +shareSelected = "ZdieľaÅ¥ vybrané" +sharedByYou = "Zdieľané vami" +sharedEditNoticeBody = "Nemáte práva na úpravu serverovej verzie tohto súboru. VÅ¡etky úpravy sa uložia ako lokálna kópia." +sharedEditNoticeConfirm = "Rozumiem" +sharedEditNoticeTitle = "Serverová kópia len na Äítanie" +sharedWithYou = "Zdieľané s vami" +sharing = "Zdieľanie" +storageState = "Úložisko" +synced = "Synchronizované" +updateOnServer = "AktualizovaÅ¥ na serveri" +uploadSelected = "NahraÅ¥ vybrané" +uploadToServer = "NahraÅ¥ na server" [files] addFiles = "PridaÅ¥ súbory" @@ -3367,6 +3696,77 @@ title = "O splošťovaní PDF" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "O skupinovom podpisovaní" + +[groupSigning.tooltip.finalization] +bullet1 = "VÅ¡etky podpisy sa aplikujú v poradí úÄastníkov, ktoré ste urÄili" +bullet2 = "V prípade potreby môžete finalizovaÅ¥ aj s ÄiastoÄnými podpismi" +bullet3 = "Po finalizácii už reláciu nemožno upravovaÅ¥" +description = "KeÄ vÅ¡etci úÄastníci podpíšu (alebo sa rozhodnete finalizovaÅ¥ skôr), môžete vygenerovaÅ¥ finálne podpísané PDF." +title = "Proces finalizácie" + +[groupSigning.tooltip.roles] +bullet1 = "Vlastník (vy): vytvára reláciu, nastavuje predvolené hodnoty podpisu, finalizuje dokument" +bullet2 = "ÚÄastníci: vytvoria svoj podpis, zvolia certifikát, umiestnia ho do PDF" +bullet3 = "ÚÄastníci nemôžu meniÅ¥ nastavenia viditeľnosti podpisu, dôvodu ani polohy" +description = "Nastavenia vzhľadu podpisu riadite pre vÅ¡etkých úÄastníkov." +title = "Roly úÄastníkov" + +[groupSigning.tooltip.sequential] +bullet1 = "Prvý úÄastník musí podpísaÅ¥, aby mal druhý prístup k dokumentu" +bullet2 = "ZabezpeÄuje správne poradie podpisov pre právny súlad" +bullet3 = "ÚÄastníkov môžete preusporiadaÅ¥ Å¥ahaním v zozname" +description = "ÚÄastníci podpisujú dokumenty v poradí, ktoré urÄíte. Každý podpisovateľ dostane upozornenie, keÄ je na rade." +title = "Postupné podpisovanie" + +[groupSigning.steps] +back = "Späť" +completed = "DokonÄené" +current = "Aktuálne" +stepLabel = "Krok {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "PokraÄovaÅ¥ na kontrolu" +invisible = "Podpisy budú neviditeľné (len metadáta)" +locationLabel = "Poloha:" +preview = "Náhľad" +reasonLabel = "Dôvod:" +title = "NastaviÅ¥ parametre podpisu" +visible = "Podpisy budú viditeľné na strane {{page}}" + +[groupSigning.steps.review] +document = "Dokument" +dueDate = "Termín (voliteľné)" +dueDatePlaceholder = "Vyberte termín..." +invisible = "Neviditeľné (len metadáta)" +location = "Poloha:" +logo = "Logo:" +logoHidden = "Bez loga" +logoShown = "Zobrazené logo Stirling PDF" +participants = "ÚÄastníci" +reason = "Dôvod:" +send = "OdoslaÅ¥ žiadosti o podpis" +signatureSettings = "Nastavenia podpisu" +title = "SkontrolovaÅ¥ podrobnosti relácie" +titleShort = "SkontrolovaÅ¥ a odoslaÅ¥" +visibility = "ViditeľnosÅ¥:" +visible = "Viditeľné na strane {{page}}" +participantCount = "{{count}} úÄastníkov bude podpisovaÅ¥ v poradí" + +[groupSigning.steps.selectDocument] +continue = "PokraÄovaÅ¥ na výber úÄastníkov" +noFile = "Vyberte jeden súbor PDF zo svojich aktívnych súborov na vytvorenie podpisovej relácie." +selectedFile = "Vybraný dokument" +title = "VybraÅ¥ dokument" + +[groupSigning.steps.selectParticipants] +continue = "PokraÄovaÅ¥ na nastavenia podpisu" +count = "{{count}} vybraných úÄastníkov" +label = "VybraÅ¥ úÄastníkov" +placeholder = "Vyberte úÄastníkov na podpis..." +title = "VybraÅ¥ úÄastníkov" + [getPdfInfo] downloadJson = "StiahnuÅ¥ JSON" downloads = "SÅ¥ahovania" @@ -4460,7 +4860,10 @@ zoomOut = "OddialiÅ¥" [viewer] cannotPreviewFile = "Nedá sa zobraziÅ¥ náhľad súboru" +disableColorFilter = "ZakázaÅ¥ farebný filter" dualPageView = "Dvojstranové zobrazenie" +enableDarkFilter = "PovoliÅ¥ tmavý filter" +enableSepiaFilter = "PovoliÅ¥ sépiový filter" firstPage = "Prvá strana" lastPage = "Posledná strana" nextPage = "Nasledujúca strana" @@ -4470,6 +4873,22 @@ singlePageView = "Zobrazenie jednej strany" unknownFile = "Neznámy súbor" zoomIn = "PriblížiÅ¥" zoomOut = "OddialiÅ¥" +resetZoom = "ObnoviÅ¥ priblíženie" + +[viewer.nonPdf] +fileTypeBadge = "{{type}} súbor" +convertToPdf = "KonvertovaÅ¥ do PDF" +loading = "NaÄítava sa..." +emptyFile = "Prázdny súbor" +csvStats = "{{rows}} riadkov · {{columns}} stĺpcov · {{size}}" +sortedBy = "Zoradené podľa: {{column}}" +columnDefault = "Stĺpec {{index}}" +htmlPreviewWarning = "Náhľad HTML — externé zdroje sa nemusia naÄítaÅ¥ · {{size}}" +htmlPreview = "Náhľad HTML" +invalidJson = "Neplatný JSON — zobrazuje sa surový obsah" +textStats = "{{lines}} riadkov · {{size}}" +lineNumbers = "Čísla riadkov" +renderMarkdown = "ZobraziÅ¥ Markdown" [viewer.attachments] title = "Prílohy" @@ -4531,6 +4950,7 @@ toggleAttachments = "ZobraziÅ¥/skryÅ¥ prílohy" toggleTheme = "Prepnúť tému" language = "Jazyk" toggleAnnotations = "Prepnúť zobrazenie anotácií" +toggleLayers = "Prepnúť vrstvy" search = "HľadaÅ¥ v PDF" panMode = "Režim posunu" applyRedactionsFirst = "Najprv použite zaÄiernenia" @@ -5407,20 +5827,72 @@ title = "VytlaÄiÅ¥ súbor" 2 = "Zadajte názov tlaÄiarne" [quickAccess] +access = "Prístup" +accessAddPerson = "PridaÅ¥ ÄalÅ¡iu osobu" +accessBack = "Späť" +accessCopyLink = "KopírovaÅ¥ odkaz" +accessEmail = "E‑mailová adresa" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Súbor" +accessGeneral = "VÅ¡eobecný prístup" +accessInviteTitle = "PozvaÅ¥ ľudí" +accessOwner = "Vlastník" +accessPanel = "Prístup k dokumentu" +accessPeople = "Používatelia s prístupom" +accessRemove = "OdstrániÅ¥" +accessRestricted = "Obmedzené" +accessRestrictedHint = "OtvoriÅ¥ môžu len osoby s prístupom" +accessRole = "Rola" +accessRoleCommenter = "Komentujúci" +accessRoleEditor = "Editor" +accessRoleViewer = "ÄŒitateľ" +accessSelectedFile = "Vybraný súbor" +accessSendInvite = "OdoslaÅ¥ pozvánku" +accessTitle = "Prístup k dokumentu" +accessYou = "Vy" account = "ÚÄet" +activeSessions = "Aktívne relácie" +activeTab = "Aktívne" activity = "Aktivita" adminSettings = "Admin nast." +allSessions = "VÅ¡etky relácie" allTools = "All Tools" automate = "Auto" +back = "Späť" +certSign = "Podpis certifikátom" +completedSessions = "DokonÄené relácie" +completedTab = "DokonÄené" config = "Konfig" +createNew = "VytvoriÅ¥ novú žiadosÅ¥" +createSession = "VytvoriÅ¥ žiadosÅ¥ o podpis" +dueDate = "Termín (voliteľné)" files = "Súbory" help = "Pomoc" +noActiveSessions = "Žiadne Äakajúce žiadosti o podpis ani aktívne relácie" +noCompletedSessions = "Žiadne dokonÄené relácie" +noFile = "Nie je vybraný žiadny súbor" read = "ČítaÅ¥" reader = "ČítaÄka" +refresh = "ObnoviÅ¥" +requestSignatures = "PožiadaÅ¥ o podpisy" +selectSingleFileToRequest = "Vyberte jeden súbor PDF na vyžiadanie podpisov" +selectedFile = "Vybraný súbor" +selectUsers = "Vyberte používateľov na podpis" +selectUsersPlaceholder = "Vyberte úÄastníkov..." +sendingRequest = "Odosiela sa..." settings = "Nast." showMeAround = "PreviesÅ¥ ma" sign = "PodpísaÅ¥" +signatureRequests = "Žiadosti o podpis" +signYourself = "PodpísaÅ¥ sám/sama" +newRequest = "Nová žiadosÅ¥" tours = "Prehliadky" +wetSign = "PridaÅ¥ podpis" +filterMine = "Moje" +filterOverdue = "Po termíne" +filterSigned = "Podpísané" +filterDeclined = "Odmietnuté" +searchDocuments = "HľadaÅ¥ dokumenty…" [quickAccess.helpMenu] adminTour = "Prehliadka administrácie" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Váš server Stirling-PDF je offline a \"{{endpoint}} expired = "VaÅ¡a relácia vyprÅ¡ala. Obnovte stránku a skúste znova." refreshPage = "ObnoviÅ¥ stránku" +[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" + +[signatureSettings.tooltip] +header = "Nastavenia vzhľadu podpisu" + +[signatureSettings.tooltip.location] +bullet1 = "Príklady: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Nie je to isté ako pozícia na strane" +bullet3 = "Môže byÅ¥ vyžadované v niektorých právnych jurisdikciách" +description = "Voliteľná geografická poloha, kde bol podpis aplikovaný. Uložené v metadátach certifikátu." +title = "Umiestnenie podpisu" + +[signatureSettings.tooltip.logo] +bullet1 = "Zobrazuje sa popri podpise a texte" +bullet2 = "Podporované formáty PNG, JPG" +bullet3 = "ZvyÅ¡uje profesionálny vzhľad" +description = "Pridajte k viditeľným podpisom firemné logo pre branding a autenticitu." +title = "Firemné logo" + +[signatureSettings.tooltip.reason] +bullet1 = "Príklady: \"Approval\", \"Contract Agreement\", \"Review Complete\"" +bullet2 = "Viditeľné vo vlastnostiach podpisu PDF" +bullet3 = "UžitoÄné pre audit a compliance" +description = "Voliteľný text vysvetľujúci, preÄo sa dokument podpisuje. Uložené v metadátach certifikátu." +title = "Dôvod podpisu" + +[signatureSettings.tooltip.visibility] +bullet1 = "Viditeľné: Podpis sa zobrazí v PDF s vlastným vzhľadom" +bullet2 = "Neviditeľné: Certifikát je vložený bez vizuálnej stopy" +bullet3 = "Neviditeľné podpisy stále poskytujú kryptografické overenie" +description = "UrÄuje, Äi je podpis na dokumente viditeľný alebo vložený neviditeľne." +title = "ViditeľnosÅ¥ podpisu" + [settings.configuration] advanced = "PokroÄilé" database = "Databáza" endpoints = "Endpointy" features = "Funkcie" +storageSharing = "Úložisko súborov a zdieľanie" systemSettings = "Systémové nastavenia" title = "Konfigurácia" @@ -6332,10 +6868,13 @@ title = "Prihláste sa do Stirling" [setup.selfhosted] link = "alebo sa pripojte k self-hosted úÄtu" subtitle = "Zadajte prihlasovacie údaje k serveru" +changeServerLocked = "VaÅ¡a organizácia obmedzila túto aplikáciu na konkrétny server" switchToLocal = "Namiesto toho použiÅ¥ lokálne nástroje" title = "PrihlásiÅ¥ sa na server" [setup.selfhosted.unreachable] +changeServer = "PripojiÅ¥ sa k inému serveru" +changeServerLocked = "VaÅ¡a organizácia obmedzila túto aplikáciu na konkrétny server" continueOffline = "Namiesto toho použiÅ¥ lokálne nástroje" message = "Nepodarilo sa dosiahnuÅ¥ {{url}}. Skontrolujte, Äi server beží a je prístupný." retry = "SkúsiÅ¥ znova" @@ -6529,6 +7068,15 @@ saved = "Uložené" text = "Text" title = "Typ podpisu" +[signRequest] +declined = "ŽiadosÅ¥ o podpis bola odmietnutá" +fetchFailed = "Nepodarilo sa naÄítaÅ¥ žiadosÅ¥ o podpis" +signed = "Dokument bol úspeÅ¡ne podpísaný" + +[signSession] +createFailed = "Nepodarilo sa vytvoriÅ¥ žiadosÅ¥ o podpis" +created = "ŽiadosÅ¥ o podpis bola odoslaná" + [signup] accountCreatedSuccessfully = "ÚÄet bol úspeÅ¡ne vytvorený! Teraz sa môžete prihlásiÅ¥." alreadyHaveAccount = "Už máte úÄet? Prihláste sa" @@ -6807,6 +7355,106 @@ title = "RozdeliÅ¥ PDF podľa kapitol" [splitPdfByChapters] tags = "rozdeliÅ¥,kapitoly,záložky,organizovaÅ¥" +[storageShare] +accessed = "Otvorené" +accessDenied = "K tomuto zdieľanému súboru nemáte prístup. Požiadajte vlastníka, aby ho s vami zdieľal." +accessFailed = "Aktivitu sa nepodarilo naÄítaÅ¥." +accessDeniedBody = "K tomuto súboru nemáte prístup. Požiadajte vlastníka, aby ho s vami zdieľal." +accessDeniedTitle = "Bez prístupu" +accessLimitedCommenter = "Prístup na komentovanie Äoskoro pribudne. Ak potrebujete sÅ¥ahovaÅ¥, požiadajte vlastníka o oprávnenie editora." +accessLimitedTitle = "Obmedzený prístup" +accessLimitedViewer = "Tento odkaz je len na zobrazenie. Ak potrebujete sÅ¥ahovaÅ¥, požiadajte vlastníka o oprávnenie editora." +createdAt = "Vytvorené" +download = "StiahnuÅ¥" +downloadFailed = "Nepodarilo sa stiahnuÅ¥ tento súbor." +expiredBody = "Tento zdieľací odkaz je neplatný alebo vyprÅ¡al." +expiredTitle = "Odkaz vyprÅ¡al" +goToLogin = "PrejsÅ¥ na prihlásenie" +loadFailed = "Zdieľaný súbor sa nepodarilo otvoriÅ¥." +loading = "NaÄítava sa zdieľací odkaz..." +loginPrompt = "Na prístup k tomuto zdieľanému súboru sa prihláste." +loginRequired = "Vyžaduje sa prihlásenie" +openInApp = "OtvoriÅ¥ v Stirling PDF" +ownerLabel = "Vlastník" +ownerUnknown = "Neznámy" +requiresLogin = "Tento zdieľaný súbor vyžaduje prihlásenie." +roleCommenter = "Komentujúci" +roleEditor = "Editor" +roleViewer = "ÄŒitateľ" +shareHeading = "Zdieľaný súbor" +titleDefault = "Zdieľaný súbor" +tryAgain = "Skúste to znova neskôr." +addUser = "PridaÅ¥" +commenterHint = "Komentovanie Äoskoro pribudne." +copied = "Odkaz skopírovaný do schránky" +copy = "KopírovaÅ¥" +copyFailed = "Kopírovanie zlyhalo" +description = "Vytvorte pre tento súbor zdieľací odkaz. Prihlásení používatelia s odkazom k nemu budú maÅ¥ prístup." +downloadsCount = "Stiahnutia: {{count}}" +emailWarningBody = "Vyzerá to ako e‑mailová adresa. Ak táto osoba eÅ¡te nie je používateľom Stirling PDF, k súboru nebude maÅ¥ prístup." +emailWarningConfirm = "Aj tak zdieľaÅ¥" +emailWarningTitle = "E‑mailová adresa" +errorTitle = "Zdieľanie zlyhalo" +failure = "Zdieľací odkaz sa nepodarilo vygenerovaÅ¥. Skúste to znova." +fileLabel = "Súbor" +generate = "VygenerovaÅ¥ odkaz" +generated = "Zdieľací odkaz bol vygenerovaný" +hideActivity = "SkryÅ¥ aktivitu" +invalidUsername = "Zadajte platné používateľské meno alebo e‑mailovú adresu." +lastAccessed = "Naposledy otvorené" +linkAccessTitle = "Prístup cez zdieľací odkaz" +linkLabel = "Zdieľací odkaz" +linksDisabled = "Zdieľacie odkazy sú zakázané." +linksDisabledBody = "Zdieľacie odkazy sú zakázané nastaveniami vášho servera." +manage = "SpravovaÅ¥ zdieľanie" +manageDescription = "Vytvárajte a spravujte odkazy na zdieľanie tohto súboru." +manageLoadFailed = "Zdieľacie odkazy sa nepodarilo naÄítaÅ¥." +manageTitle = "Správa zdieľania" +noActivity = "Zatiaľ žiadna aktivita." +noLinks = "Zatiaľ žiadne aktívne zdieľacie odkazy." +noSharedUsers = "Zatiaľ nemá prístup žiadny používateľ." +removeLink = "OdstrániÅ¥ odkaz" +removeUser = "OdstrániÅ¥" +revokeFailed = "Zdieľací odkaz sa nepodarilo odstrániÅ¥." +revoked = "Odkaz na zdieľanie odstránený" +roleLabel = "Rola" +sharingDisabled = "Zdieľanie je zakázané." +sharingDisabledBody = "Zdieľanie bolo zakázané nastaveniami servera." +sharedUsersTitle = "Zdieľaní používatelia" +title = "ZdieľaÅ¥ súbor" +unknownUser = "Neznámy používateľ" +userAddFailed = "Nepodarilo sa zdieľaÅ¥ s týmto používateľom." +userAdded = "Používateľ pridaný do zoznamu zdieľania." +usernameLabel = "Používateľské meno alebo e-mail" +usernamePlaceholder = "Zadajte používateľské meno alebo e-mail" +userRemoveFailed = "Nepodarilo sa odstrániÅ¥ tohto používateľa." +userRemoved = "Používateľ odstránený zo zoznamu zdieľania." +viewActivity = "ZobraziÅ¥ aktivitu" +viewed = "Zobrazené" +viewsCount = "Zobrazenia: {{count}}" +downloaded = "Stiahnuté" +bulkDescription = "Vytvorte jeden odkaz na zdieľanie vÅ¡etkých vybraných súborov s prihlásenými používateľmi." +bulkTitle = "ZdieľaÅ¥ vybrané súbory" +copyLink = "KopírovaÅ¥ odkaz na zdieľanie" +fileCount = "{{count}} vybraných súborov" +ownerOnly = "Zdieľanie môže spravovaÅ¥ iba vlastník." +selectSingleFile = "Na správu zdieľania vyberte jeden súbor." + +[storageUpload] +description = "Týmto nahráte aktuálny súbor do serverového úložiska pre vlastný prístup." +errorTitle = "Nahrávanie zlyhalo" +failure = "Nahrávanie zlyhalo. Skontrolujte svoje prihlásenie a nastavenia úložiska." +fileLabel = "Súbor" +hint = "Verejné odkazy a režimy prístupu sú riadené nastaveniami vášho servera." +success = "Nahraté na server" +title = "NahraÅ¥ na server" +updateButton = "AktualizovaÅ¥ na serveri" +uploadButton = "NahraÅ¥ na server" +bulkDescription = "Týmto nahráte vybrané súbory do serverového úložiska." +bulkTitle = "NahraÅ¥ vybrané súbory" +fileCount = "{{count}} vybraných súborov" +more = " +{{count}} Äalších" + [storage] approximateSize = "Približná veľkosÅ¥" fileTooLarge = "Súbor je príliÅ¡ veľký. Maximálna veľkosÅ¥ na súbor je" @@ -7153,6 +7801,30 @@ title = "ZobraziÅ¥/UpraviÅ¥ PDF" [warning] tooltipTitle = "Upozornenie" +[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" + [watermark] completed = "Vodoznak pridaný" desc = "Pridajte textové alebo obrázkové vodoznaky do súborov PDF" @@ -7333,6 +8005,7 @@ activeSession = "Aktívna relácia" addMembers = "PridaÅ¥ Älenov" admin = "Admin" confirmDelete = "Naozaj chcete odstrániÅ¥ tohto používateľa? Táto akcia sa nedá vrátiÅ¥." +confirmUnlock = "Naozaj chcete odomknúť tento používateľský úÄet?" deleteUser = "OdstrániÅ¥ používateľa" deleteUserError = "Používateľa sa nepodarilo odstrániÅ¥" deleteUserSuccess = "Používateľ úspeÅ¡ne odstránený" @@ -7341,6 +8014,8 @@ disable = "ZakázaÅ¥" disabled = "Zakázaný" editRole = "UpraviÅ¥ rolu" enable = "PovoliÅ¥" +locked = "zamknutý" +lockedBadge = "Zamknutý" loading = "NaÄítavajú sa ľudia..." loginRequired = "Najprv zapnite režim prihlásenia" member = "ÄŒlen" @@ -7350,6 +8025,9 @@ searchMembers = "HľadaÅ¥ Älenov..." status = "Stav" team = "Tím" title = "Ľudia" +unlockAccount = "Odomknúť úÄet" +unlockUserError = "Nepodarilo sa odomknúť používateľský úÄet" +unlockUserSuccess = "Používateľský úÄet bol úspeÅ¡ne odomknutý" user = "Používateľ" [workspace.people.actions] diff --git a/frontend/public/locales/sl-SI/translation.toml b/frontend/public/locales/sl-SI/translation.toml index 702dc56428..eea64f09c6 100644 --- a/frontend/public/locales/sl-SI/translation.toml +++ b/frontend/public/locales/sl-SI/translation.toml @@ -8,6 +8,7 @@ black = "Ärna" blue = "modra" bored = "dolgoÄaseno Äakanje?" cancel = "PrekliÄi" +confirm = "Potrdi" changedCredsMessage = "Poverilnice spremenjene!" chooseFile = "Izberi datoteko" close = "Zapri" @@ -146,6 +147,7 @@ insufficientCredits = "Premalo kreditov. Zahtevano: {{requiredCredits}}, Na volj loadingCredits = "Preverjanje kreditov..." loadingProStatus = "Preverjanje stanja naroÄnine..." noticeTopUpOrPlan = "Ni dovolj kreditov, kupite dodatne kredite ali nadgradite na paket" +accessInvite = "Povabi" [account] accountSettings = "Nastavitve raÄuna" @@ -1427,6 +1429,34 @@ title = "Obdelava" description = "NajdaljÅ¡i Äas Äakanja na opravilo obdelave, preden se prijavi napaka." label = "ÄŒasovna omejitev obdelave (sekunde)" +[admin.settings.storage] +description = "Nadzirajte shrambo strežnika in možnosti skupne rabe." +title = "Shramba datotek in skupna raba" + +[admin.settings.storage.enabled] +description = "Dovoli uporabnikom shranjevanje datotek na strežniku." +label = "OmogoÄi shranjevanje datotek na strežniku" + +[admin.settings.storage.sharing.email] +description = "Dovoli skupno rabo z e-poÅ¡tnimi naslovi." +label = "OmogoÄi skupno rabo prek e-poÅ¡te" +mailLink = "Nastavi nastavitve e-poÅ¡te" +mailNote = "Zahteva konfiguracijo e-poÅ¡te. " + +[admin.settings.storage.sharing.enabled] +description = "Dovoli uporabnikom deliti shranjene datoteke." +label = "OmogoÄi skupno rabo" + +[admin.settings.storage.sharing.links] +description = "Dovoli skupno rabo prek povezav, ki zahtevajo prijavo." +frontendUrlLink = "Nastavi v sistemskih nastavitvah" +frontendUrlNote = "Zahteva Frontend URL. " +label = "OmogoÄi povezave za skupno rabo" + +[admin.settings.storage.signing.enabled] +description = "Dovoli uporabnikom ustvariti seje podpisovanja dokumentov z veÄ udeleženci. Zahteva omogoÄeno shranjevanje datotek na strežniku." +label = "OmogoÄi skupinsko podpisovanje (Alpha)" + [admin.settings.unsavedChanges] cancel = "Nadaljujte z urejanjem" discard = "Zavrzite spremembe" @@ -2059,7 +2089,19 @@ numbers = "Å tevilke/obsegi: 5, 10-20" progressions = "Zaporedja: 3n, 4n+1" [certSign] +allSigned = "Vsi udeleženci so podpisali. Pripravljeno za zakljuÄek." +awaitingSignatures = "ÄŒakanje na podpise" +signatureProgress = "{{signedCount}}/{{totalCount}} podpisov" chooseCertificate = "Izberite datoteko potrdila" +declined = "Zavrnjeno" +fetchFailed = "Podatkov o podpisovanju ni bilo mogoÄe naložiti" +finalized = "ZakljuÄeno" +notified = "Na Äakanju" +partialNote = "Sejo lahko predÄasno zakljuÄite z obstojeÄimi podpisi. Nepodpisani udeleženci bodo izkljuÄeni." +pending = "Na Äakanju" +readyToFinalize = "Pripravljeno za zakljuÄek" +signed = "Podpisano" +viewed = "Ogledano" chooseJksFile = "Izberite datoteko JKS" chooseP12File = "Izberite datoteko PKCS12" choosePfxFile = "Izberite datoteko PFX" @@ -2082,6 +2124,7 @@ title = "Podpisovanje potrdila" invisible = "Neviden" stepTitle = "Videz podpisa" visible = "Viden" +visibility = "Vidnost" [certSign.appearance.options] title = "Podrobnosti podpisa" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "ZakljuÄi in naloži podpisan PDF" +early = "ZakljuÄi z obstojeÄimi podpisi" + +[certSign.collab.sessionDetail] +addButton = "Dodaj udeležence" +addParticipants = "Dodaj udeležence" +addParticipantsError = "Udeležencev ni bilo mogoÄe dodati" +backToList = "Nazaj na seje" +deleteConfirm = "Ali ste prepriÄani? Tega dejanja ni mogoÄe razveljaviti." +deleteError = "Seje ni bilo mogoÄe izbrisati" +deleted = "Seja izbrisana" +deleteSession = "IzbriÅ¡i sejo" +dueDate = "Rok" +finalizeError = "Seje ni bilo mogoÄe zakljuÄiti" +loadPdfError = "Podpisanega PDF ni bilo mogoÄe naložiti" +loadSignedPdf = "Naloži podpisan PDF v aktivne datoteke" +messageLabel = "SporoÄilo" +noAdditionalInfo = "Ni dodatnih informacij" +owner = "Lastnik" +participantRemoved = "Udeleženec odstranjen" +participants = "Udeleženci" +participantsAdded = "Udeleženci uspeÅ¡no dodani" +removeParticipant = "Odstrani" +removeParticipantError = "Udeleženca ni bilo mogoÄe odstraniti" +selectUsers = "Izberite uporabnike..." +sessionInfo = "Podatki o seji" +workbenchTitle = "Upravljanje seje" + +[certSign.collab.signRequest] +addedToFiles = "Dokument dodan v aktivne datoteke" +addSignature = "Dodajte svoj podpis" +addToFiles = "Dodaj v aktivne datoteke" +advancedSettings = "Napredne nastavitve" +backToList = "Nazaj na zahteve za podpis" +certificateChoice = "Izberite potrdilo za podpis" +changeSignature = "Spremeni podpis" +clearSignature = "PoÄisti podpis" +completeAndSign = "DokonÄaj in podpiÅ¡i" +createNewSignature = "Ustvari nov podpis" +declineButton = "Zavrni" +decline = "Zavrni zahtevo" +deleteSelected = "IzbriÅ¡i izbrani podpis" +drawSignature = "Spodaj nariÅ¡ite svoj podpis" +dueDate = "Rok" +fileTooLarge = "Velikost datoteke mora biti manjÅ¡a od 5 MB" +fontFamily = "Družina pisav" +fontSize = "Velikost pisave: {{size}}px" +fontSizePlaceholder = "Velikost" +from = "Od" +invalidCertFile = "Izberite datoteko potrdila P12 ali PFX" +invalidFileType = "Izberite slikovno datoteko" +location = "Lokacija (neobvezno)" +locationPlaceholder = "Od kod podpisujete?" +message = "SporoÄilo" +noCertificate = "Izberite datoteko potrdila" +noSignatures = "Na PDF postavite vsaj en podpis" +p12File = "Datoteka potrdila P12/PFX" +password = "Geslo potrdila" +passwordPlaceholder = "Vnesite geslo..." +penColor = "Barva poteze" +penSize = "Debelina poteze: {{size}}px" +placementActive = "Kliknite PDF za postavitev" +placeSignatureButton = "Postavi podpis na PDF" +reason = "Razlog (neobvezno)" +reasonPlaceholder = "Zakaj podpisujete?" +removeImage = "Odstrani sliko" +removeCertFile = "Odstrani datoteko" +savedSignatures = "Shranjeni podpisi" +selectFile = "Izberi slikovno datoteko" +selectSignatureTitle = "Izberite ali ustvarite podpis" +signButton = "PodpiÅ¡i dokument" +signatureInfo = "Te nastavitve doloÄi lastnik dokumenta" +signaturePlaced = "Podpis je postavljen na strani" +signatureSettings = "Nastavitve podpisa" +signatureText = "Besedilo podpisa" +signatureTextPlaceholder = "Vnesite svoje ime..." +signatureTypeLabel = "Vrsta podpisa" +signingTitle = "Podpisovanje" +textColor = "Barva besedila" +typeSignature = "Vnesite svoje ime za ustvaritev podpisa" +uploadCert = "Lastno potrdilo" +uploadCertDesc = "Uporabite svoje potrdilo P12/PFX" +uploadSignature = "Naložite sliko svojega podpisa" +usePersonalCert = "Osebno potrdilo" +usePersonalCertDesc = "Samodejno ustvarjeno za vaÅ¡ raÄun" +useServerCert = "Organizacijsko potrdilo" +useServerCertDesc = "Skupno organizacijsko potrdilo" +workbenchTitle = "Zahteva za podpis" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Izberite barvo poteze" +continue = "Nadaljuj" + +[certSign.collab.signRequest.certModal] +description = "Postavili ste {{count}} podpis(ov). Za dokonÄanje podpisa izberite svoje potrdilo." +sign = "PodpiÅ¡i dokument" +certValidating = "Preverjanje potrdila..." +certValidUntil = "Potrdilo velja do {{date}}" +certInvalid = "Neveljavno potrdilo: {{error}}" +certInvalidFallback = "Neveljavno potrdilo" +certNetworkError = "Potrdila ni bilo mogoÄe preveriti" +title = "Konfiguriraj potrdilo" + +[certSign.collab.signRequest.image] +hint = "Naložite sliko PNG ali JPG svojega podpisa" + +[certSign.collab.signRequest.mode] +move = "Premakni podpis" +place = "Postavi podpis" +title = "NaÄin: podpis ali premik" + +[certSign.collab.signRequest.modeTabs] +draw = "RiÅ¡i" +image = "Naloži" +text = "Vnesi" + +[certSign.collab.signRequest.placeSignature] +message = "Kliknite na PDF, da postavite svoj podpis" +title = "Postavitev podpisa" + +[certSign.collab.signRequest.preview] +imageAlt = "Izbran podpis" +missing = "Ni predogleda" +textFallback = "Podpis" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Narisani podpis" +defaultImageLabel = "Naloženi podpis" +defaultLabel = "Podpis" +defaultTextLabel = "Vneseni podpis" +delete = "IzbriÅ¡i podpis" +none = "Ni shranjenih podpisov" + +[certSign.collab.signRequest.signatureType] +draw = "RiÅ¡i" +type = "Vnesi" +upload = "Naloži" + +[certSign.collab.signRequest.steps] +back = "Nazaj" +cancelPlacement = "PrekliÄi postavitev" +certificate = "Potrdilo" +clickMultipleTimes = "Kliknite na PDF veÄkrat, da postavite podpise. Povlecite kateri koli podpis za premik ali spremembo velikosti." +clickToPlace = "Kliknite na PDF, kamor želite, da se pojavi vaÅ¡ podpis." +continue = "Nadaljuj na izbiro potrdila" +continueToPlacement = "Nadaljuj na postavitev" +continueToReview = "Nadaljuj na pregled" +createSignature = "Ustvari podpis" +invisible = "Nevidno" +location = "Lokacija:" +multipleSignatures = "{{count}} podpisov bo uporabljenih na PDF" +oneSignature = "1 podpis bo uporabljen na PDF" +placeOnPdf = "Postavi na PDF" +reason = "Razlog:" +reviewTitle = "Pregled pred podpisom" +signaturePlaced = "Podpis je postavljen na stran {{page}}. Položaj lahko prilagodite z novim klikom ali nadaljujete na pregled." +visible = "Vidno" +visibility = "Vidnost:" +yourSignatures = "VaÅ¡i podpisi ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Barva" +fontLabel = "Pisava" +fontSizeLabel = "Velikost" +fontSizePlaceholder = "16" +label = "Besedilo podpisa" +modalHint = "Vnesite svoje ime, nato kliknite Nadaljuj, da ga postavite na PDF." +placeholder = "Vnesite svoje ime..." + +[certSign.collab.participant] +certValidating = "Preverjanje potrdila..." +certValid = "✓ Potrdilo veljavno" +certValidUntil = " do {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Neveljavno potrdilo" +certNetworkError = "Potrdila ni bilo mogoÄe preveriti" + +[certSign.collab.addParticipants] +add = "Dodaj {{count}} udeležencev" +back = "Nazaj" +configureSignatures = "Nastavi nastavitve podpisa" +continue = "Nadaljuj na nastavitve podpisa" +reasonHelp = "Vnaprej doloÄite razlog podpisa za te udeležence (neobvezno; lahko ga spremenijo ob podpisu)" +reasonPlaceholder = "npr. Odobritev, Pregled..." +selectUsers = "Izberite uporabnike" + +[certSign.collab.sessionCreation] +includeSummaryPage = "VkljuÄi stran s povzetkom podpisov" +includeSummaryPageHelp = "Na koncu bo dodana stran s povzetkom z vsemi metapodatki podpisov. Polja digitalnega potrdila na posameznih straneh bodo skrita (lastnoroÄni podpisi niso prizadeti)." + +[certSign.collab.sessionList] +active = "Aktivno" +finalized = "ZakljuÄeno" + +[certSign.collab.signatureSettings] +description = "Nastavite, kako bodo podpisi videti za vse udeležence" +title = "Videz podpisa" + +[certSign.collab.userSelector] +inviteUsers = "Dodaj uporabnike" +loadError = "Uporabnikov ni bilo mogoÄe naložiti" +noTeam = "Brez ekipe" +noUsers = "Ni najdenih drugih uporabnikov." +placeholder = "Izberite uporabnike..." + +[certSign.mobile] +panelActions = "Dejanja" +panelDocument = "Dokument" +panelPeople = "Osebe" + +[certSign.sessions] +deleted = "Seja izbrisana" +fetchFailed = "Podrobnosti seje ni bilo mogoÄe naložiti" +finalized = "Seja zakljuÄena" +loaded = "Podpisan PDF naložen" +pdfNotReady = "PDF Å¡e ni pripravljen" +pdfNotReadyDesc = "Podpisani PDF se ustvarja. Poskusite znova Äez trenutek." + +[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" + [changeCreds] changePassword = "Uporabljate privzete poverilnice za prijavo. Prosim vnesite novo geslo" changeUsername = "Posodobite uporabniÅ¡ko ime. Po posodobitvi boste odjavljeni." @@ -3242,6 +3531,46 @@ totalSelected = "Skupaj izbrano" unsupported = "Nepodprto" unzip = "Razpakiraj" uploadError = "Nekaterih datotek ni bilo mogoÄe naložiti." +copyCreated = "Kopija je shranjena v to napravo." +copyFailed = "Kopije ni bilo mogoÄe ustvariti." +leaveShare = "Odstrani z mojega seznama" +leaveShareFailed = "Skupne datoteke ni bilo mogoÄe odstraniti." +leaveShareSuccess = "Odstranjeno z vaÅ¡ega seznama v skupni rabi." +removeBoth = "Odstrani z obeh" +removeFilePrompt = "Ta datoteka je shranjena na tej napravi in na vaÅ¡em strežniku. Kje jo želite odstraniti?" +removeFileTitle = "Odstrani datoteko" +removeLocalOnly = "Samo s te naprave" +removeServerFailed = "Datoteke ni bilo mogoÄe odstraniti s strežnika." +removeServerOnly = "Samo s strežnika" +removeServerOnlyPrompt = "Ta datoteka je shranjena samo na vaÅ¡em strežniku. Ali jo želite odstraniti s strežnika?" +removeServerSuccess = "Odstranjeno s strežnika." +removeSharedPrompt = "Ta datoteka je v skupni rabi z vami. Lahko jo odstranite s te naprave ali s seznama v skupni rabi." +removeSharedServerOnlyBlockedPrompt = "Ta datoteka je v skupni rabi z vami in je shranjena samo na strežniku." +removeSharedServerOnlyPrompt = "Ta datoteka je v skupni rabi z vami in je shranjena samo na strežniku. Jo želite odstraniti s svojega seznama?" +changesNotUploaded = "Spremembe niso naložene" +cloudFile = "Datoteka v oblaku" +filterAll = "Vse" +filterLocal = "Krajevne" +filterSharedByMe = "V skupni rabi z moje strani" +filterSharedWithMe = "Z mano v skupni rabi" +lastSynced = "Zadnja sinhronizacija" +localOnly = "Samo krajevno" +makeCopy = "Ustvari kopijo" +owner = "Lastnik" +ownerUnknown = "Neznano" +share = "Skupna raba" +shareSelected = "Daj v skupno rabo izbrano" +sharedByYou = "V skupni rabi z moje strani" +sharedEditNoticeBody = "Nimate pravic za urejanje strežniÅ¡ke razliÄice te datoteke. Vse spremembe bodo shranjene kot krajevna kopija." +sharedEditNoticeConfirm = "Razumem" +sharedEditNoticeTitle = "Kopija na strežniku samo za branje" +sharedWithYou = "Z vami v skupni rabi" +sharing = "Skupna raba" +storageState = "Shramba" +synced = "Sinhronizirano" +updateOnServer = "Posodobi na strežniku" +uploadSelected = "Naloži izbrano" +uploadToServer = "Naloži na strežnik" [files] addFiles = "Dodaj datoteke" @@ -3367,6 +3696,77 @@ title = "O sploÅ¡Äenju PDF-jev" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "O skupinskem podpisovanju" + +[groupSigning.tooltip.finalization] +bullet1 = "Vsi podpisi se uporabijo v vrstnem redu udeležencev, ki ste ga doloÄili" +bullet2 = "Po potrebi lahko zakljuÄite z delnimi podpisi" +bullet3 = "Ko je zakljuÄeno, seje ni veÄ mogoÄe spreminjati" +description = "Ko vsi udeleženci podpiÅ¡ejo (ali se odloÄite za predÄasen zakljuÄek), lahko ustvarite konÄni podpisani PDF." +title = "Postopek zakljuÄevanja" + +[groupSigning.tooltip.roles] +bullet1 = "Lastnik (vi): Ustvari sejo, nastavi privzete vrednosti podpisa, zakljuÄi dokument" +bullet2 = "Udeleženci: Ustvarijo svoj podpis, izberejo potrdilo, ga postavijo na PDF" +bullet3 = "Udeleženci ne morejo spreminjati nastavitev vidnosti, razloga ali lokacije podpisa" +description = "Vi nadzirate nastavitve videza podpisa za vse udeležence." +title = "Vloge udeležencev" + +[groupSigning.tooltip.sequential] +bullet1 = "Prvi udeleženec mora podpisati, preden lahko drugi dostopa do dokumenta" +bullet2 = "Zagotavlja pravilen vrstni red podpisovanja za pravno skladnost" +bullet3 = "Udeležence lahko prerazporedite z vleÄenjem po seznamu" +description = "Udeleženci podpisujejo dokumente v vrstnem redu, ki ga doloÄite. Vsak podpisnik prejme obvestilo, ko je na vrsti." +title = "Zaporedno podpisovanje" + +[groupSigning.steps] +back = "Nazaj" +completed = "DokonÄano" +current = "Trenutno" +stepLabel = "Korak {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Nadaljuj na pregled" +invisible = "Podpisi bodo nevidni (samo metapodatki)" +locationLabel = "Lokacija:" +preview = "Predogled" +reasonLabel = "Razlog:" +title = "Nastavi nastavitve podpisa" +visible = "Podpisi bodo vidni na strani {{page}}" + +[groupSigning.steps.review] +document = "Dokument" +dueDate = "Rok (neobvezno)" +dueDatePlaceholder = "Izberite rok..." +invisible = "Nevidno (samo metapodatki)" +location = "Lokacija:" +logo = "Logotip:" +logoHidden = "Brez logotipa" +logoShown = "Prikazan logotip Stirling PDF" +participants = "Udeleženci" +reason = "Razlog:" +send = "PoÅ¡lji zahteve za podpis" +signatureSettings = "Nastavitve podpisa" +title = "Pregled podrobnosti seje" +titleShort = "Pregled in poÅ¡lji" +visibility = "Vidnost:" +visible = "Vidno na strani {{page}}" +participantCount = "{{count}} udeležencev bo podpisovalo po vrsti" + +[groupSigning.steps.selectDocument] +continue = "Nadaljuj na izbor udeležencev" +noFile = "Za ustvarjanje seje podpisovanja izberite eno datoteko PDF iz svojih aktivnih datotek." +selectedFile = "Izbran dokument" +title = "Izberi dokument" + +[groupSigning.steps.selectParticipants] +continue = "Nadaljuj na nastavitve podpisa" +count = "Izbranih {{count}} udeležencev" +label = "Izberite udeležence" +placeholder = "Izberite udeležence za podpis..." +title = "Izberite udeležence" + [getPdfInfo] downloadJson = "Prenesite JSON" downloads = "Prenosi" @@ -4460,7 +4860,10 @@ zoomOut = "PomanjÅ¡aj" [viewer] cannotPreviewFile = "Predogled datoteke ni mogoÄ" +disableColorFilter = "OnemogoÄi barvni filter" dualPageView = "Dvo-stranski pogled" +enableDarkFilter = "OmogoÄi temni filter" +enableSepiaFilter = "OmogoÄi sepia filter" firstPage = "Prva stran" lastPage = "Zadnja stran" nextPage = "Naslednja stran" @@ -4470,6 +4873,22 @@ singlePageView = "Enostranski pogled" unknownFile = "Neznana datoteka" zoomIn = "PoveÄaj" zoomOut = "PomanjÅ¡aj" +resetZoom = "Ponastavi poveÄavo" + +[viewer.nonPdf] +fileTypeBadge = "Datoteka {{type}}" +convertToPdf = "Pretvori v PDF" +loading = "Nalaganje..." +emptyFile = "Prazna datoteka" +csvStats = "{{rows}} vrstic · {{columns}} stolpcev · {{size}}" +sortedBy = "RazvrÅ¡Äeno po: {{column}}" +columnDefault = "Stolpec {{index}}" +htmlPreviewWarning = "Predogled HTML — zunanja sredstva se morda ne bodo naložila · {{size}}" +htmlPreview = "Predogled HTML" +invalidJson = "Neveljaven JSON — prikaz neoblikovane vsebine" +textStats = "{{lines}} vrstic · {{size}}" +lineNumbers = "Å tevilke vrstic" +renderMarkdown = "Upodobi markdown" [viewer.attachments] title = "Priloge" @@ -4531,6 +4950,7 @@ toggleAttachments = "Preklopi priloge" toggleTheme = "Preklopi temo" language = "Jezik" toggleAnnotations = "Preklopi vidnost opomb" +toggleLayers = "Preklopi plasti" search = "IÅ¡Äi v PDF" panMode = "NaÄin premikanja" applyRedactionsFirst = "Najprej uveljavi prekrivanja" @@ -5407,20 +5827,72 @@ title = "Natisni datoteko" 2 = "Vnesite ime tiskalnika" [quickAccess] +access = "Dostop" +accessAddPerson = "Dodaj Å¡e eno osebo" +accessBack = "Nazaj" +accessCopyLink = "Kopiraj povezavo" +accessEmail = "E-poÅ¡tni naslov" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Datoteka" +accessGeneral = "SploÅ¡ni dostop" +accessInviteTitle = "Povabi osebe" +accessOwner = "Lastnik" +accessPanel = "Dostop do dokumenta" +accessPeople = "Osebe z dostopom" +accessRemove = "Odstrani" +accessRestricted = "Omejeno" +accessRestrictedHint = "Odpiranje je omogoÄeno le osebam z dostopom" +accessRole = "Vloga" +accessRoleCommenter = "Komentator" +accessRoleEditor = "Urejevalec" +accessRoleViewer = "Ogledovalec" +accessSelectedFile = "Izbrana datoteka" +accessSendInvite = "PoÅ¡lji povabilo" +accessTitle = "Dostop do dokumenta" +accessYou = "Vi" account = "RaÄun" +activeSessions = "Aktivne seje" +activeTab = "Aktivno" activity = "Dnevnik" adminSettings = "Skrbnik" +allSessions = "Vse seje" allTools = "All Tools" automate = "Poteki" +back = "Nazaj" +certSign = "Podpis s potrdilom" +completedSessions = "ZakljuÄene seje" +completedTab = "ZakljuÄeno" config = "Konfig." +createNew = "Ustvari novo zahtevo" +createSession = "Ustvari zahtevo za podpis" +dueDate = "Rok (neobvezno)" files = "Datoteke" help = "PomoÄ" +noActiveSessions = "Ni ÄakajoÄih zahtev za podpis ali aktivnih sej" +noCompletedSessions = "Ni zakljuÄenih sej" +noFile = "Ni izbrane datoteke" read = "Preberi" reader = "Bralnik" +refresh = "Osveži" +requestSignatures = "Zahtevaj podpise" +selectSingleFileToRequest = "Izberite eno datoteko PDF za zahtevo za podpise" +selectedFile = "Izbrana datoteka" +selectUsers = "Izberite uporabnike za podpis" +selectUsersPlaceholder = "Izberite udeležence..." +sendingRequest = "PoÅ¡iljanje..." settings = "Možnosti" showMeAround = "Pokaži mi naokrog" sign = "PodpiÅ¡i" +signatureRequests = "Zahteve za podpis" +signYourself = "PodpiÅ¡ite sami" +newRequest = "Nova zahteva" tours = "Ogledi" +wetSign = "Dodaj podpis" +filterMine = "Moje" +filterOverdue = "Zapadlo" +filterSigned = "Podpisano" +filterDeclined = "Zavrnjeno" +searchDocuments = "IÅ¡Äi dokumente…" [quickAccess.helpMenu] adminTour = "Ogled za skrbnike" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "VaÅ¡ strežnik Stirling-PDF je brez povezave in \"{{e expired = "VaÅ¡a seja je potekla. Osvežite stran in poskusite znova." refreshPage = "Osveži stran" +[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" + +[signatureSettings.tooltip] +header = "Nastavitve videza podpisa" + +[signatureSettings.tooltip.location] +bullet1 = "Primeri: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Ni isto kot položaj na strani" +bullet3 = "Lahko je zahtevano v doloÄenih pravnih jurisdikcijah" +description = "Izbirna geografska lokacija, kjer je bil podpis uporabljen. Shranjeno v metapodatkih potrdila." +title = "Lokacija podpisa" + +[signatureSettings.tooltip.logo] +bullet1 = "Prikazano ob podpisu in besedilu" +bullet2 = "Podpira formate PNG, JPG" +bullet3 = "IzboljÅ¡a profesionalen videz" +description = "Dodajte logotip podjetja k vidnim podpisom za branding in verodostojnost." +title = "Logotip podjetja" + +[signatureSettings.tooltip.reason] +bullet1 = "Primeri: \"Approval\", \"Contract Agreement\", \"Review Complete\"" +bullet2 = "Vidno v lastnostih podpisa PDF" +bullet3 = "Uporabno za revizijsko sled in skladnost" +description = "Izbirno besedilo, ki pojasnjuje, zakaj je dokument podpisan. Shranjeno v metapodatkih potrdila." +title = "Razlog podpisa" + +[signatureSettings.tooltip.visibility] +bullet1 = "Vidno: Podpis se prikaže v PDF s prilagojenim videzom" +bullet2 = "Nevidno: Potrdilo je vdelano brez vidnega znaka" +bullet3 = "Nevidni podpisi Å¡e vedno zagotavljajo kriptografsko veljavnost" +description = "Nadzira, ali je podpis viden v dokumentu ali vdelan nevidno." +title = "Vidnost podpisa" + [settings.configuration] advanced = "Napredno" database = "Podatkovna baza" endpoints = "KonÄne toÄke" features = "Funkcije" +storageSharing = "Shramba datotek in skupna raba" systemSettings = "Sistemske nastavitve" title = "Konfiguracija" @@ -6332,10 +6868,13 @@ title = "Prijavite se v Stirling" [setup.selfhosted] link = "ali se povežite z raÄunom na lastnem strežniku" subtitle = "Vnesite poverilnice strežnika" +changeServerLocked = "VaÅ¡a organizacija je omejila to aplikacijo na doloÄen strežnik" switchToLocal = "Namesto tega uporabi lokalna orodja" title = "Prijavite se v strežnik" [setup.selfhosted.unreachable] +changeServer = "Poveži se z drugim strežnikom" +changeServerLocked = "VaÅ¡a organizacija je omejila to aplikacijo na doloÄen strežnik" continueOffline = "Namesto tega uporabi lokalna orodja" message = "Povezave do {{url}} ni bilo mogoÄe vzpostaviti. Preverite, ali strežnik deluje in je dostopen." retry = "Poskusi znova" @@ -6529,6 +7068,15 @@ saved = "Shranjeno" text = "Besedilo" title = "Vrsta podpisa" +[signRequest] +declined = "Zahteva za podpis zavrnjena" +fetchFailed = "Zahteve za podpis ni bilo mogoÄe naložiti" +signed = "Dokument uspeÅ¡no podpisan" + +[signSession] +createFailed = "Zahteve za podpis ni bilo mogoÄe ustvariti" +created = "Zahteva za podpis poslana" + [signup] accountCreatedSuccessfully = "RaÄun je uspeÅ¡no ustvarjen! Zdaj se lahko prijavite." alreadyHaveAccount = "Že imate raÄun? Prijavite se" @@ -6807,6 +7355,106 @@ title = "Razdeli PDF po poglavjih" [splitPdfByChapters] tags = "razdeli,poglavja,zaznamki,organiziraj" +[storageShare] +accessed = "Dostopano" +accessDenied = "Nimate dostopa do te skupne datoteke. Prosite lastnika, da jo da v skupno rabo z vami." +accessFailed = "Dejavnosti ni mogoÄe naložiti." +accessDeniedBody = "Nimate dostopa do te datoteke. Prosite lastnika, da jo da v skupno rabo z vami." +accessDeniedTitle = "Brez dostopa" +accessLimitedCommenter = "Dostop za komentiranje prihaja kmalu. ÄŒe morate prenesti, prosite lastnika za dostop urejevalca." +accessLimitedTitle = "Omejen dostop" +accessLimitedViewer = "Ta povezava omogoÄa le ogled. ÄŒe morate prenesti, prosite lastnika za dostop urejevalca." +createdAt = "Ustvarjeno" +download = "Prenesi" +downloadFailed = "Te datoteke ni mogoÄe prenesti." +expiredBody = "Ta povezava za skupno rabo je neveljavna ali je potekla." +expiredTitle = "Povezava je potekla" +goToLogin = "Pojdi na prijavo" +loadFailed = "Skupne datoteke ni mogoÄe odpreti." +loading = "Nalaganje povezave za skupno rabo..." +loginPrompt = "Prijavite se za dostop do te skupne datoteke." +loginRequired = "Potrebna je prijava" +openInApp = "Odpri v Stirling PDF" +ownerLabel = "Lastnik" +ownerUnknown = "Neznano" +requiresLogin = "Ta skupna datoteka zahteva prijavo." +roleCommenter = "Komentator" +roleEditor = "Urejevalec" +roleViewer = "Ogledovalec" +shareHeading = "Datoteka v skupni rabi" +titleDefault = "Datoteka v skupni rabi" +tryAgain = "Poskusite znova pozneje." +addUser = "Dodaj" +commenterHint = "Komentiranje prihaja kmalu." +copied = "Povezava kopirana v odložiÅ¡Äe" +copy = "Kopiraj" +copyFailed = "Kopiranje ni uspelo" +description = "Ustvarite povezavo za skupno rabo te datoteke. Prijavljeni uporabniki s povezavo bodo imeli dostop." +downloadsCount = "Prenosi: {{count}}" +emailWarningBody = "Videti je kot e-poÅ¡tni naslov. ÄŒe ta oseba ni uporabnik Stirling PDF, ne bo mogla dostopati do datoteke." +emailWarningConfirm = "Vseeno daj v skupno rabo" +emailWarningTitle = "E-poÅ¡tni naslov" +errorTitle = "Skupna raba ni uspela" +failure = "Povezave za skupno rabo ni bilo mogoÄe ustvariti. Poskusite znova." +fileLabel = "Datoteka" +generate = "Ustvari povezavo" +generated = "Povezava za skupno rabo ustvarjena" +hideActivity = "Skrij dejavnost" +invalidUsername = "Vnesite veljavno uporabniÅ¡ko ime ali e-poÅ¡tni naslov." +lastAccessed = "Zadnji dostop" +linkAccessTitle = "Dostop s povezavo za skupno rabo" +linkLabel = "Povezava za skupno rabo" +linksDisabled = "Povezave za skupno rabo so onemogoÄene." +linksDisabledBody = "Povezave za skupno rabo so onemogoÄene v nastavitvah vaÅ¡ega strežnika." +manage = "Upravljaj skupno rabo" +manageDescription = "Ustvarjajte in upravljajte povezave za skupno rabo te datoteke." +manageLoadFailed = "Povezav za skupno rabo ni mogoÄe naložiti." +manageTitle = "Upravljanje skupne rabe" +noActivity = "Å e ni dejavnosti." +noLinks = "Ni aktivnih povezav za skupno rabo." +noSharedUsers = "Noben uporabnik Å¡e nima dostopa." +removeLink = "Odstrani povezavo" +removeUser = "Odstrani" +revokeFailed = "Povezave za skupno rabo ni mogoÄe odstraniti." +revoked = "Povezava za skupno rabo odstranjena" +roleLabel = "Vloga" +sharingDisabled = "Skupna raba je onemogoÄena." +sharingDisabledBody = "Skupna raba je onemogoÄena v nastavitvah vaÅ¡ega strežnika." +sharedUsersTitle = "Uporabniki z dostopom" +title = "Deli datoteko" +unknownUser = "Neznan uporabnik" +userAddFailed = "Skupna raba s tem uporabnikom ni mogoÄa." +userAdded = "Uporabnik dodan na seznam skupne rabe." +usernameLabel = "UporabniÅ¡ko ime ali e-poÅ¡ta" +usernamePlaceholder = "Vnesite uporabniÅ¡ko ime ali e-poÅ¡to" +userRemoveFailed = "Tega uporabnika ni mogoÄe odstraniti." +userRemoved = "Uporabnik odstranjen s seznama skupne rabe." +viewActivity = "Ogled dejavnosti" +viewed = "Ogledano" +viewsCount = "Ogledi: {{count}}" +downloaded = "Preneseno" +bulkDescription = "Ustvarite eno povezavo za skupno rabo vseh izbranih datotek s prijavljenimi uporabniki." +bulkTitle = "Deli izbrane datoteke" +copyLink = "Kopiraj povezavo za skupno rabo" +fileCount = "{{count}} izbranih datotek" +ownerOnly = "Le lastnik lahko upravlja skupno rabo." +selectSingleFile = "Za upravljanje skupne rabe izberite eno datoteko." + +[storageUpload] +description = "To naloži trenutno datoteko v strežniÅ¡ko shrambo za vaÅ¡ dostop." +errorTitle = "Nalaganje ni uspelo" +failure = "Nalaganje ni uspelo. Preverite svojo prijavo in nastavitve shrambe." +fileLabel = "Datoteka" +hint = "Javne povezave in naÄine dostopa nadzirajo nastavitve vaÅ¡ega strežnika." +success = "Naloženo na strežnik" +title = "Naloži na strežnik" +updateButton = "Posodobi na strežniku" +uploadButton = "Naloži na strežnik" +bulkDescription = "To naloži izbrane datoteke v vaÅ¡o strežniÅ¡ko shrambo." +bulkTitle = "Naloži izbrane datoteke" +fileCount = "{{count}} izbranih datotek" +more = " +{{count}} veÄ" + [storage] approximateSize = "Približna velikost" fileTooLarge = "Datoteka je prevelika. NajveÄja velikost na datoteko je" @@ -7153,6 +7801,30 @@ title = "Ogled/Uredi PDF" [warning] tooltipTitle = "Opozorilo" +[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" + [watermark] completed = "Vodni žig dodan" desc = "Dodajte besedilne ali slikovne vodne žige v PDF datoteke" @@ -7333,6 +8005,7 @@ activeSession = "Aktivna seja" addMembers = "Dodaj Älane" admin = "Skrbnik" confirmDelete = "Ste prepriÄani, da želite izbrisati tega uporabnika? Tega dejanja ni mogoÄe razveljaviti." +confirmUnlock = "Ali ste prepriÄani, da želite odkleniti ta uporabniÅ¡ki raÄun?" deleteUser = "IzbriÅ¡i uporabnika" deleteUserError = "Brisanje uporabnika ni uspelo" deleteUserSuccess = "Uporabnik uspeÅ¡no izbrisan" @@ -7341,6 +8014,8 @@ disable = "OnemogoÄi" disabled = "OnemogoÄen" editRole = "Uredi vlogo" enable = "OmogoÄi" +locked = "zaklenjen" +lockedBadge = "Zaklenjeno" loading = "Nalaganje Älanov..." loginRequired = "Najprej omogoÄite naÄin prijave" member = "ÄŒlan" @@ -7350,6 +8025,9 @@ searchMembers = "IÅ¡Äi Älane..." status = "Stanje" team = "Skupina" title = "Ljudje" +unlockAccount = "Odkleni raÄun" +unlockUserError = "Odklep uporabniÅ¡kega raÄuna ni uspel" +unlockUserSuccess = "UporabniÅ¡ki raÄun je bil uspeÅ¡no odklenjen" user = "Uporabnik" [workspace.people.actions] diff --git a/frontend/public/locales/sr-LATN-RS/translation.toml b/frontend/public/locales/sr-LATN-RS/translation.toml index ca40dec57e..2de6f7438d 100644 --- a/frontend/public/locales/sr-LATN-RS/translation.toml +++ b/frontend/public/locales/sr-LATN-RS/translation.toml @@ -8,6 +8,7 @@ black = "Crno" blue = "Plavo" bored = "Da li ti je dosadno dok ÄekaÅ¡?" cancel = "Otkaži" +confirm = "Potvrdi" changedCredsMessage = "Podaci za prijavu uspeÅ¡no promenjeni!" chooseFile = "Izaberi datoteku" close = "Zatvori" @@ -146,6 +147,7 @@ insufficientCredits = "Nedovoljno kredita. Potrebno: {{requiredCredits}}, Dostup loadingCredits = "Provera kredita..." loadingProStatus = "Provera statusa pretplate..." noticeTopUpOrPlan = "Nedovoljno kredita, dopunite ili preÄ‘ite na plan" +accessInvite = "Pozovi" [account] accountSettings = "PodeÅ¡avanja naloga" @@ -1427,6 +1429,34 @@ title = "Obrada" description = "Maksimalno vreme Äekanja na zadatak obrade pre prijave greÅ¡ke." label = "Vremensko ograniÄenje obrade (sekunde)" +[admin.settings.storage] +description = "KontroliÅ¡ite skladiÅ¡tenje na serveru i opcije deljenja." +title = "SkladiÅ¡tenje datoteka i deljenje" + +[admin.settings.storage.enabled] +description = "Dozvoli korisnicima da Äuvaju datoteke na serveru." +label = "Omogući skladiÅ¡tenje datoteka na serveru" + +[admin.settings.storage.sharing.email] +description = "Dozvoli deljenje preko adresa e-poÅ¡te." +label = "Omogući deljenje putem e-poÅ¡te" +mailLink = "Podesi podeÅ¡avanja poÅ¡te" +mailNote = "Zahteva podeÅ¡avanje poÅ¡te. " + +[admin.settings.storage.sharing.enabled] +description = "Dozvoli korisnicima da dele saÄuvane datoteke." +label = "Omogući deljenje" + +[admin.settings.storage.sharing.links] +description = "Dozvoli deljenje putem linkova uz prijavu." +frontendUrlLink = "Podesi u sistemskim podeÅ¡avanjima" +frontendUrlNote = "Zahteva Frontend URL. " +label = "Omogući linkove za deljenje" + +[admin.settings.storage.signing.enabled] +description = "Dozvoli korisnicima da kreiraju sesije potpisivanja sa viÅ¡e uÄesnika. Zahteva da skladiÅ¡tenje datoteka na serveru bude omogućeno." +label = "Omogući grupno potpisivanje (alfa)" + [admin.settings.unsavedChanges] cancel = "Nastavi ureÄ‘ivanje" discard = "Odbaci promene" @@ -2059,7 +2089,19 @@ numbers = "Brojevi/opsezi: 5, 10-20" progressions = "Progresije: 3n, 4n+1" [certSign] +allSigned = "Svi uÄesnici su potpisali. Spremno za finalizaciju." +awaitingSignatures = "ÄŒeka se na potpise" +signatureProgress = "{{signedCount}}/{{totalCount}} potpisa" chooseCertificate = "Izaberite fajl sertifikata" +declined = "Odbijeno" +fetchFailed = "UÄitavanje podataka o potpisivanju nije uspelo" +finalized = "Finalizovano" +notified = "Na Äekanju" +partialNote = "Možete finalizovati ranije sa trenutnim potpisima. Nepotpisani uÄesnici biće iskljuÄeni." +pending = "Na Äekanju" +readyToFinalize = "Spremno za finalizaciju" +signed = "Potpisano" +viewed = "Pregledano" chooseJksFile = "Izaberite JKS fajl" chooseP12File = "Izaberite PKCS12 fajl" choosePfxFile = "Izaberite PFX fajl" @@ -2082,6 +2124,7 @@ title = "Potpisivanje Sertifikatom" invisible = "Nevidljiv" stepTitle = "Izgled potpisa" visible = "Vidljiv" +visibility = "Vidljivost" [certSign.appearance.options] title = "Detalji potpisa" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Finalizuj i uÄitaj potpisani PDF" +early = "Finalizuj sa trenutnim potpisima" + +[certSign.collab.sessionDetail] +addButton = "Dodaj uÄesnike" +addParticipants = "Dodaj uÄesnike" +addParticipantsError = "Dodavanje uÄesnika nije uspelo" +backToList = "Nazad na sesije" +deleteConfirm = "Da li ste sigurni? Ovo se ne može opozvati." +deleteError = "Brisanje sesije nije uspelo" +deleted = "Sesija je obrisana" +deleteSession = "ObriÅ¡i sesiju" +dueDate = "Rok" +finalizeError = "Finalizacija sesije nije uspela" +loadPdfError = "UÄitavanje potpisanog PDF-a nije uspelo" +loadSignedPdf = "UÄitaj potpisani PDF u aktivne datoteke" +messageLabel = "Poruka" +noAdditionalInfo = "Nema dodatnih informacija" +owner = "Vlasnik" +participantRemoved = "UÄesnik uklonjen" +participants = "UÄesnici" +participantsAdded = "UÄesnici su uspeÅ¡no dodati" +removeParticipant = "Ukloni" +removeParticipantError = "Uklanjanje uÄesnika nije uspelo" +selectUsers = "Izaberite korisnike..." +sessionInfo = "Informacije o sesiji" +workbenchTitle = "Upravljanje sesijom" + +[certSign.collab.signRequest] +addedToFiles = "Dokument dodat u aktivne datoteke" +addSignature = "Dodajte svoj potpis" +addToFiles = "Dodaj u aktivne datoteke" +advancedSettings = "Napredna podeÅ¡avanja" +backToList = "Nazad na zahteve za potpisivanje" +certificateChoice = "Izaberite sertifikat za potpisivanje" +changeSignature = "Promeni potpis" +clearSignature = "ObriÅ¡i potpis" +completeAndSign = "DovrÅ¡i i potpiÅ¡i" +createNewSignature = "Kreiraj novi potpis" +declineButton = "Odbij" +decline = "Odbij zahtev" +deleteSelected = "ObriÅ¡i izabrani potpis" +drawSignature = "Nacrtajte svoj potpis ispod" +dueDate = "Rok" +fileTooLarge = "VeliÄina datoteke mora biti manja od 5 MB" +fontFamily = "Porodica fonta" +fontSize = "VeliÄina fonta: {{size}}px" +fontSizePlaceholder = "VeliÄina" +from = "Od" +invalidCertFile = "Izaberite P12 ili PFX datoteku sertifikata" +invalidFileType = "Izaberite slikovnu datoteku" +location = "Lokacija (opciono)" +locationPlaceholder = "Odakle potpisujete?" +message = "Poruka" +noCertificate = "Izaberite datoteku sertifikata" +noSignatures = "Postavite bar jedan potpis na PDF" +p12File = "P12/PFX datoteka sertifikata" +password = "Lozinka sertifikata" +passwordPlaceholder = "Unesite lozinku..." +penColor = "Boja olovke" +penSize = "VeliÄina olovke: {{size}}px" +placementActive = "Kliknite na PDF za postavljanje" +placeSignatureButton = "Postavi potpis na PDF" +reason = "Razlog (opciono)" +reasonPlaceholder = "ZaÅ¡to potpisujete?" +removeImage = "Ukloni sliku" +removeCertFile = "Ukloni datoteku" +savedSignatures = "SaÄuvani potpisi" +selectFile = "Izaberite slikovnu datoteku" +selectSignatureTitle = "Izaberite ili kreirajte potpis" +signButton = "PotpiÅ¡i dokument" +signatureInfo = "Ova podeÅ¡avanja je konfigurisao vlasnik dokumenta" +signaturePlaced = "Potpis postavljen na stranici" +signatureSettings = "Postavke potpisa" +signatureText = "Tekst potpisa" +signatureTextPlaceholder = "Unesite svoje ime..." +signatureTypeLabel = "Tip potpisa" +signingTitle = "Potpisivanje" +textColor = "Boja teksta" +typeSignature = "Unesite svoje ime da kreirate potpis" +uploadCert = "PrilagoÄ‘eni sertifikat" +uploadCertDesc = "Koristite sopstveni P12/PFX sertifikat" +uploadSignature = "Otpremite sliku svog potpisa" +usePersonalCert = "LiÄni sertifikat" +usePersonalCertDesc = "Automatski generisan za vaÅ¡ nalog" +useServerCert = "Sertifikat organizacije" +useServerCertDesc = "Deljeni sertifikat organizacije" +workbenchTitle = "Zahtev za potpisivanje" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Izaberite boju poteza" +continue = "Nastavi" + +[certSign.collab.signRequest.certModal] +description = "Postavili ste {{count}} potpis(a). Izaberite sertifikat da dovrÅ¡ite potpisivanje." +sign = "PotpiÅ¡i dokument" +certValidating = "Provera sertifikata..." +certValidUntil = "Sertifikat važi do {{date}}" +certInvalid = "Nevažeći sertifikat: {{error}}" +certInvalidFallback = "Nevažeći sertifikat" +certNetworkError = "Nije moguće potvrditi sertifikat" +title = "Podesi sertifikat" + +[certSign.collab.signRequest.image] +hint = "Otpremite PNG ili JPG sliku svog potpisa" + +[certSign.collab.signRequest.mode] +move = "Pomeri potpis" +place = "Postavi potpis" +title = "Režim potpisivanja ili pomeranja" + +[certSign.collab.signRequest.modeTabs] +draw = "Crtanje" +image = "Otpremanje" +text = "Kucanje" + +[certSign.collab.signRequest.placeSignature] +message = "Kliknite na PDF da postavite svoj potpis" +title = "Postavljanje potpisa" + +[certSign.collab.signRequest.preview] +imageAlt = "Izabrani potpis" +missing = "Nema pregleda" +textFallback = "Potpis" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Crtani potpis" +defaultImageLabel = "Otpremljeni potpis" +defaultLabel = "Potpis" +defaultTextLabel = "Kucani potpis" +delete = "ObriÅ¡i potpis" +none = "Nema saÄuvanih potpisa" + +[certSign.collab.signRequest.signatureType] +draw = "Crtanje" +type = "Kucanje" +upload = "Otpremanje" + +[certSign.collab.signRequest.steps] +back = "Nazad" +cancelPlacement = "Otkaži postavljanje" +certificate = "Sertifikat" +clickMultipleTimes = "Kliknite viÅ¡e puta na PDF da postavite potpise. Prevucite bilo koji potpis da ga pomerite ili promenite veliÄinu." +clickToPlace = "Kliknite na PDF gde želite da se pojavi vaÅ¡ potpis." +continue = "Nastavi na izbor sertifikata" +continueToPlacement = "Nastavi na postavljanje" +continueToReview = "Nastavi na pregled" +createSignature = "Kreiraj potpis" +invisible = "Nevidljivo" +location = "Lokacija:" +multipleSignatures = "{{count}} potpisa biće primenjeno na PDF" +oneSignature = "1 potpis biće primenjen na PDF" +placeOnPdf = "Postavi na PDF" +reason = "Razlog:" +reviewTitle = "Pregled pre potpisivanja" +signaturePlaced = "Potpis postavljen na stranici {{page}}. Možete prilagoditi poziciju ponovnim klikom ili nastaviti na pregled." +visible = "Vidljivo" +visibility = "Vidljivost:" +yourSignatures = "VaÅ¡i potpisi ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Boja" +fontLabel = "Font" +fontSizeLabel = "VeliÄina" +fontSizePlaceholder = "16" +label = "Tekst potpisa" +modalHint = "Unesite svoje ime, zatim kliknite na Nastavi da ga postavite na PDF." +placeholder = "Unesite svoje ime..." + +[certSign.collab.participant] +certValidating = "Provera sertifikata..." +certValid = "✓ Sertifikat važi" +certValidUntil = " do {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Nevažeći sertifikat" +certNetworkError = "Nije moguće potvrditi sertifikat" + +[certSign.collab.addParticipants] +add = "Dodaj {{count}} uÄesnika" +back = "Nazad" +configureSignatures = "Podesi postavke potpisa" +continue = "Nastavi na postavke potpisa" +reasonHelp = "Unapred postavite razlog potpisivanja za ove uÄesnike (opciono, mogu ga promeniti pri potpisivanju)" +reasonPlaceholder = "npr. Odobrenje, Revizija..." +selectUsers = "Izaberi korisnike" + +[certSign.collab.sessionCreation] +includeSummaryPage = "UkljuÄi stranicu sa sažetkom potpisa" +includeSummaryPageHelp = "Na kraju će biti dodata stranica sa svim metapodacima potpisa. Okviri digitalnog sertifikata na pojedinaÄnim stranicama biće potisnuti (vlažni potpisi nisu pogoÄ‘eni)." + +[certSign.collab.sessionList] +active = "Aktivno" +finalized = "Finalizovano" + +[certSign.collab.signatureSettings] +description = "Podesite kako će potpisi izgledati za sve uÄesnike" +title = "Izgled potpisa" + +[certSign.collab.userSelector] +inviteUsers = "Dodaj korisnike" +loadError = "UÄitavanje korisnika nije uspelo" +noTeam = "Nema tima" +noUsers = "Nisu pronaÄ‘eni drugi korisnici." +placeholder = "Izaberite korisnike..." + +[certSign.mobile] +panelActions = "Radnje" +panelDocument = "Dokument" +panelPeople = "Osobe" + +[certSign.sessions] +deleted = "Sesija je obrisana" +fetchFailed = "UÄitavanje detalja sesije nije uspelo" +finalized = "Sesija finalizovana" +loaded = "Potpisani PDF je uÄitan" +pdfNotReady = "PDF nije spreman" +pdfNotReadyDesc = "Potpisani PDF se generiÅ¡e. PokuÅ¡ajte ponovo za trenutak." + +[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" + [changeCreds] changePassword = "KoristiÅ¡ podrazumevane pristupne podatke. Molim te unesi novu lozinku" changeUsername = "Ažurirajte korisniÄko ime. Bićete odjavljeni nakon ažuriranja." @@ -3242,6 +3531,46 @@ totalSelected = "Ukupno izabrano" unsupported = "Nepodržano" unzip = "Raspakuj" uploadError = "Nije uspelo otpremanje nekih datoteka." +copyCreated = "Kopija saÄuvana na ovom ureÄ‘aju." +copyFailed = "Nije moguće napraviti kopiju." +leaveShare = "Ukloni sa moje liste" +leaveShareFailed = "Nije moguće ukloniti deljenu datoteku." +leaveShareSuccess = "Uklonjeno sa vaÅ¡e liste deljenih datoteka." +removeBoth = "Ukloni sa oba mesta" +removeFilePrompt = "Ova datoteka je saÄuvana na ovom ureÄ‘aju i na vaÅ¡em serveru. Odakle želite da je uklonite?" +removeFileTitle = "Ukloni datoteku" +removeLocalOnly = "Samo ovaj ureÄ‘aj" +removeServerFailed = "Nije moguće ukloniti datoteku sa servera." +removeServerOnly = "Samo server" +removeServerOnlyPrompt = "Ova datoteka je saÄuvana samo na vaÅ¡em serveru. Želite li da je uklonite sa servera?" +removeServerSuccess = "Uklonjeno sa servera." +removeSharedPrompt = "Ova datoteka je podeljena sa vama. Možete je ukloniti sa ovog ureÄ‘aja ili sa liste deljenih." +removeSharedServerOnlyBlockedPrompt = "Ova datoteka je podeljena sa vama i nalazi se samo na serveru." +removeSharedServerOnlyPrompt = "Ova datoteka je podeljena sa vama i nalazi se samo na serveru. Ukloniti je sa vaÅ¡e liste?" +changesNotUploaded = "Izmene nisu otpremljene" +cloudFile = "Datoteka u oblaku" +filterAll = "Sve" +filterLocal = "Lokalno" +filterSharedByMe = "Deljeno od mene" +filterSharedWithMe = "Deljeno sa mnom" +lastSynced = "Poslednja sinhronizacija" +localOnly = "Samo lokalno" +makeCopy = "Napravi kopiju" +owner = "Vlasnik" +ownerUnknown = "Nepoznato" +share = "Podeli" +shareSelected = "Podeli izabrano" +sharedByYou = "Deljeno od vas" +sharedEditNoticeBody = "Nemate prava za ureÄ‘ivanje serverske verzije ove datoteke. Sve izmene koje napravite biće saÄuvane kao lokalna kopija." +sharedEditNoticeConfirm = "Razumem" +sharedEditNoticeTitle = "Serverska kopija samo za Äitanje" +sharedWithYou = "Deljeno sa vama" +sharing = "Deljenje" +storageState = "SkladiÅ¡te" +synced = "Sinhronizovano" +updateOnServer = "Ažuriraj na serveru" +uploadSelected = "Otpremi izabrano" +uploadToServer = "Otpremi na server" [files] addFiles = "Dodaj datoteke" @@ -3367,6 +3696,77 @@ title = "O ravnanju PDF-ova" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "O grupnom potpisivanju" + +[groupSigning.tooltip.finalization] +bullet1 = "Svi potpisi se primenjuju redom uÄesnika koji ste naveli" +bullet2 = "Možete finalizovati sa delimiÄnim potpisima po potrebi" +bullet3 = "Kada se finalizuje, sesija se ne može izmeniti" +description = "Kada svi uÄesnici potpiÅ¡u (ili izaberete da finalizujete ranije), možete generisati konaÄni potpisani PDF." +title = "Proces finalizacije" + +[groupSigning.tooltip.roles] +bullet1 = "Vlasnik (vi): Kreira sesiju, podeÅ¡ava podrazumevani izgled potpisa, finalizuje dokument" +bullet2 = "UÄesnici: Kreiraju svoj potpis, biraju sertifikat, postavljaju potpis na PDF" +bullet3 = "UÄesnici ne mogu menjati vidljivost, razlog ili postavke lokacije potpisa" +description = "Vi kontroliÅ¡ete podeÅ¡avanja izgleda potpisa za sve uÄesnike." +title = "Uloge uÄesnika" + +[groupSigning.tooltip.sequential] +bullet1 = "Prvi uÄesnik mora potpisati pre nego Å¡to drugi dobije pristup dokumentu" +bullet2 = "ObezbeÄ‘uje ispravan redosled potpisivanja radi pravne usklaÄ‘enosti" +bullet3 = "Možete promeniti redosled prevlaÄenjem u listi" +description = "UÄesnici potpisuju dokumente redosledom koji navedete. Svaki potpisnik dobija obaveÅ¡tenje kada doÄ‘e na red." +title = "Sekvencijalno potpisivanje" + +[groupSigning.steps] +back = "Nazad" +completed = "ZavrÅ¡eno" +current = "Trenutno" +stepLabel = "Korak {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Nastavi na pregled" +invisible = "Potpisi će biti nevidljivi (samo metapodaci)" +locationLabel = "Lokacija:" +preview = "Pregled" +reasonLabel = "Razlog:" +title = "Podesi postavke potpisa" +visible = "Potpisi će biti vidljivi na stranici {{page}}" + +[groupSigning.steps.review] +document = "Dokument" +dueDate = "Rok (opciono)" +dueDatePlaceholder = "Izaberite rok..." +invisible = "Nevidljivo (samo metapodaci)" +location = "Lokacija:" +logo = "Logotip:" +logoHidden = "Bez logotipa" +logoShown = "Prikazan Stirling PDF logotip" +participants = "UÄesnici" +reason = "Razlog:" +send = "PoÅ¡alji zahteve za potpisivanje" +signatureSettings = "Postavke potpisa" +title = "Pregled detalja sesije" +titleShort = "Pregled i slanje" +visibility = "Vidljivost:" +visible = "Vidljivo na stranici {{page}}" +participantCount = "{{count}} uÄesnika će potpisivati redom" + +[groupSigning.steps.selectDocument] +continue = "Nastavi na izbor uÄesnika" +noFile = "Izaberite jednu PDF datoteku iz svojih aktivnih datoteka da biste kreirali sesiju potpisivanja." +selectedFile = "Izabrani dokument" +title = "Izaberite dokument" + +[groupSigning.steps.selectParticipants] +continue = "Nastavi na postavke potpisa" +count = "Izabrano uÄesnika: {{count}}" +label = "Izaberite uÄesnike" +placeholder = "Izaberite uÄesnike za potpisivanje..." +title = "Izaberite uÄesnike" + [getPdfInfo] downloadJson = "Preuzmi JSON" downloads = "Preuzimanja" @@ -4460,7 +4860,10 @@ zoomOut = "Umanji" [viewer] cannotPreviewFile = "Nije moguće pregledati datoteku" +disableColorFilter = "Onemogući filter boja" dualPageView = "Prikaz dve stranice" +enableDarkFilter = "Omogući tamni filter" +enableSepiaFilter = "Omogući sepija filter" firstPage = "Prva stranica" lastPage = "Poslednja stranica" nextPage = "Sledeća stranica" @@ -4470,6 +4873,22 @@ singlePageView = "Prikaz jedne stranice" unknownFile = "Nepoznata datoteka" zoomIn = "Uvećaj" zoomOut = "Umanji" +resetZoom = "Resetuj zum" + +[viewer.nonPdf] +fileTypeBadge = "{{type}} datoteka" +convertToPdf = "Konvertuj u PDF" +loading = "UÄitavanje..." +emptyFile = "Prazna datoteka" +csvStats = "{{rows}} redova · {{columns}} kolona · {{size}}" +sortedBy = "Sortirano po: {{column}}" +columnDefault = "Kolona {{index}}" +htmlPreviewWarning = "HTML pregled — spoljaÅ¡nji resursi možda neće biti uÄitani · {{size}}" +htmlPreview = "HTML pregled" +invalidJson = "Nevažeći JSON — prikaz sirovog sadržaja" +textStats = "{{lines}} linija · {{size}}" +lineNumbers = "Brojevi linija" +renderMarkdown = "Prikaži markdown" [viewer.attachments] title = "Prilozi" @@ -4531,6 +4950,7 @@ toggleAttachments = "Prikaži/sakrij priloge" toggleTheme = "UkljuÄi/iskljuÄi temu" language = "Jezik" toggleAnnotations = "UkljuÄi/iskljuÄi vidljivost anotacija" +toggleLayers = "UkljuÄi/iskljuÄi slojeve" search = "Pretraži PDF" panMode = "Režim pomeranja" applyRedactionsFirst = "Prvo primenite zacrnjivanja" @@ -5407,20 +5827,72 @@ title = "OdÅ¡tampaj datoteku" 2 = "Unesi naziv Å¡tampaÄa" [quickAccess] +access = "Pristup" +accessAddPerson = "Dodaj joÅ¡ jednu osobu" +accessBack = "Nazad" +accessCopyLink = "Kopiraj link" +accessEmail = "Adresa e-poÅ¡te" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Datoteka" +accessGeneral = "OpÅ¡ti pristup" +accessInviteTitle = "Pozovi osobe" +accessOwner = "Vlasnik" +accessPanel = "Pristup dokumentu" +accessPeople = "Osobe sa pristupom" +accessRemove = "Ukloni" +accessRestricted = "OgraniÄeno" +accessRestrictedHint = "Samo osobe sa pristupom mogu da otvore" +accessRole = "Uloga" +accessRoleCommenter = "Komentator" +accessRoleEditor = "Urednik" +accessRoleViewer = "ÄŒitalac" +accessSelectedFile = "Izabrana datoteka" +accessSendInvite = "PoÅ¡alji poziv" +accessTitle = "Pristup dokumentu" +accessYou = "Vi" account = "Nalog" +activeSessions = "Aktivne sesije" +activeTab = "Aktivno" activity = "Istorija" adminSettings = "Admin postavke" +allSessions = "Sve sesije" allTools = "All Tools" automate = "Auto radnje" +back = "Nazad" +certSign = "Potpisivanje sertifikatom" +completedSessions = "ZavrÅ¡ene sesije" +completedTab = "ZavrÅ¡eno" config = "Konfig" +createNew = "Kreiraj novi zahtev" +createSession = "Kreiraj zahtev za potpisivanje" +dueDate = "Rok (opciono)" files = "Fajlovi" help = "Pomoć" +noActiveSessions = "Nema zahteva za potpisivanje na Äekanju niti aktivnih sesija" +noCompletedSessions = "Nema zavrÅ¡enih sesija" +noFile = "Nije izabrana datoteka" read = "ÄŒitanje" reader = "ÄŒitaÄ" +refresh = "Osveži" +requestSignatures = "Zatraži potpise" +selectSingleFileToRequest = "Izaberite jednu PDF datoteku da zatražite potpise" +selectedFile = "Izabrana datoteka" +selectUsers = "Izaberite korisnike za potpisivanje" +selectUsersPlaceholder = "Izaberite uÄesnike..." +sendingRequest = "Slanje..." settings = "Postavke" showMeAround = "Provedi me kroz" sign = "Potpis" +signatureRequests = "Zahtevi za potpisivanje" +signYourself = "PotpiÅ¡ite sami" +newRequest = "Novi zahtev" tours = "Obilasci" +wetSign = "Dodaj potpis" +filterMine = "Moji" +filterOverdue = "Istekli rok" +filterSigned = "Potpisano" +filterDeclined = "Odbijeno" +searchDocuments = "Pretraži dokumente…" [quickAccess.helpMenu] adminTour = "Administratorski obilazak" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "VaÅ¡ Stirling-PDF server je van mreže i \"{{endpoint expired = "Istekla ti je sesija. Osveži stranicu i pokuÅ¡aj ponovo." refreshPage = "Osveži stranicu" +[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" + +[signatureSettings.tooltip] +header = "PodeÅ¡avanja izgleda potpisa" + +[signatureSettings.tooltip.location] +bullet1 = "Primeri: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Nije isto Å¡to i pozicija na stranici" +bullet3 = "Može biti obavezno u odreÄ‘enim pravnim jurisdikcijama" +description = "Opciona geografska lokacija gde je potpis primenjen. ÄŒuva se u metapodacima sertifikata." +title = "Lokacija potpisa" + +[signatureSettings.tooltip.logo] +bullet1 = "Prikazano uz potpis i tekst" +bullet2 = "Podržava PNG, JPG formate" +bullet3 = "PoboljÅ¡ava profesionalni izgled" +description = "Dodajte logotip kompanije vidljivim potpisima radi brendiranja i autentiÄnosti." +title = "Logotip kompanije" + +[signatureSettings.tooltip.reason] +bullet1 = "Primeri: \"Approval\", \"Contract Agreement\", \"Review Complete\"" +bullet2 = "Vidljivo u svojstvima potpisa u PDF-u" +bullet3 = "Korisno za revizione tragove i usklaÄ‘enost" +description = "Opcioni tekst koji objaÅ¡njava zaÅ¡to se dokument potpisuje. ÄŒuva se u metapodacima sertifikata." +title = "Razlog potpisa" + +[signatureSettings.tooltip.visibility] +bullet1 = "Vidljivo: Potpis se pojavljuje u PDF-u sa prilagoÄ‘enim izgledom" +bullet2 = "Nevidljivo: Sertifikat je ugraÄ‘en bez vizuelne oznake" +bullet3 = "Nevidljivi potpisi i dalje pružaju kriptografsku verifikaciju" +description = "KontroliÅ¡e da li je potpis vidljiv na dokumentu ili je nevidljivo ugraÄ‘en." +title = "Vidljivost potpisa" + [settings.configuration] advanced = "Napredno" database = "Baza podataka" endpoints = "Krajnje taÄke" features = "Funkcije" +storageSharing = "SkladiÅ¡tenje datoteka i deljenje" systemSettings = "Sistemska podeÅ¡avanja" title = "Konfiguracija" @@ -6332,10 +6868,13 @@ title = "Prijavite se u Stirling" [setup.selfhosted] link = "ili se povežite na samohostovani nalog" subtitle = "Unesite kredencijale servera" +changeServerLocked = "VaÅ¡a organizacija je ograniÄila ovu aplikaciju na odreÄ‘eni server" switchToLocal = "Umesto toga koristite lokalne alate" title = "Prijava na server" [setup.selfhosted.unreachable] +changeServer = "Povežite se na drugi server" +changeServerLocked = "VaÅ¡a organizacija je ograniÄila ovu aplikaciju na odreÄ‘eni server" continueOffline = "Umesto toga koristite lokalne alate" message = "Nije moguće pristupiti {{url}}. Proverite da li server radi i da li je dostupan." retry = "PokuÅ¡ajte ponovo" @@ -6529,6 +7068,15 @@ saved = "SaÄuvano" text = "Tekst" title = "Tip potpisa" +[signRequest] +declined = "Zahtev za potpisivanje je odbijen" +fetchFailed = "UÄitavanje zahteva za potpisivanje nije uspelo" +signed = "Dokument je uspeÅ¡no potpisan" + +[signSession] +createFailed = "Kreiranje zahteva za potpisivanje nije uspelo" +created = "Zahtev za potpisivanje je poslat" + [signup] accountCreatedSuccessfully = "Nalog je uspeÅ¡no kreiran! Sada se možete prijaviti." alreadyHaveAccount = "Već imate nalog? Prijavite se" @@ -6807,6 +7355,106 @@ title = "Podeli PDF po poglavljima" [splitPdfByChapters] tags = "podeli,poglavlja,zabeleÅ¡ke,organizacija" +[storageShare] +accessed = "Pristupljeno" +accessDenied = "Nemate pristup ovoj deljenoj datoteci. Zatražite od vlasnika da je podeli sa vama." +accessFailed = "Nije moguće uÄitati aktivnost." +accessDeniedBody = "Nemate pristup ovoj datoteci. Zatražite od vlasnika da je podeli sa vama." +accessDeniedTitle = "Nema pristupa" +accessLimitedCommenter = "Pristup za komentarisanje uskoro stiže. Zatražite od vlasnika pristup urednika ako treba da preuzmete." +accessLimitedTitle = "OgraniÄen pristup" +accessLimitedViewer = "Ovaj link je samo za pregled. Zatražite od vlasnika pristup urednika ako treba da preuzmete." +createdAt = "Kreirano" +download = "Preuzmi" +downloadFailed = "Nije moguće preuzeti ovu datoteku." +expiredBody = "Ovaj link za deljenje je nevažeći ili je istekao." +expiredTitle = "Link je istekao" +goToLogin = "Idi na prijavu" +loadFailed = "Nije moguće otvoriti deljenu datoteku." +loading = "UÄitavanje linka za deljenje..." +loginPrompt = "Prijavite se da pristupite ovoj deljenoj datoteci." +loginRequired = "Potrebna je prijava" +openInApp = "Otvori u Stirling PDF" +ownerLabel = "Vlasnik" +ownerUnknown = "Nepoznato" +requiresLogin = "Ova deljena datoteka zahteva prijavu." +roleCommenter = "Komentator" +roleEditor = "Urednik" +roleViewer = "ÄŒitalac" +shareHeading = "Deljena datoteka" +titleDefault = "Deljena datoteka" +tryAgain = "PokuÅ¡ajte ponovo kasnije." +addUser = "Dodaj" +commenterHint = "Komentarisanje uskoro stiže." +copied = "Link kopiran u privremenu memoriju" +copy = "Kopiraj" +copyFailed = "Kopiranje nije uspelo" +description = "Kreirajte link za deljenje za ovu datoteku. Prijavljeni korisnici sa linkom mogu da mu pristupe." +downloadsCount = "Preuzimanja: {{count}}" +emailWarningBody = "Ovo izgleda kao adresa e-poÅ¡te. Ako ova osoba nije već korisnik Stirling PDF-a, neće moći da pristupi datoteci." +emailWarningConfirm = "Ipak podeli" +emailWarningTitle = "Adresa e-poÅ¡te" +errorTitle = "Deljenje nije uspelo" +failure = "Nije moguće generisati link za deljenje. PokuÅ¡ajte ponovo." +fileLabel = "Datoteka" +generate = "GeneriÅ¡i link" +generated = "Link za deljenje je generisan" +hideActivity = "Sakrij aktivnost" +invalidUsername = "Unesite važeće korisniÄko ime ili adresu e-poÅ¡te." +lastAccessed = "Poslednji pristup" +linkAccessTitle = "Pristup putem linka za deljenje" +linkLabel = "Link za deljenje" +linksDisabled = "Linkovi za deljenje su onemogućeni." +linksDisabledBody = "Linkovi za deljenje su onemogućeni podeÅ¡avanjima vaÅ¡eg servera." +manage = "Upravljaj deljenjem" +manageDescription = "Kreirajte i upravljajte linkovima za deljenje ove datoteke." +manageLoadFailed = "Nije moguće uÄitati linkove za deljenje." +manageTitle = "Upravljanje deljenjem" +noActivity = "JoÅ¡ nema aktivnosti." +noLinks = "JoÅ¡ nema aktivnih linkova za deljenje." +noSharedUsers = "JoÅ¡ niko nema pristup." +removeLink = "Ukloni link" +removeUser = "Ukloni" +revokeFailed = "Nije moguće ukloniti link za deljenje." +revoked = "Link za deljenje uklonjen" +roleLabel = "Uloga" +sharingDisabled = "Deljenje je onemogućeno." +sharingDisabledBody = "Deljenje je onemogućeno podeÅ¡avanjima vaÅ¡eg servera." +sharedUsersTitle = "Korisnici sa kojima je podeljeno" +title = "Podeli datoteku" +unknownUser = "Nepoznat korisnik" +userAddFailed = "Nije moguće podeliti sa tim korisnikom." +userAdded = "Korisnik je dodat na listu deljenja." +usernameLabel = "KorisniÄko ime ili e-poÅ¡ta" +usernamePlaceholder = "Unesite korisniÄko ime ili e-poÅ¡tu" +userRemoveFailed = "Nije moguće ukloniti tog korisnika." +userRemoved = "Korisnik je uklonjen sa liste deljenja." +viewActivity = "Prikaži aktivnost" +viewed = "Pregledano" +viewsCount = "Pregledi: {{count}}" +downloaded = "Preuzeto" +bulkDescription = "Napravite jedan link za deljenje svih izabranih datoteka sa prijavljenim korisnicima." +bulkTitle = "Podeli izabrane datoteke" +copyLink = "Kopiraj link za deljenje" +fileCount = "{{count}} izabranih datoteka" +ownerOnly = "Samo vlasnik može da upravlja deljenjem." +selectSingleFile = "Izaberite jednu datoteku da biste upravljali deljenjem." + +[storageUpload] +description = "Ovo otprema trenutnu datoteku na serversko skladiÅ¡te za vaÅ¡ sopstveni pristup." +errorTitle = "Otpremanje nije uspelo" +failure = "Otpremanje nije uspelo. Proverite prijavu i podeÅ¡avanja skladiÅ¡ta." +fileLabel = "Datoteka" +hint = "Javni linkovi i režimi pristupa kontroliÅ¡u se podeÅ¡avanjima vaÅ¡eg servera." +success = "Otpremljeno na server" +title = "Otpremi na server" +updateButton = "Ažuriraj na serveru" +uploadButton = "Otpremi na server" +bulkDescription = "Ovo otprema izabrane datoteke na vaÅ¡e serversko skladiÅ¡te." +bulkTitle = "Otpremi izabrane datoteke" +fileCount = "{{count}} izabranih datoteka" +more = " +{{count}} joÅ¡" + [storage] approximateSize = "Približna veliÄina" fileTooLarge = "Datoteka je prevelika. Maksimalna veliÄina po datoteci je" @@ -7153,6 +7801,30 @@ title = "Pogledaj/Izmeni PDF" [warning] tooltipTitle = "Upozorenje" +[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" + [watermark] completed = "Vodeni žig je dodat" desc = "Dodajte tekstualne ili slikovne vodene žigove PDF datotekama" @@ -7333,6 +8005,7 @@ activeSession = "Aktivna sesija" addMembers = "Dodaj Älanove" admin = "Admin" confirmDelete = "Da li ste sigurni da želite da obriÅ¡ete ovog korisnika? Ova radnja je nepovratna." +confirmUnlock = "Da li ste sigurni da želite da otkljuÄate ovaj korisniÄki nalog?" deleteUser = "ObriÅ¡i korisnika" deleteUserError = "Brisanje korisnika nije uspelo" deleteUserSuccess = "Korisnik uspeÅ¡no obrisan" @@ -7341,6 +8014,8 @@ disable = "Onemogući" disabled = "Onemogućen" editRole = "Uredi ulogu" enable = "Omogući" +locked = "zakljuÄan" +lockedBadge = "ZakljuÄan" loading = "UÄitavanje osoba..." loginRequired = "Prvo omogućite režim prijave" member = "ÄŒlan" @@ -7350,6 +8025,9 @@ searchMembers = "Pretraži Älanove..." status = "Status" team = "Tim" title = "Osobe" +unlockAccount = "OtkljuÄaj nalog" +unlockUserError = "Nije uspelo otkljuÄavanje korisniÄkog naloga" +unlockUserSuccess = "KorisniÄki nalog je uspeÅ¡no otkljuÄan" user = "Korisnik" [workspace.people.actions] diff --git a/frontend/public/locales/sv-SE/translation.toml b/frontend/public/locales/sv-SE/translation.toml index 2490a70f48..5831b683c4 100644 --- a/frontend/public/locales/sv-SE/translation.toml +++ b/frontend/public/locales/sv-SE/translation.toml @@ -8,6 +8,7 @@ black = "Svart" blue = "BlÃ¥" bored = "Trött pÃ¥ att vänta?" cancel = "Avbryt" +confirm = "Bekräfta" changedCredsMessage = "Inloggningsuppgifter ändrade!" chooseFile = "Välj fil" close = "Stäng" @@ -146,6 +147,7 @@ insufficientCredits = "Otillräckliga krediter. Krävs: {{requiredCredits}}, Til loadingCredits = "Kontrollerar krediter..." loadingProStatus = "Kontrollerar prenumerationsstatus..." noticeTopUpOrPlan = "Inte tillräckligt med krediter, fyll pÃ¥ eller uppgradera till ett abonnemang" +accessInvite = "Bjud in" [account] accountSettings = "Kontoinställningar" @@ -1427,6 +1429,34 @@ title = "Bearbetning" description = "Maximal väntetid för ett bearbetningsjobb innan fel rapporteras." label = "Tidsgräns för bearbetning (sekunder)" +[admin.settings.storage] +description = "Styr serverlagring och delningsalternativ." +title = "Fillagring och delning" + +[admin.settings.storage.enabled] +description = "TillÃ¥t användare att lagra filer pÃ¥ servern." +label = "Aktivera fillagring pÃ¥ servern" + +[admin.settings.storage.sharing.email] +description = "TillÃ¥t delning med e-postadresser." +label = "Aktivera delning via e-post" +mailLink = "Konfigurera e-postinställningar" +mailNote = "Kräver e-postkonfiguration. " + +[admin.settings.storage.sharing.enabled] +description = "TillÃ¥t användare att dela lagrade filer." +label = "Aktivera delning" + +[admin.settings.storage.sharing.links] +description = "TillÃ¥t delning via länkar som kräver inloggning." +frontendUrlLink = "Konfigurera i systeminställningar" +frontendUrlNote = "Kräver en Frontend URL. " +label = "Aktivera delningslänkar" + +[admin.settings.storage.signing.enabled] +description = "TillÃ¥t användare att skapa signeringssessioner med flera deltagare. Kräver att fillagring pÃ¥ servern är aktiverad." +label = "Aktivera gruppsignering (Alpha)" + [admin.settings.unsavedChanges] cancel = "Fortsätt redigera" discard = "Förkasta ändringar" @@ -2059,7 +2089,19 @@ numbers = "Tal/intervall: 5, 10-20" progressions = "Progressioner: 3n, 4n+1" [certSign] +allSigned = "Alla deltagare har signerat. Klart att slutföra." +awaitingSignatures = "Väntar pÃ¥ signaturer" +signatureProgress = "{{signedCount}}/{{totalCount}} signaturer" chooseCertificate = "Välj certifikatfil" +declined = "Avböjt" +fetchFailed = "Det gick inte att läsa in signeringsdata" +finalized = "Slutförd" +notified = "Väntar" +partialNote = "Du kan slutföra i förtid med nuvarande signaturer. Osignerade deltagare utesluts." +pending = "Väntar" +readyToFinalize = "Klart att slutföra" +signed = "Signerad" +viewed = "Visad" chooseJksFile = "Välj JKS-fil" chooseP12File = "Välj PKCS12-fil" choosePfxFile = "Välj PFX-fil" @@ -2082,6 +2124,7 @@ title = "Certifikatsignering" invisible = "Osynlig" stepTitle = "Signaturutseende" visible = "Synlig" +visibility = "Synlighet" [certSign.appearance.options] title = "Signaturdetaljer" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Slutför och ladda signerad PDF" +early = "Slutför med nuvarande signaturer" + +[certSign.collab.sessionDetail] +addButton = "Lägg till deltagare" +addParticipants = "Lägg till deltagare" +addParticipantsError = "Det gick inte att lägga till deltagare" +backToList = "Tillbaka till sessioner" +deleteConfirm = "Är du säker? Detta kan inte Ã¥ngras." +deleteError = "Det gick inte att ta bort sessionen" +deleted = "Session borttagen" +deleteSession = "Ta bort session" +dueDate = "Sista datum" +finalizeError = "Det gick inte att slutföra sessionen" +loadPdfError = "Det gick inte att läsa in signerad PDF" +loadSignedPdf = "Ladda in signerad PDF i aktiva filer" +messageLabel = "Meddelande" +noAdditionalInfo = "Ingen ytterligare information" +owner = "Ägare" +participantRemoved = "Deltagare borttagen" +participants = "Deltagare" +participantsAdded = "Deltagare har lagts till" +removeParticipant = "Ta bort" +removeParticipantError = "Det gick inte att ta bort deltagare" +selectUsers = "Välj användare..." +sessionInfo = "Sessionsinfo" +workbenchTitle = "Sessionshantering" + +[certSign.collab.signRequest] +addedToFiles = "Dokument tillagt i aktiva filer" +addSignature = "Lägg till din signatur" +addToFiles = "Lägg till i aktiva filer" +advancedSettings = "Avancerade inställningar" +backToList = "Tillbaka till signeringsförfrÃ¥gningar" +certificateChoice = "Välj ett certifikat att signera med" +changeSignature = "Ändra signatur" +clearSignature = "Rensa signatur" +completeAndSign = "Slutför och signera" +createNewSignature = "Skapa ny signatur" +declineButton = "Avböj" +decline = "Avböj förfrÃ¥gan" +deleteSelected = "Ta bort vald signatur" +drawSignature = "Rita din signatur nedan" +dueDate = "Sista datum" +fileTooLarge = "Filstorleken mÃ¥ste vara under 5 MB" +fontFamily = "Typsnittsfamilj" +fontSize = "Teckenstorlek: {{size}}px" +fontSizePlaceholder = "Storlek" +from = "FrÃ¥n" +invalidCertFile = "Välj en P12- eller PFX-certifikatfil" +invalidFileType = "Välj en bildfil" +location = "Plats (valfritt)" +locationPlaceholder = "Var signerar du frÃ¥n?" +message = "Meddelande" +noCertificate = "Välj en certifikatfil" +noSignatures = "Placera minst en signatur pÃ¥ PDF:en" +p12File = "P12/PFX-certifikatfil" +password = "Certifikatlösenord" +passwordPlaceholder = "Ange lösenord..." +penColor = "Pennfärg" +penSize = "Pennstorlek: {{size}}px" +placementActive = "Klicka pÃ¥ PDF:en för att placera" +placeSignatureButton = "Placera signatur pÃ¥ PDF:en" +reason = "Orsak (valfritt)" +reasonPlaceholder = "Varför signerar du?" +removeImage = "Ta bort bild" +removeCertFile = "Ta bort fil" +savedSignatures = "Sparade signaturer" +selectFile = "Välj bildfil" +selectSignatureTitle = "Välj eller skapa signatur" +signButton = "Signera dokument" +signatureInfo = "Dessa inställningar konfigureras av dokumentägaren" +signaturePlaced = "Signatur placerad pÃ¥ sidan" +signatureSettings = "Signaturinställningar" +signatureText = "Signaturtext" +signatureTextPlaceholder = "Ange ditt namn..." +signatureTypeLabel = "Signaturtyp" +signingTitle = "Signering" +textColor = "Textfärg" +typeSignature = "Skriv ditt namn för att skapa en signatur" +uploadCert = "Eget certifikat" +uploadCertDesc = "Använd ditt eget P12/PFX-certifikat" +uploadSignature = "Ladda upp din signaturbild" +usePersonalCert = "Personligt certifikat" +usePersonalCertDesc = "Skapas automatiskt för ditt konto" +useServerCert = "Organisationscertifikat" +useServerCertDesc = "Delat organisationscertifikat" +workbenchTitle = "SigneringsförfrÃ¥gan" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Välj linjefärg" +continue = "Fortsätt" + +[certSign.collab.signRequest.certModal] +description = "Du har placerat {{count}} signatur(er). Välj ditt certifikat för att slutföra signeringen." +sign = "Signera dokument" +certValidating = "Validerar certifikat..." +certValidUntil = "Certifikatet giltigt till {{date}}" +certInvalid = "Ogiltigt certifikat: {{error}}" +certInvalidFallback = "Ogiltigt certifikat" +certNetworkError = "Kunde inte validera certifikat" +title = "Konfigurera certifikat" + +[certSign.collab.signRequest.image] +hint = "Ladda upp en PNG- eller JPG-bild av din signatur" + +[certSign.collab.signRequest.mode] +move = "Flytta signatur" +place = "Placera signatur" +title = "Läge för signering eller flytt" + +[certSign.collab.signRequest.modeTabs] +draw = "Rita" +image = "Ladda upp" +text = "Skriv" + +[certSign.collab.signRequest.placeSignature] +message = "Klicka pÃ¥ PDF:en för att placera din signatur" +title = "Placera signatur" + +[certSign.collab.signRequest.preview] +imageAlt = "Vald signatur" +missing = "Ingen förhandsvisning" +textFallback = "Signatur" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Ritad signatur" +defaultImageLabel = "Uppladdad signatur" +defaultLabel = "Signatur" +defaultTextLabel = "Skriven signatur" +delete = "Ta bort signatur" +none = "Inga sparade signaturer" + +[certSign.collab.signRequest.signatureType] +draw = "Rita" +type = "Skriv" +upload = "Ladda upp" + +[certSign.collab.signRequest.steps] +back = "Tillbaka" +cancelPlacement = "Avbryt placering" +certificate = "Certifikat" +clickMultipleTimes = "Klicka pÃ¥ PDF:en flera gÃ¥nger för att placera signaturer. Dra en signatur för att flytta eller ändra storlek." +clickToPlace = "Klicka pÃ¥ PDF:en där du vill att din signatur ska visas." +continue = "Fortsätt till certifikatval" +continueToPlacement = "Fortsätt till placering" +continueToReview = "Fortsätt till granskning" +createSignature = "Skapa signatur" +invisible = "Osynlig" +location = "Plats:" +multipleSignatures = "{{count}} signaturer kommer att tillämpas pÃ¥ PDF:en" +oneSignature = "1 signatur kommer att tillämpas pÃ¥ PDF:en" +placeOnPdf = "Placera pÃ¥ PDF:en" +reason = "Orsak:" +reviewTitle = "Granska före signering" +signaturePlaced = "Signatur placerad pÃ¥ sidan {{page}}. Du kan justera positionen genom att klicka igen eller fortsätta till granskning." +visible = "Synlig" +visibility = "Synlighet:" +yourSignatures = "Dina signaturer ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Färg" +fontLabel = "Typsnitt" +fontSizeLabel = "Storlek" +fontSizePlaceholder = "16" +label = "Signaturtext" +modalHint = "Ange ditt namn och klicka sedan pÃ¥ Fortsätt för att placera det pÃ¥ PDF:en." +placeholder = "Ange ditt namn..." + +[certSign.collab.participant] +certValidating = "Validerar certifikat..." +certValid = "✓ Giltigt certifikat" +certValidUntil = " till {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Ogiltigt certifikat" +certNetworkError = "Kunde inte validera certifikat" + +[certSign.collab.addParticipants] +add = "Lägg till {{count}} deltagare" +back = "Tillbaka" +configureSignatures = "Konfigurera signaturinställningar" +continue = "Fortsätt till signaturinställningar" +reasonHelp = "Förinställ en signeringsorsak för dessa deltagare (valfritt, de kan Ã¥sidosätta vid signering)" +reasonPlaceholder = "t.ex. Godkännande, Granskning..." +selectUsers = "Välj användare" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Inkludera sammanfattningssida för signaturer" +includeSummaryPageHelp = "En sammanfattningssida läggs till sist med all signaturmetadata. De digitala certifikatsignaturrutorna pÃ¥ enskilda sidor undertrycks (handskrivna signaturer pÃ¥verkas inte)." + +[certSign.collab.sessionList] +active = "Aktiva" +finalized = "Slutförda" + +[certSign.collab.signatureSettings] +description = "Konfigurera hur signaturer ska visas för alla deltagare" +title = "Signaturutseende" + +[certSign.collab.userSelector] +inviteUsers = "Lägg till användare" +loadError = "Det gick inte att läsa in användare" +noTeam = "Inget team" +noUsers = "Inga andra användare hittades." +placeholder = "Välj användare..." + +[certSign.mobile] +panelActions = "Ã…tgärder" +panelDocument = "Dokument" +panelPeople = "Personer" + +[certSign.sessions] +deleted = "Session borttagen" +fetchFailed = "Det gick inte att läsa in sessionsdetaljer" +finalized = "Session slutförd" +loaded = "Signerad PDF inläst" +pdfNotReady = "PDF inte klar" +pdfNotReadyDesc = "Den signerade PDF:en genereras. Försök igen om en stund." + +[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" + [changeCreds] changePassword = "Du använder standardinloggningsuppgifter. Vänligen ange ett nytt lösenord" changeUsername = "Uppdatera ditt användarnamn. Du loggas ut efter uppdateringen." @@ -3242,6 +3531,46 @@ totalSelected = "Totalt markerade" unsupported = "Stöds inte" unzip = "Packa upp" uploadError = "Det gick inte att ladda upp vissa filer." +copyCreated = "Kopia sparad pÃ¥ den här enheten." +copyFailed = "Kunde inte skapa en kopia." +leaveShare = "Ta bort frÃ¥n min lista" +leaveShareFailed = "Kunde inte ta bort den delade filen." +leaveShareSuccess = "Borttagen frÃ¥n din delningslista." +removeBoth = "Ta bort frÃ¥n bÃ¥da" +removeFilePrompt = "Den här filen är sparad pÃ¥ den här enheten och pÃ¥ din server. Var vill du ta bort den?" +removeFileTitle = "Ta bort fil" +removeLocalOnly = "Endast den här enheten" +removeServerFailed = "Kunde inte ta bort filen frÃ¥n servern." +removeServerOnly = "Endast servern" +removeServerOnlyPrompt = "Den här filen lagras endast pÃ¥ din server. Vill du ta bort den frÃ¥n servern?" +removeServerSuccess = "Borttagen frÃ¥n servern." +removeSharedPrompt = "Den här filen är delad med dig. Du kan ta bort den frÃ¥n den här enheten eller din delningslista." +removeSharedServerOnlyBlockedPrompt = "Den här filen är delad med dig och lagras endast pÃ¥ servern." +removeSharedServerOnlyPrompt = "Den här filen är delad med dig och lagras endast pÃ¥ servern. Ta bort den frÃ¥n din lista?" +changesNotUploaded = "Ändringar inte uppladdade" +cloudFile = "Molnfil" +filterAll = "Alla" +filterLocal = "Lokalt" +filterSharedByMe = "Delad av mig" +filterSharedWithMe = "Delad med mig" +lastSynced = "Senast synkroniserad" +localOnly = "Endast lokalt" +makeCopy = "Skapa en kopia" +owner = "Ägare" +ownerUnknown = "Okänd" +share = "Dela" +shareSelected = "Dela valda" +sharedByYou = "Delad av dig" +sharedEditNoticeBody = "Du har inte redigeringsrättigheter till serverversionen av den här filen. Alla ändringar du gör sparas som en lokal kopia." +sharedEditNoticeConfirm = "Jag förstÃ¥r" +sharedEditNoticeTitle = "Skrivskyddad serverkopia" +sharedWithYou = "Delad med dig" +sharing = "Delning" +storageState = "Lagring" +synced = "Synkroniserad" +updateOnServer = "Uppdatera pÃ¥ servern" +uploadSelected = "Ladda upp valda" +uploadToServer = "Ladda upp till servern" [files] addFiles = "Lägg till filer" @@ -3367,6 +3696,77 @@ title = "Om att platta ut PDF:er" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Om gruppsignering" + +[groupSigning.tooltip.finalization] +bullet1 = "Alla signaturer tillämpas i den deltagarordning du angav" +bullet2 = "Du kan slutföra med ofullständiga signaturer vid behov" +bullet3 = "När den väl är slutförd kan sessionen inte ändras" +description = "När alla deltagare har signerat (eller om du väljer att slutföra i förtid) kan du generera den slutliga signerade PDF:en." +title = "Slutförandeprocess" + +[groupSigning.tooltip.roles] +bullet1 = "Ägare (du): Skapar session, konfigurerar signaturstandarder, slutför dokumentet" +bullet2 = "Deltagare: Skapar sin signatur, väljer certifikat, placerar pÃ¥ PDF:en" +bullet3 = "Deltagare kan inte ändra inställningar för signatursynlighet, orsak eller plats" +description = "Du styr inställningarna för signaturutseende för alla deltagare." +title = "Deltagarroller" + +[groupSigning.tooltip.sequential] +bullet1 = "Första deltagaren mÃ¥ste signera innan den andra kan fÃ¥ Ã¥tkomst till dokumentet" +bullet2 = "Säkerställer korrekt signeringsordning för regelefterlevnad" +bullet3 = "Du kan ändra ordningen genom att dra deltagare i listan" +description = "Deltagarna signerar dokument i den ordning du anger. Varje undertecknare fÃ¥r en avisering när det är deras tur." +title = "Sekventiell signering" + +[groupSigning.steps] +back = "Tillbaka" +completed = "Slutfört" +current = "Aktuell" +stepLabel = "Steg {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Fortsätt till granskning" +invisible = "Signaturer kommer att vara osynliga (endast metadata)" +locationLabel = "Plats:" +preview = "Förhandsvisa" +reasonLabel = "Orsak:" +title = "Konfigurera signaturinställningar" +visible = "Signaturer kommer att vara synliga pÃ¥ sidan {{page}}" + +[groupSigning.steps.review] +document = "Dokument" +dueDate = "Sista datum (valfritt)" +dueDatePlaceholder = "Välj sista datum..." +invisible = "Osynlig (endast metadata)" +location = "Plats:" +logo = "Logotyp:" +logoHidden = "Ingen logotyp" +logoShown = "Stirling PDF-logotyp visas" +participants = "Deltagare" +reason = "Orsak:" +send = "Skicka signeringsförfrÃ¥gningar" +signatureSettings = "Signaturinställningar" +title = "Granska sessionsdetaljer" +titleShort = "Granska och skicka" +visibility = "Synlighet:" +visible = "Synlig pÃ¥ sidan {{page}}" +participantCount = "{{count}} deltagare kommer att signera i ordning" + +[groupSigning.steps.selectDocument] +continue = "Fortsätt till deltagarval" +noFile = "Välj en enda PDF-fil frÃ¥n dina aktiva filer för att skapa en signeringssession." +selectedFile = "Valt dokument" +title = "Välj dokument" + +[groupSigning.steps.selectParticipants] +continue = "Fortsätt till signaturinställningar" +count = "{{count}} deltagare valda" +label = "Välj deltagare" +placeholder = "Välj deltagare som ska signera..." +title = "Välj deltagare" + [getPdfInfo] downloadJson = "Ladda ner JSON" downloads = "Nedladdningar" @@ -4460,7 +4860,10 @@ zoomOut = "Zooma ut" [viewer] cannotPreviewFile = "Kan inte förhandsgranska filen" +disableColorFilter = "Inaktivera färgfilter" dualPageView = "Dubbelsidig vy" +enableDarkFilter = "Aktivera mörkt filter" +enableSepiaFilter = "Aktivera sepiafilter" firstPage = "Första sidan" lastPage = "Sista sidan" nextPage = "Nästa sida" @@ -4470,6 +4873,22 @@ singlePageView = "Ensidig vy" unknownFile = "Okänd fil" zoomIn = "Zooma in" zoomOut = "Zooma ut" +resetZoom = "Ã…terställ zoom" + +[viewer.nonPdf] +fileTypeBadge = "{{type}}-fil" +convertToPdf = "Konvertera till PDF" +loading = "Läser in..." +emptyFile = "Tom fil" +csvStats = "{{rows}} rader · {{columns}} kolumner · {{size}}" +sortedBy = "Sorterad efter: {{column}}" +columnDefault = "Kolumn {{index}}" +htmlPreviewWarning = "HTML-förhandsvisning — externa resurser kanske inte läses in · {{size}}" +htmlPreview = "HTML-förhandsvisning" +invalidJson = "Ogiltig JSON — visar rÃ¥tt innehÃ¥ll" +textStats = "{{lines}} rader · {{size}}" +lineNumbers = "Radnummer" +renderMarkdown = "Rendera markdown" [viewer.attachments] title = "Bilagor" @@ -4531,6 +4950,7 @@ toggleAttachments = "Visa/dölj bilagor" toggleTheme = "Växla tema" language = "SprÃ¥k" toggleAnnotations = "Växla synlighet för anteckningar" +toggleLayers = "Växla lager" search = "Sök i PDF" panMode = "Panoreringsläge" applyRedactionsFirst = "Tillämpa maskningar först" @@ -5407,20 +5827,72 @@ title = "Skriv ut fil" 2 = "Ange skrivarnamn" [quickAccess] +access = "Ã…tkomst" +accessAddPerson = "Lägg till ytterligare en person" +accessBack = "Tillbaka" +accessCopyLink = "Kopiera länk" +accessEmail = "E-postadress" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Fil" +accessGeneral = "Allmän Ã¥tkomst" +accessInviteTitle = "Bjud in personer" +accessOwner = "Ägare" +accessPanel = "DokumentÃ¥tkomst" +accessPeople = "Personer med Ã¥tkomst" +accessRemove = "Ta bort" +accessRestricted = "Begränsad" +accessRestrictedHint = "Endast personer med Ã¥tkomst kan öppna" +accessRole = "Roll" +accessRoleCommenter = "Kommentator" +accessRoleEditor = "Redigerare" +accessRoleViewer = "Läsare" +accessSelectedFile = "Vald fil" +accessSendInvite = "Skicka inbjudan" +accessTitle = "DokumentÃ¥tkomst" +accessYou = "Du" account = "Konto" +activeSessions = "Aktiva sessioner" +activeTab = "Aktiva" activity = "Aktivitet" adminSettings = "Admin inst." +allSessions = "Alla sessioner" allTools = "All Tools" automate = "Automatisera" +back = "Tillbaka" +certSign = "Certifikatsignering" +completedSessions = "Slutförda sessioner" +completedTab = "Slutförda" config = "Konfig" +createNew = "Skapa ny förfrÃ¥gan" +createSession = "Skapa signeringsförfrÃ¥gan" +dueDate = "Sista datum (valfritt)" files = "Filer" help = "Hjälp" +noActiveSessions = "Inga väntande signeringsförfrÃ¥gningar eller aktiva sessioner" +noCompletedSessions = "Inga slutförda sessioner" +noFile = "Ingen fil vald" read = "Läs" reader = "Läsare" +refresh = "Uppdatera" +requestSignatures = "Begär signaturer" +selectSingleFileToRequest = "Välj en enda PDF-fil för att begära signaturer" +selectedFile = "Vald fil" +selectUsers = "Välj användare som ska signera" +selectUsersPlaceholder = "Välj deltagare..." +sendingRequest = "Skickar..." settings = "Inst." showMeAround = "Visa mig runt" sign = "Signera" +signatureRequests = "SigneringsförfrÃ¥gningar" +signYourself = "Signera själv" +newRequest = "Ny förfrÃ¥gan" tours = "Guider" +wetSign = "Lägg till signatur" +filterMine = "Mina" +filterOverdue = "Försenade" +filterSigned = "Signerade" +filterDeclined = "Avböjda" +searchDocuments = "Sök dokument…" [quickAccess.helpMenu] adminTour = "Adminrundtur" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Din Stirling-PDF-server är offline och \"{{endpoint} expired = "Din session har löpt ut. Uppdatera sidan och försök igen." refreshPage = "Uppdatera sida" +[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" + +[signatureSettings.tooltip] +header = "Inställningar för signaturutseende" + +[signatureSettings.tooltip.location] +bullet1 = "Exempel: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Inte samma som sidposition" +bullet3 = "Kan krävas i vissa rättsliga jurisdiktioner" +description = "Valfri geografisk plats där signaturen tillämpades. Lagradas i certifikatets metadata." +title = "Signaturplats" + +[signatureSettings.tooltip.logo] +bullet1 = "Visas tillsammans med signatur och text" +bullet2 = "Stöder formaten PNG, JPG" +bullet3 = "Förbättrar professionellt utseende" +description = "Lägg till en företagslogotyp i synliga signaturer för varumärke och äkthet." +title = "Företagslogotyp" + +[signatureSettings.tooltip.reason] +bullet1 = "Exempel: \"Godkännande\", \"Kontraktsavtal\", \"Granskning slutförd\"" +bullet2 = "Synlig i PDF:ens signatur-egenskaper" +bullet3 = "Användbart för granskningsspÃ¥r och regelefterlevnad" +description = "Valfri text som förklarar varför dokumentet signeras. Lagradas i certifikatets metadata." +title = "Signeringsorsak" + +[signatureSettings.tooltip.visibility] +bullet1 = "Synlig: Signaturen visas pÃ¥ PDF:en med anpassat utseende" +bullet2 = "Osynlig: Certifikat bäddas in utan visuellt märke" +bullet3 = "Osynliga signaturer ger fortfarande kryptografisk validering" +description = "Styr om signaturen är synlig i dokumentet eller bäddas in osynligt." +title = "Signaturens synlighet" + [settings.configuration] advanced = "Avancerat" database = "Databas" endpoints = "Slutpunkter" features = "Funktioner" +storageSharing = "Fillagring och delning" systemSettings = "Systeminställningar" title = "Konfiguration" @@ -6332,10 +6868,13 @@ title = "Logga in pÃ¥ Stirling" [setup.selfhosted] link = "eller anslut till ett självhostat konto" subtitle = "Ange dina serveruppgifter" +changeServerLocked = "Din organisation har begränsat den här appen till en specifik server" switchToLocal = "Använd lokala verktyg i stället" title = "Logga in pÃ¥ server" [setup.selfhosted.unreachable] +changeServer = "Anslut till en annan server" +changeServerLocked = "Din organisation har begränsat den här appen till en specifik server" continueOffline = "Använd lokala verktyg i stället" message = "Kunde inte nÃ¥ {{url}}. Kontrollera att servern körs och är tillgänglig." retry = "Försök igen" @@ -6529,6 +7068,15 @@ saved = "Sparad" text = "Text" title = "Signaturtyp" +[signRequest] +declined = "SigneringsförfrÃ¥gan avböjdes" +fetchFailed = "Det gick inte att läsa in signeringsförfrÃ¥gan" +signed = "Dokumentet signerades" + +[signSession] +createFailed = "Det gick inte att skapa signeringsförfrÃ¥gan" +created = "SigneringsförfrÃ¥gan skickad" + [signup] accountCreatedSuccessfully = "Kontot har skapats! Du kan nu logga in." alreadyHaveAccount = "Har du redan ett konto? Logga in" @@ -6807,6 +7355,106 @@ title = "Dela upp PDF efter kapitel" [splitPdfByChapters] tags = "dela,kapitel,bokmärken,organisera" +[storageShare] +accessed = "Ã…tkomst" +accessDenied = "Du har inte Ã¥tkomst till den här delade filen. Be ägaren dela den med dig." +accessFailed = "Det gÃ¥r inte att läsa in aktivitet." +accessDeniedBody = "Du har inte Ã¥tkomst till den här filen. Be ägaren dela den med dig." +accessDeniedTitle = "Ingen Ã¥tkomst" +accessLimitedCommenter = "KommentarÃ¥tkomst kommer snart. Be ägaren om redigerarÃ¥tkomst om du behöver ladda ned." +accessLimitedTitle = "Begränsad Ã¥tkomst" +accessLimitedViewer = "Den här länken är endast för visning. Be ägaren om redigerarÃ¥tkomst om du behöver ladda ned." +createdAt = "Skapad" +download = "Ladda ned" +downloadFailed = "Det gÃ¥r inte att ladda ned den här filen." +expiredBody = "Den här delningslänken är ogiltig eller har löpt ut." +expiredTitle = "Länken har löpt ut" +goToLogin = "GÃ¥ till inloggningen" +loadFailed = "Det gÃ¥r inte att öppna den delade filen." +loading = "Läser in delningslänk..." +loginPrompt = "Logga in för att fÃ¥ Ã¥tkomst till den här delade filen." +loginRequired = "Inloggning krävs" +openInApp = "Öppna i Stirling PDF" +ownerLabel = "Ägare" +ownerUnknown = "Okänd" +requiresLogin = "Den här delade filen kräver inloggning." +roleCommenter = "Kommentator" +roleEditor = "Redigerare" +roleViewer = "Läsare" +shareHeading = "Delad fil" +titleDefault = "Delad fil" +tryAgain = "Försök igen senare." +addUser = "Lägg till" +commenterHint = "Kommentering kommer snart." +copied = "Länk kopierad till urklipp" +copy = "Kopiera" +copyFailed = "Kopiering misslyckades" +description = "Skapa en delningslänk för den här filen. Inloggade användare med länken kan fÃ¥ Ã¥tkomst till den." +downloadsCount = "Nedladdningar: {{count}}" +emailWarningBody = "Detta ser ut som en e-postadress. Om den här personen inte redan är Stirling PDF-användare kommer de inte att kunna fÃ¥ Ã¥tkomst till filen." +emailWarningConfirm = "Dela ändÃ¥" +emailWarningTitle = "E-postadress" +errorTitle = "Delning misslyckades" +failure = "Det gick inte att skapa en delningslänk. Försök igen." +fileLabel = "Fil" +generate = "Generera länk" +generated = "Delningslänk skapad" +hideActivity = "Dölj aktivitet" +invalidUsername = "Ange ett giltigt användarnamn eller e-postadress." +lastAccessed = "Senast Ã¥tkommet" +linkAccessTitle = "Ã…tkomst via delningslänk" +linkLabel = "Delningslänk" +linksDisabled = "Delningslänkar är inaktiverade." +linksDisabledBody = "Delningslänkar är inaktiverade i dina serverinställningar." +manage = "Hantera delning" +manageDescription = "Skapa och hantera länkar för att dela den här filen." +manageLoadFailed = "Det gÃ¥r inte att läsa in delningslänkar." +manageTitle = "Hantera delning" +noActivity = "Ingen aktivitet ännu." +noLinks = "Inga aktiva delningslänkar ännu." +noSharedUsers = "Inga användare har Ã¥tkomst ännu." +removeLink = "Ta bort länk" +removeUser = "Ta bort" +revokeFailed = "Det gÃ¥r inte att ta bort delningslänken." +revoked = "Delningslänk borttagen" +roleLabel = "Roll" +sharingDisabled = "Delning är inaktiverad." +sharingDisabledBody = "Delning har inaktiverats av dina serverinställningar." +sharedUsersTitle = "Delade användare" +title = "Dela fil" +unknownUser = "Okänd användare" +userAddFailed = "Det gÃ¥r inte att dela med den användaren." +userAdded = "Användare tillagd i delningslistan." +usernameLabel = "Användarnamn eller e-post" +usernamePlaceholder = "Ange ett användarnamn eller e-post" +userRemoveFailed = "Det gÃ¥r inte att ta bort den användaren." +userRemoved = "Användare borttagen frÃ¥n delningslistan." +viewActivity = "Visa aktivitet" +viewed = "Visad" +viewsCount = "Visningar: {{count}}" +downloaded = "Nedladdad" +bulkDescription = "Skapa en länk för att dela alla markerade filer med inloggade användare." +bulkTitle = "Dela markerade filer" +copyLink = "Kopiera delningslänk" +fileCount = "{{count}} filer markerade" +ownerOnly = "Endast ägaren kan hantera delning." +selectSingleFile = "Välj en enskild fil för att hantera delning." + +[storageUpload] +description = "Detta laddar upp den aktuella filen till serverlagring för din egen Ã¥tkomst." +errorTitle = "Uppladdning misslyckades" +failure = "Uppladdning misslyckades. Kontrollera din inloggning och dina lagringsinställningar." +fileLabel = "Fil" +hint = "Offentliga länkar och Ã¥tkomstlägen styrs av dina serverinställningar." +success = "Uppladdad till server" +title = "Ladda upp till server" +updateButton = "Uppdatera pÃ¥ servern" +uploadButton = "Ladda upp till server" +bulkDescription = "Detta laddar upp de markerade filerna till din serverlagring." +bulkTitle = "Ladda upp markerade filer" +fileCount = "{{count}} filer markerade" +more = " +{{count}} till" + [storage] approximateSize = "Ungefärlig storlek" fileTooLarge = "Filen är för stor. Maximal storlek per fil är" @@ -7153,6 +7801,30 @@ title = "Visa/redigera PDF" [warning] tooltipTitle = "Varning" +[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" + [watermark] completed = "Vattenstämpel tillagd" desc = "Lägg till text- eller bildvattenstämplar i PDF‑filer" @@ -7333,6 +8005,7 @@ activeSession = "Aktiv session" addMembers = "Lägg till medlemmar" admin = "Admin" confirmDelete = "Är du säker pÃ¥ att du vill ta bort denna användare? Detta kan inte Ã¥ngras." +confirmUnlock = "Är du säker pÃ¥ att du vill lÃ¥sa upp detta användarkonto?" deleteUser = "Ta bort användare" deleteUserError = "Misslyckades med att ta bort användare" deleteUserSuccess = "Användare borttagen" @@ -7341,6 +8014,8 @@ disable = "Inaktivera" disabled = "Inaktiverad" editRole = "Redigera roll" enable = "Aktivera" +locked = "lÃ¥st" +lockedBadge = "LÃ¥st" loading = "Laddar personer..." loginRequired = "Aktivera inloggningsläge först" member = "Medlem" @@ -7350,6 +8025,9 @@ searchMembers = "Sök medlemmar..." status = "Status" team = "Team" title = "Personer" +unlockAccount = "LÃ¥s upp konto" +unlockUserError = "Det gick inte att lÃ¥sa upp användarkontot" +unlockUserSuccess = "Användarkontot har lÃ¥sts upp" user = "Användare" [workspace.people.actions] diff --git a/frontend/public/locales/th-TH/translation.toml b/frontend/public/locales/th-TH/translation.toml index ea1827d48e..947a4b2217 100644 --- a/frontend/public/locales/th-TH/translation.toml +++ b/frontend/public/locales/th-TH/translation.toml @@ -8,6 +8,7 @@ black = "ดำ" blue = "น้ำเงิน" bored = "เบื่อรอหรือยัง?" cancel = "ยà¸à¹€à¸¥à¸´à¸" +confirm = "ยืนยัน" changedCredsMessage = "ข้อมูลรับรองเปลี่ยนà¹à¸›à¸¥à¸‡à¹à¸¥à¹‰à¸§!" chooseFile = "เลือà¸à¹„ฟล์" close = "ปิด" @@ -146,6 +147,7 @@ insufficientCredits = "เครดิตไม่เพียงพอ ต้ loadingCredits = "à¸à¸³à¸¥à¸±à¸‡à¸•รวจสอบเครดิต..." loadingProStatus = "à¸à¸³à¸¥à¸±à¸‡à¸•รวจสอบสถานะà¸à¸²à¸£à¸ªà¸¡à¸±à¸„รสมาชิà¸..." noticeTopUpOrPlan = "เครดิตไม่พอ โปรดเติมเครดิตหรืออัปเà¸à¸£à¸”เป็นà¹à¸œà¸™" +accessInvite = "เชิà¸" [account] accountSettings = "à¸à¸²à¸£à¸•ั้งค่าบัà¸à¸Šà¸µ" @@ -1427,6 +1429,34 @@ title = "à¸à¸²à¸£à¸›à¸£à¸°à¸¡à¸§à¸¥à¸œà¸¥" description = "เวลาสูงสุดที่รอà¸à¸²à¸£à¸›à¸£à¸°à¸¡à¸§à¸¥à¸œà¸¥à¸à¹ˆà¸­à¸™à¸£à¸²à¸¢à¸‡à¸²à¸™à¸‚้อผิดพลาด" label = "เวลา Timeout à¸à¸²à¸£à¸›à¸£à¸°à¸¡à¸§à¸¥à¸œà¸¥ (วินาที)" +[admin.settings.storage] +description = "ควบคุมà¸à¸²à¸£à¸ˆà¸±à¸”เà¸à¹‡à¸šà¹„ฟล์บนเซิร์ฟเวอร์à¹à¸¥à¸°à¸•ัวเลือà¸à¸à¸²à¸£à¹à¸Šà¸£à¹Œ" +title = "à¸à¸²à¸£à¸ˆà¸±à¸”เà¸à¹‡à¸šà¹„ฟล์à¹à¸¥à¸°à¸à¸²à¸£à¹à¸Šà¸£à¹Œ" + +[admin.settings.storage.enabled] +description = "อนุà¸à¸²à¸•ให้ผู้ใช้จัดเà¸à¹‡à¸šà¹„ฟล์บนเซิร์ฟเวอร์" +label = "เปิดใช้งานà¸à¸²à¸£à¸ˆà¸±à¸”เà¸à¹‡à¸šà¹„ฟล์บนเซิร์ฟเวอร์" + +[admin.settings.storage.sharing.email] +description = "อนุà¸à¸²à¸•ให้à¹à¸Šà¸£à¹Œà¸”้วยที่อยู่อีเมล" +label = "เปิดใช้งานà¸à¸²à¸£à¹à¸Šà¸£à¹Œà¸œà¹ˆà¸²à¸™à¸­à¸µà¹€à¸¡à¸¥" +mailLink = "à¸à¸³à¸«à¸™à¸”ค่าเมล" +mailNote = "ต้องà¸à¸³à¸«à¸™à¸”ค่าเมล " + +[admin.settings.storage.sharing.enabled] +description = "อนุà¸à¸²à¸•ให้ผู้ใช้à¹à¸Šà¸£à¹Œà¹„ฟล์ที่จัดเà¸à¹‡à¸šà¹„ว้" +label = "เปิดใช้งานà¸à¸²à¸£à¹à¸Šà¸£à¹Œ" + +[admin.settings.storage.sharing.links] +description = "อนุà¸à¸²à¸•ให้à¹à¸Šà¸£à¹Œà¸œà¹ˆà¸²à¸™à¸¥à¸´à¸‡à¸à¹Œà¸—ี่ต้องลงชื่อเข้าใช้" +frontendUrlLink = "à¸à¸³à¸«à¸™à¸”ค่าในà¸à¸²à¸£à¸•ั้งค่าระบบ" +frontendUrlNote = "ต้องมี Frontend URL " +label = "เปิดใช้งานลิงà¸à¹Œà¹à¸Šà¸£à¹Œ" + +[admin.settings.storage.signing.enabled] +description = "อนุà¸à¸²à¸•ให้ผู้ใช้สร้างเซสชันà¸à¸²à¸£à¸¥à¸‡à¸™à¸²à¸¡à¹€à¸­à¸à¸ªà¸²à¸£à¸«à¸¥à¸²à¸¢à¸œà¸¹à¹‰à¹€à¸‚้าร่วม จำเป็นต้องเปิดใช้งานà¸à¸²à¸£à¸ˆà¸±à¸”เà¸à¹‡à¸šà¹„ฟล์บนเซิร์ฟเวอร์" +label = "เปิดใช้งานà¸à¸²à¸£à¸¥à¸‡à¸™à¸²à¸¡à¹à¸šà¸šà¸à¸¥à¸¸à¹ˆà¸¡ (อัลฟา)" + [admin.settings.unsavedChanges] cancel = "à¹à¸à¹‰à¹„ขต่อ" discard = "ละทิ้งà¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡" @@ -2059,7 +2089,19 @@ numbers = "ตัวเลข/ช่วง: 5, 10-20" progressions = "ลำดับขั้น: 3n, 4n+1" [certSign] +allSigned = "ผู้เข้าร่วมทั้งหมดได้ลงนามà¹à¸¥à¹‰à¸§ พร้อมสำหรับà¸à¸²à¸£à¸ªà¸£à¸¸à¸›à¸œà¸¥" +awaitingSignatures = "รอà¸à¸²à¸£à¸¥à¸‡à¸™à¸²à¸¡" +signatureProgress = "{{signedCount}}/{{totalCount}} ลายเซ็น" chooseCertificate = "เลือà¸à¹„ฟล์ใบรับรอง" +declined = "ปà¸à¸´à¹€à¸ªà¸˜à¹à¸¥à¹‰à¸§" +fetchFailed = "ไม่สามารถโหลดข้อมูลà¸à¸²à¸£à¸¥à¸‡à¸™à¸²à¸¡à¹„ด้" +finalized = "สรุปผลà¹à¸¥à¹‰à¸§" +notified = "รอดำเนินà¸à¸²à¸£" +partialNote = "คุณสามารถสรุปผลล่วงหน้าด้วยลายเซ็นปัจจุบันได้ ผู้เข้าร่วมที่ยังไม่ลงนามจะถูà¸à¸•ัดออà¸" +pending = "รอดำเนินà¸à¸²à¸£" +readyToFinalize = "พร้อมสรุปผล" +signed = "ลงนามà¹à¸¥à¹‰à¸§" +viewed = "เปิดดูà¹à¸¥à¹‰à¸§" chooseJksFile = "เลือà¸à¹„ฟล์ JKS" chooseP12File = "เลือà¸à¹„ฟล์ PKCS12" choosePfxFile = "เลือà¸à¹„ฟล์ PFX" @@ -2082,6 +2124,7 @@ title = "à¸à¸²à¸£à¹€à¸‹à¹‡à¸™à¸Šà¸·à¹ˆà¸­à¸”้วยใบรับรอง" invisible = "มองไม่เห็น" stepTitle = "ลัà¸à¸©à¸“ะลายเซ็น" visible = "มองเห็นได้" +visibility = "à¸à¸²à¸£à¹à¸ªà¸”งผล" [certSign.appearance.options] title = "รายละเอียดลายเซ็น" @@ -2188,6 +2231,252 @@ bullet4 = "สามารถใช้ใบรับรองà¹à¸šà¸šà¸à¸³à¸« text = "เมื่อคุณตรวจสอบลายเซ็น เครื่องมือจะà¹à¸ˆà¹‰à¸‡à¸§à¹ˆà¸²à¸–ูà¸à¸•้องหรือไม่ ใครเป็นผู้ลงนาม ลงนามเมื่อใด à¹à¸¥à¸°à¹€à¸­à¸à¸ªà¸²à¸£à¸–ูà¸à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¸«à¸¥à¸±à¸‡à¸à¸²à¸£à¸¥à¸‡à¸™à¸²à¸¡à¸«à¸£à¸·à¸­à¹„ม่" title = "à¸à¸²à¸£à¸•รวจสอบลายเซ็น" +[certSign.collab.finalize] +button = "สรุปผลà¹à¸¥à¸°à¹‚หลด PDF ที่ลงนามà¹à¸¥à¹‰à¸§" +early = "สรุปผลด้วยลายเซ็นปัจจุบัน" + +[certSign.collab.sessionDetail] +addButton = "เพิ่มผู้เข้าร่วม" +addParticipants = "เพิ่มผู้เข้าร่วม" +addParticipantsError = "ไม่สามารถเพิ่มผู้เข้าร่วมได้" +backToList = "ย้อนà¸à¸¥à¸±à¸šà¹„ปยังเซสชัน" +deleteConfirm = "à¹à¸™à¹ˆà¹ƒà¸ˆà¸«à¸£à¸·à¸­à¹„ม่? à¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£à¸™à¸µà¹‰à¹„ม่สามารถยà¸à¹€à¸¥à¸´à¸à¹„ด้" +deleteError = "ไม่สามารถลบเซสชันได้" +deleted = "ลบเซสชันà¹à¸¥à¹‰à¸§" +deleteSession = "ลบเซสชัน" +dueDate = "วันครบà¸à¸³à¸«à¸™à¸”" +finalizeError = "ไม่สามารถสรุปผลเซสชันได้" +loadPdfError = "ไม่สามารถโหลด PDF ที่ลงนามà¹à¸¥à¹‰à¸§à¹„ด้" +loadSignedPdf = "โหลด PDF ที่ลงนามà¹à¸¥à¹‰à¸§à¹„ปยังไฟล์ที่ใช้งานอยู่" +messageLabel = "ข้อความ" +noAdditionalInfo = "ไม่มีข้อมูลเพิ่มเติม" +owner = "เจ้าของ" +participantRemoved = "ลบผู้เข้าร่วมà¹à¸¥à¹‰à¸§" +participants = "ผู้เข้าร่วม" +participantsAdded = "เพิ่มผู้เข้าร่วมเรียบร้อยà¹à¸¥à¹‰à¸§" +removeParticipant = "ลบ" +removeParticipantError = "ไม่สามารถลบผู้เข้าร่วมได้" +selectUsers = "เลือà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰..." +sessionInfo = "ข้อมูลเซสชัน" +workbenchTitle = "à¸à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£à¹€à¸‹à¸ªà¸Šà¸±à¸™" + +[certSign.collab.signRequest] +addedToFiles = "เพิ่มเอà¸à¸ªà¸²à¸£à¹„ปยังไฟล์ที่ใช้งานอยู่à¹à¸¥à¹‰à¸§" +addSignature = "เพิ่มลายเซ็นของคุณ" +addToFiles = "เพิ่มไปยังไฟล์ที่ใช้งานอยู่" +advancedSettings = "à¸à¸²à¸£à¸•ั้งค่าขั้นสูง" +backToList = "ย้อนà¸à¸¥à¸±à¸šà¹„ปยังคำขอลงนาม" +certificateChoice = "เลือà¸à¹ƒà¸šà¸£à¸±à¸šà¸£à¸­à¸‡à¸ªà¸³à¸«à¸£à¸±à¸šà¸à¸²à¸£à¸¥à¸‡à¸™à¸²à¸¡" +changeSignature = "เปลี่ยนลายเซ็น" +clearSignature = "ล้างลายเซ็น" +completeAndSign = "เสร็จสิ้นà¹à¸¥à¸°à¸¥à¸‡à¸™à¸²à¸¡" +createNewSignature = "สร้างลายเซ็นใหม่" +declineButton = "ปà¸à¸´à¹€à¸ªà¸˜" +decline = "ปà¸à¸´à¹€à¸ªà¸˜à¸„ำขอ" +deleteSelected = "ลบลายเซ็นที่เลือà¸" +drawSignature = "วาดลายเซ็นของคุณด้านล่าง" +dueDate = "วันครบà¸à¸³à¸«à¸™à¸”" +fileTooLarge = "ขนาดไฟล์ต้องน้อยà¸à¸§à¹ˆà¸² 5MB" +fontFamily = "ตระà¸à¸¹à¸¥à¸Ÿà¸­à¸™à¸•์" +fontSize = "ขนาดฟอนต์: {{size}}px" +fontSizePlaceholder = "ขนาด" +from = "จาà¸" +invalidCertFile = "โปรดเลือà¸à¹„ฟล์ใบรับรอง P12 หรือ PFX" +invalidFileType = "โปรดเลือà¸à¹„ฟล์รูปภาพ" +location = "สถานที่ (ไม่บังคับ)" +locationPlaceholder = "คุณà¸à¸³à¸¥à¸±à¸‡à¸¥à¸‡à¸™à¸²à¸¡à¸ˆà¸²à¸à¸—ี่ไหน?" +message = "ข้อความ" +noCertificate = "โปรดเลือà¸à¹„ฟล์ใบรับรอง" +noSignatures = "โปรดวางลายเซ็นอย่างน้อยหนึ่งลายเซ็นบน PDF" +p12File = "ไฟล์ใบรับรอง P12/PFX" +password = "รหัสผ่านใบรับรอง" +passwordPlaceholder = "à¸à¸£à¸­à¸à¸£à¸«à¸±à¸ªà¸œà¹ˆà¸²à¸™..." +penColor = "สีปาà¸à¸à¸²" +penSize = "ขนาดปาà¸à¸à¸²: {{size}}px" +placementActive = "คลิà¸à¸—ี่ PDF เพื่อวาง" +placeSignatureButton = "วางลายเซ็นบน PDF" +reason = "เหตุผล (ไม่บังคับ)" +reasonPlaceholder = "ทำไมคุณจึงลงนาม?" +removeImage = "นำรูปภาพออà¸" +removeCertFile = "นำไฟล์ออà¸" +savedSignatures = "ลายเซ็นที่บันทึà¸à¹„ว้" +selectFile = "เลือà¸à¹„ฟล์รูปภาพ" +selectSignatureTitle = "เลือà¸à¸«à¸£à¸·à¸­à¸ªà¸£à¹‰à¸²à¸‡à¸¥à¸²à¸¢à¹€à¸‹à¹‡à¸™" +signButton = "ลงนามเอà¸à¸ªà¸²à¸£" +signatureInfo = "à¸à¸²à¸£à¸•ั้งค่าเหล่านี้à¸à¸³à¸«à¸™à¸”โดยเจ้าของเอà¸à¸ªà¸²à¸£" +signaturePlaced = "วางลายเซ็นบนหน้าà¹à¸¥à¹‰à¸§" +signatureSettings = "à¸à¸²à¸£à¸•ั้งค่าลายเซ็น" +signatureText = "ข้อความลายเซ็น" +signatureTextPlaceholder = "à¸à¸£à¸­à¸à¸Šà¸·à¹ˆà¸­à¸‚องคุณ..." +signatureTypeLabel = "ประเภทลายเซ็น" +signingTitle = "à¸à¸²à¸£à¸¥à¸‡à¸™à¸²à¸¡" +textColor = "สีข้อความ" +typeSignature = "พิมพ์ชื่อของคุณเพื่อสร้างลายเซ็น" +uploadCert = "ใบรับรองà¸à¸³à¸«à¸™à¸”เอง" +uploadCertDesc = "ใช้ใบรับรอง P12/PFX ของคุณเอง" +uploadSignature = "อัปโหลดรูปภาพลายเซ็นของคุณ" +usePersonalCert = "ใบรับรองส่วนบุคคล" +usePersonalCertDesc = "สร้างอัตโนมัติสำหรับบัà¸à¸Šà¸µà¸‚องคุณ" +useServerCert = "ใบรับรองขององค์à¸à¸£" +useServerCertDesc = "ใบรับรองที่ใช้ร่วมà¸à¸±à¸™à¸‚ององค์à¸à¸£" +workbenchTitle = "คำขอลงนาม" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "เลือà¸à¸ªà¸µà¹€à¸ªà¹‰à¸™" +continue = "ดำเนินà¸à¸²à¸£à¸•่อ" + +[certSign.collab.signRequest.certModal] +description = "คุณได้วางลายเซ็น {{count}} รายà¸à¸²à¸£ เลือà¸à¹ƒà¸šà¸£à¸±à¸šà¸£à¸­à¸‡à¸‚องคุณเพื่อทำà¸à¸²à¸£à¸¥à¸‡à¸™à¸²à¸¡à¹ƒà¸«à¹‰à¹€à¸ªà¸£à¹‡à¸ˆà¸ªà¸´à¹‰à¸™" +sign = "ลงนามเอà¸à¸ªà¸²à¸£" +certValidating = "à¸à¸³à¸¥à¸±à¸‡à¸•รวจสอบความถูà¸à¸•้องของใบรับรอง..." +certValidUntil = "ใบรับรองใช้ได้จนถึง {{date}}" +certInvalid = "ใบรับรองไม่ถูà¸à¸•้อง: {{error}}" +certInvalidFallback = "ใบรับรองไม่ถูà¸à¸•้อง" +certNetworkError = "ไม่สามารถตรวจสอบความถูà¸à¸•้องของใบรับรองได้" +title = "à¸à¸³à¸«à¸™à¸”ค่าใบรับรอง" + +[certSign.collab.signRequest.image] +hint = "อัปโหลดรูปภาพ PNG หรือ JPG ของลายเซ็นของคุณ" + +[certSign.collab.signRequest.mode] +move = "ย้ายลายเซ็น" +place = "วางลายเซ็น" +title = "โหมดลงนามหรือย้าย" + +[certSign.collab.signRequest.modeTabs] +draw = "วาด" +image = "อัปโหลด" +text = "พิมพ์" + +[certSign.collab.signRequest.placeSignature] +message = "คลิà¸à¸šà¸™ PDF เพื่อวางลายเซ็นของคุณ" +title = "วางลายเซ็น" + +[certSign.collab.signRequest.preview] +imageAlt = "ลายเซ็นที่เลือà¸" +missing = "ไม่มีตัวอย่าง" +textFallback = "ลายเซ็น" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "ลายเซ็นà¹à¸šà¸šà¸§à¸²à¸”" +defaultImageLabel = "ลายเซ็นที่อัปโหลด" +defaultLabel = "ลายเซ็น" +defaultTextLabel = "ลายเซ็นà¹à¸šà¸šà¸žà¸´à¸¡à¸žà¹Œ" +delete = "ลบลายเซ็น" +none = "ยังไม่มีลายเซ็นที่บันทึà¸à¹„ว้" + +[certSign.collab.signRequest.signatureType] +draw = "วาด" +type = "พิมพ์" +upload = "อัปโหลด" + +[certSign.collab.signRequest.steps] +back = "ย้อนà¸à¸¥à¸±à¸š" +cancelPlacement = "ยà¸à¹€à¸¥à¸´à¸à¸à¸²à¸£à¸§à¸²à¸‡" +certificate = "ใบรับรอง" +clickMultipleTimes = "คลิà¸à¸šà¸™ PDF หลายครั้งเพื่อวางลายเซ็น ลาà¸à¸¥à¸²à¸¢à¹€à¸‹à¹‡à¸™à¹ƒà¸”ๆ เพื่อย้ายหรือปรับขนาด" +clickToPlace = "คลิà¸à¸šà¸™ PDF ตำà¹à¸«à¸™à¹ˆà¸‡à¸—ี่คุณต้องà¸à¸²à¸£à¹ƒà¸«à¹‰à¸¥à¸²à¸¢à¹€à¸‹à¹‡à¸™à¸›à¸£à¸²à¸à¸" +continue = "ดำเนินà¸à¸²à¸£à¸•่อไปยังà¸à¸²à¸£à¹€à¸¥à¸·à¸­à¸à¹ƒà¸šà¸£à¸±à¸šà¸£à¸­à¸‡" +continueToPlacement = "ดำเนินà¸à¸²à¸£à¸•่อไปยังà¸à¸²à¸£à¸§à¸²à¸‡" +continueToReview = "ดำเนินà¸à¸²à¸£à¸•่อไปยังà¸à¸²à¸£à¸•รวจทาน" +createSignature = "สร้างลายเซ็น" +invisible = "ไม่à¹à¸ªà¸”ง" +location = "สถานที่:" +multipleSignatures = "จะมีà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸¥à¸²à¸¢à¹€à¸‹à¹‡à¸™ {{count}} รายà¸à¸²à¸£à¸à¸±à¸š PDF" +oneSignature = "จะมีà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸¥à¸²à¸¢à¹€à¸‹à¹‡à¸™ 1 รายà¸à¸²à¸£à¸à¸±à¸š PDF" +placeOnPdf = "วางบน PDF" +reason = "เหตุผล:" +reviewTitle = "ตรวจทานà¸à¹ˆà¸­à¸™à¸¥à¸‡à¸™à¸²à¸¡" +signaturePlaced = "วางลายเซ็นบนหน้า {{page}} à¹à¸¥à¹‰à¸§ คุณสามารถปรับตำà¹à¸«à¸™à¹ˆà¸‡à¹„ด้โดยà¸à¸²à¸£à¸„ลิà¸à¸­à¸µà¸à¸„รั้ง หรือดำเนินà¸à¸²à¸£à¸•่อเพื่อà¸à¸²à¸£à¸•รวจทาน" +visible = "à¹à¸ªà¸”ง" +visibility = "à¸à¸²à¸£à¹à¸ªà¸”งผล:" +yourSignatures = "ลายเซ็นของคุณ ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "สี" +fontLabel = "ฟอนต์" +fontSizeLabel = "ขนาด" +fontSizePlaceholder = "16" +label = "ข้อความลายเซ็น" +modalHint = "à¸à¸£à¸­à¸à¸Šà¸·à¹ˆà¸­à¸‚องคุณ à¹à¸¥à¹‰à¸§à¸„ลิภดำเนินà¸à¸²à¸£à¸•่อ เพื่อวางลงบน PDF" +placeholder = "à¸à¸£à¸­à¸à¸Šà¸·à¹ˆà¸­à¸‚องคุณ..." + +[certSign.collab.participant] +certValidating = "à¸à¸³à¸¥à¸±à¸‡à¸•รวจสอบความถูà¸à¸•้องของใบรับรอง..." +certValid = "✓ ใบรับรองถูà¸à¸•้อง" +certValidUntil = " จนถึง {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "ใบรับรองไม่ถูà¸à¸•้อง" +certNetworkError = "ไม่สามารถตรวจสอบความถูà¸à¸•้องของใบรับรองได้" + +[certSign.collab.addParticipants] +add = "เพิ่มผู้เข้าร่วม {{count}} คน" +back = "ย้อนà¸à¸¥à¸±à¸š" +configureSignatures = "à¸à¸³à¸«à¸™à¸”ค่าà¸à¸²à¸£à¸•ั้งค่าลายเซ็น" +continue = "ดำเนินà¸à¸²à¸£à¸•่อไปยังà¸à¸²à¸£à¸•ั้งค่าลายเซ็น" +reasonHelp = "ตั้งค่าเหตุผลà¸à¸²à¸£à¸¥à¸‡à¸™à¸²à¸¡à¸¥à¹ˆà¸§à¸‡à¸«à¸™à¹‰à¸²à¸ªà¸³à¸«à¸£à¸±à¸šà¸œà¸¹à¹‰à¹€à¸‚้าร่วมเหล่านี้ (ไม่บังคับ พวà¸à¹€à¸‚าสามารถเปลี่ยนได้เมื่อทำà¸à¸²à¸£à¸¥à¸‡à¸™à¸²à¸¡)" +reasonPlaceholder = "เช่น อนุมัติ, ตรวจทาน..." +selectUsers = "เลือà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" + +[certSign.collab.sessionCreation] +includeSummaryPage = "รวมหน้าสรุปลายเซ็น" +includeSummaryPageHelp = "จะมีà¸à¸²à¸£à¹€à¸žà¸´à¹ˆà¸¡à¸«à¸™à¹‰à¸²à¸ªà¸£à¸¸à¸›à¸—ี่ท้ายเอà¸à¸ªà¸²à¸£à¸žà¸£à¹‰à¸­à¸¡à¹€à¸¡à¸—าดาต้าลายเซ็นทั้งหมด à¸à¸¥à¹ˆà¸­à¸‡à¸¥à¸²à¸¢à¹€à¸‹à¹‡à¸™à¹ƒà¸šà¸£à¸±à¸šà¸£à¸­à¸‡à¸”ิจิทัลบนà¹à¸•่ละหน้าจะถูà¸à¸£à¸°à¸‡à¸±à¸š (ลายเซ็นà¹à¸šà¸šà¹€à¸›à¸µà¸¢à¸à¹„ม่ได้รับผลà¸à¸£à¸°à¸—บ)" + +[certSign.collab.sessionList] +active = "ใช้งานอยู่" +finalized = "สรุปผลà¹à¸¥à¹‰à¸§" + +[certSign.collab.signatureSettings] +description = "à¸à¸³à¸«à¸™à¸”วิธีà¸à¸²à¸£à¹à¸ªà¸”งลายเซ็นสำหรับผู้เข้าร่วมทั้งหมด" +title = "ลัà¸à¸©à¸“ะลายเซ็น" + +[certSign.collab.userSelector] +inviteUsers = "เพิ่มผู้ใช้" +loadError = "ไม่สามารถโหลดผู้ใช้ได้" +noTeam = "ไม่มีทีม" +noUsers = "ไม่พบผู้ใช้อื่น" +placeholder = "เลือà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰..." + +[certSign.mobile] +panelActions = "à¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£" +panelDocument = "เอà¸à¸ªà¸²à¸£" +panelPeople = "บุคคล" + +[certSign.sessions] +deleted = "ลบเซสชันà¹à¸¥à¹‰à¸§" +fetchFailed = "ไม่สามารถโหลดรายละเอียดเซสชันได้" +finalized = "สรุปผลเซสชันà¹à¸¥à¹‰à¸§" +loaded = "โหลด PDF ที่ลงนามà¹à¸¥à¹‰à¸§" +pdfNotReady = "PDF ยังไม่พร้อม" +pdfNotReadyDesc = "à¸à¸³à¸¥à¸±à¸‡à¸ªà¸£à¹‰à¸²à¸‡ PDF ที่ลงนาม โปรดลองอีà¸à¸„รั้งในสัà¸à¸„รู่" + +[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 à¹à¸šà¸šà¸à¸³à¸«à¸™à¸”เอง" + [changeCreds] changePassword = "คุณà¸à¸³à¸¥à¸±à¸‡à¹ƒà¸Šà¹‰à¸‚้อมูลรับรองà¸à¸²à¸£à¹€à¸‚้าสู่ระบบเริ่มต้น à¸à¸£à¸¸à¸“าใส่รหัสผ่านใหม่" changeUsername = "อัปเดตชื่อผู้ใช้ของคุณ คุณจะถูà¸à¸­à¸­à¸à¸ˆà¸²à¸à¸£à¸°à¸šà¸šà¸«à¸¥à¸±à¸‡à¸ˆà¸²à¸à¸­à¸±à¸›à¹€à¸”ต" @@ -3242,6 +3531,46 @@ totalSelected = "จำนวนที่เลือà¸à¸—ั้งหมด" unsupported = "ไม่รองรับ" unzip = "à¹à¸•à¸à¹„ฟล์" uploadError = "อัปโหลดไฟล์บางไฟล์ไม่สำเร็จ" +copyCreated = "บันทึà¸à¸ªà¸³à¹€à¸™à¸²à¸¥à¸‡à¹ƒà¸™à¸­à¸¸à¸›à¸à¸£à¸“์นี้à¹à¸¥à¹‰à¸§" +copyFailed = "ไม่สามารถสร้างสำเนาได้" +leaveShare = "นำออà¸à¸ˆà¸²à¸à¸£à¸²à¸¢à¸à¸²à¸£à¸‚องฉัน" +leaveShareFailed = "ไม่สามารถนำไฟล์ที่à¹à¸Šà¸£à¹Œà¸­à¸­à¸à¹„ด้" +leaveShareSuccess = "นำออà¸à¸ˆà¸²à¸à¸£à¸²à¸¢à¸à¸²à¸£à¸—ี่à¹à¸Šà¸£à¹Œà¸‚องคุณà¹à¸¥à¹‰à¸§" +removeBoth = "นำออà¸à¸ˆà¸²à¸à¸—ั้งสองที่" +removeFilePrompt = "ไฟล์นี้ถูà¸à¸šà¸±à¸™à¸—ึà¸à¹„ว้ในอุปà¸à¸£à¸“์นี้à¹à¸¥à¸°à¸šà¸™à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸­à¸£à¹Œà¸‚องคุณ คุณต้องà¸à¸²à¸£à¸™à¸³à¸­à¸­à¸à¸ˆà¸²à¸à¸—ี่ใด?" +removeFileTitle = "นำไฟล์ออà¸" +removeLocalOnly = "เฉพาะอุปà¸à¸£à¸“์นี้" +removeServerFailed = "ไม่สามารถนำไฟล์ออà¸à¸ˆà¸²à¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸­à¸£à¹Œà¹„ด้" +removeServerOnly = "เฉพาะเซิร์ฟเวอร์" +removeServerOnlyPrompt = "ไฟล์นี้ถูà¸à¸ˆà¸±à¸”เà¸à¹‡à¸šà¹„ว้เฉพาะบนเซิร์ฟเวอร์ของคุณ คุณต้องà¸à¸²à¸£à¸™à¸³à¸­à¸­à¸à¸ˆà¸²à¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸­à¸£à¹Œà¸«à¸£à¸·à¸­à¹„ม่?" +removeServerSuccess = "นำออà¸à¸ˆà¸²à¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸­à¸£à¹Œà¹à¸¥à¹‰à¸§" +removeSharedPrompt = "ไฟล์นี้ถูà¸à¹à¸Šà¸£à¹Œà¸à¸±à¸šà¸„ุณ คุณสามารถนำออà¸à¸ˆà¸²à¸à¸­à¸¸à¸›à¸à¸£à¸“์นี้หรือจาà¸à¸£à¸²à¸¢à¸à¸²à¸£à¸—ี่à¹à¸Šà¸£à¹Œà¸‚องคุณ" +removeSharedServerOnlyBlockedPrompt = "ไฟล์นี้ถูà¸à¹à¸Šà¸£à¹Œà¸à¸±à¸šà¸„ุณà¹à¸¥à¸°à¸ˆà¸±à¸”เà¸à¹‡à¸šà¹„ว้เฉพาะบนเซิร์ฟเวอร์" +removeSharedServerOnlyPrompt = "ไฟล์นี้ถูà¸à¹à¸Šà¸£à¹Œà¸à¸±à¸šà¸„ุณà¹à¸¥à¸°à¸ˆà¸±à¸”เà¸à¹‡à¸šà¹„ว้เฉพาะบนเซิร์ฟเวอร์ นำออà¸à¸ˆà¸²à¸à¸£à¸²à¸¢à¸à¸²à¸£à¸‚องคุณหรือไม่?" +changesNotUploaded = "ยังไม่ได้อัปโหลดà¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡" +cloudFile = "ไฟล์คลาวด์" +filterAll = "ทั้งหมด" +filterLocal = "ภายในเครื่อง" +filterSharedByMe = "ฉันเป็นผู้à¹à¸Šà¸£à¹Œ" +filterSharedWithMe = "à¹à¸Šà¸£à¹Œà¸à¸±à¸šà¸‰à¸±à¸™" +lastSynced = "ซิงค์ล่าสุด" +localOnly = "เฉพาะภายในเครื่อง" +makeCopy = "สร้างสำเนา" +owner = "เจ้าของ" +ownerUnknown = "ไม่ทราบ" +share = "à¹à¸Šà¸£à¹Œ" +shareSelected = "à¹à¸Šà¸£à¹Œà¸—ี่เลือà¸" +sharedByYou = "คุณเป็นผู้à¹à¸Šà¸£à¹Œ" +sharedEditNoticeBody = "คุณไม่มีสิทธิ์à¹à¸à¹‰à¹„ขเวอร์ชันบนเซิร์ฟเวอร์ของไฟล์นี้ à¸à¸²à¸£à¹à¸à¹‰à¹„ขใดๆ ที่คุณทำจะถูà¸à¸šà¸±à¸™à¸—ึà¸à¹€à¸›à¹‡à¸™à¸ªà¸³à¹€à¸™à¸²à¸ à¸²à¸¢à¹ƒà¸™à¹€à¸„รื่อง" +sharedEditNoticeConfirm = "รับทราบ" +sharedEditNoticeTitle = "สำเนาบนเซิร์ฟเวอร์à¹à¸šà¸šà¸­à¹ˆà¸²à¸™à¸­à¸¢à¹ˆà¸²à¸‡à¹€à¸”ียว" +sharedWithYou = "à¹à¸Šà¸£à¹Œà¸à¸±à¸šà¸„ุณ" +sharing = "à¸à¸²à¸£à¹à¸Šà¸£à¹Œ" +storageState = "ที่เà¸à¹‡à¸šà¸‚้อมูล" +synced = "ซิงค์à¹à¸¥à¹‰à¸§" +updateOnServer = "อัปเดตบนเซิร์ฟเวอร์" +uploadSelected = "อัปโหลดที่เลือà¸" +uploadToServer = "อัปโหลดไปยังเซิร์ฟเวอร์" [files] addFiles = "เพิ่มไฟล์" @@ -3367,6 +3696,77 @@ title = "เà¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¸à¸²à¸£à¸—ำให้ PDF à¹à¸šà¸™" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "เà¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¸à¸²à¸£à¸¥à¸‡à¸™à¸²à¸¡à¹à¸šà¸šà¸à¸¥à¸¸à¹ˆà¸¡" + +[groupSigning.tooltip.finalization] +bullet1 = "จะใช้ลายเซ็นทั้งหมดตามลำดับผู้เข้าร่วมที่คุณà¸à¸³à¸«à¸™à¸”" +bullet2 = "คุณสามารถสรุปผลด้วยลายเซ็นบางส่วนได้หาà¸à¸ˆà¸³à¹€à¸›à¹‡à¸™" +bullet3 = "เมื่อสรุปผลà¹à¸¥à¹‰à¸§à¸ˆà¸°à¹„ม่สามารถà¹à¸à¹‰à¹„ขเซสชันได้" +description = "เมื่อผู้เข้าร่วมทั้งหมดลงนามà¹à¸¥à¹‰à¸§ (หรือคุณเลือà¸à¸ªà¸£à¸¸à¸›à¸œà¸¥à¸¥à¹ˆà¸§à¸‡à¸«à¸™à¹‰à¸²) คุณสามารถสร้าง PDF ฉบับลงนามสุดท้ายได้" +title = "à¸à¸£à¸°à¸šà¸§à¸™à¸à¸²à¸£à¸ªà¸£à¸¸à¸›à¸œà¸¥" + +[groupSigning.tooltip.roles] +bullet1 = "เจ้าของ (คุณ): สร้างเซสชัน à¸à¸³à¸«à¸™à¸”ค่าเริ่มต้นของลายเซ็น สรุปผลเอà¸à¸ªà¸²à¸£" +bullet2 = "ผู้เข้าร่วม: สร้างลายเซ็น เลือà¸à¹ƒà¸šà¸£à¸±à¸šà¸£à¸­à¸‡ วางบน PDF" +bullet3 = "ผู้เข้าร่วมไม่สามารถà¹à¸à¹‰à¹„ขà¸à¸²à¸£à¸•ั้งค่าà¸à¸²à¸£à¹à¸ªà¸”งผล เหตุผล หรือสถานที่ของลายเซ็นได้" +description = "คุณควบคุมà¸à¸²à¸£à¸•ั้งค่าลัà¸à¸©à¸“ะลายเซ็นสำหรับผู้เข้าร่วมทั้งหมด" +title = "บทบาทของผู้เข้าร่วม" + +[groupSigning.tooltip.sequential] +bullet1 = "ผู้เข้าร่วมคนà¹à¸£à¸à¸•้องลงนามà¸à¹ˆà¸­à¸™ คนที่สองจึงจะเข้าถึงเอà¸à¸ªà¸²à¸£à¹„ด้" +bullet2 = "รับรองลำดับà¸à¸²à¸£à¸¥à¸‡à¸™à¸²à¸¡à¸—ี่ถูà¸à¸•้องตามข้อà¸à¸³à¸«à¸™à¸”ทางà¸à¸Žà¸«à¸¡à¸²à¸¢" +bullet3 = "คุณสามารถจัดเรียงลำดับผู้เข้าร่วมใหม่ได้โดยà¸à¸²à¸£à¸¥à¸²à¸à¹ƒà¸™à¸£à¸²à¸¢à¸à¸²à¸£" +description = "ผู้เข้าร่วมจะลงนามเอà¸à¸ªà¸²à¸£à¸•ามลำดับที่คุณà¸à¸³à¸«à¸™à¸” ผู้ลงนามà¹à¸•่ละคนจะได้รับà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•ือนเมื่อถึงคิว" +title = "à¸à¸²à¸£à¸¥à¸‡à¸™à¸²à¸¡à¸•ามลำดับ" + +[groupSigning.steps] +back = "ย้อนà¸à¸¥à¸±à¸š" +completed = "เสร็จสมบูรณ์" +current = "ปัจจุบัน" +stepLabel = "ขั้นตอนที่ {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "ดำเนินà¸à¸²à¸£à¸•่อไปยังà¸à¸²à¸£à¸•รวจทาน" +invisible = "ลายเซ็นจะไม่à¹à¸ªà¸”ง (เฉพาะเมทาดาต้า)" +locationLabel = "สถานที่:" +preview = "à¹à¸ªà¸”งตัวอย่าง" +reasonLabel = "เหตุผล:" +title = "à¸à¸³à¸«à¸™à¸”ค่าà¸à¸²à¸£à¸•ั้งค่าลายเซ็น" +visible = "ลายเซ็นจะà¹à¸ªà¸”งบนหน้า {{page}}" + +[groupSigning.steps.review] +document = "เอà¸à¸ªà¸²à¸£" +dueDate = "วันครบà¸à¸³à¸«à¸™à¸” (ไม่บังคับ)" +dueDatePlaceholder = "เลือà¸à¸§à¸±à¸™à¸„รบà¸à¸³à¸«à¸™à¸”..." +invisible = "ไม่à¹à¸ªà¸”ง (เฉพาะเมทาดาต้า)" +location = "สถานที่:" +logo = "โลโà¸à¹‰:" +logoHidden = "ไม่มีโลโà¸à¹‰" +logoShown = "à¹à¸ªà¸”งโลโà¸à¹‰ Stirling PDF" +participants = "ผู้เข้าร่วม" +reason = "เหตุผล:" +send = "ส่งคำขอลงนาม" +signatureSettings = "à¸à¸²à¸£à¸•ั้งค่าลายเซ็น" +title = "ตรวจทานรายละเอียดเซสชัน" +titleShort = "ตรวจทานà¹à¸¥à¸°à¸ªà¹ˆà¸‡" +visibility = "à¸à¸²à¸£à¹à¸ªà¸”งผล:" +visible = "à¹à¸ªà¸”งบนหน้า {{page}}" +participantCount = "ผู้เข้าร่วม {{count}} คนจะลงนามตามลำดับ" + +[groupSigning.steps.selectDocument] +continue = "ดำเนินà¸à¸²à¸£à¸•่อไปยังà¸à¸²à¸£à¹€à¸¥à¸·à¸­à¸à¸œà¸¹à¹‰à¹€à¸‚้าร่วม" +noFile = "โปรดเลือà¸à¹„ฟล์ PDF เดียวจาà¸à¹„ฟล์ที่ใช้งานอยู่เพื่อสร้างเซสชันà¸à¸²à¸£à¸¥à¸‡à¸™à¸²à¸¡" +selectedFile = "เอà¸à¸ªà¸²à¸£à¸—ี่เลือà¸" +title = "เลือà¸à¹€à¸­à¸à¸ªà¸²à¸£" + +[groupSigning.steps.selectParticipants] +continue = "ดำเนินà¸à¸²à¸£à¸•่อไปยังà¸à¸²à¸£à¸•ั้งค่าลายเซ็น" +count = "เลือà¸à¸œà¸¹à¹‰à¹€à¸‚้าร่วมà¹à¸¥à¹‰à¸§ {{count}} คน" +label = "เลือà¸à¸œà¸¹à¹‰à¹€à¸‚้าร่วม" +placeholder = "เลือà¸à¸œà¸¹à¹‰à¹€à¸‚้าร่วมเพื่อลงนาม..." +title = "เลือà¸à¸œà¸¹à¹‰à¹€à¸‚้าร่วม" + [getPdfInfo] downloadJson = "ดาวน์โหลด JSON" downloads = "ดาวน์โหลด" @@ -4460,7 +4860,10 @@ zoomOut = "ซูมออà¸" [viewer] cannotPreviewFile = "ไม่สามารถà¹à¸ªà¸”งตัวอย่างไฟล์ได้" +disableColorFilter = "ปิดใช้งานตัวà¸à¸£à¸­à¸‡à¸ªà¸µ" dualPageView = "มุมมองสองหน้า" +enableDarkFilter = "เปิดใช้งานตัวà¸à¸£à¸­à¸‡à¹‚ทนมืด" +enableSepiaFilter = "เปิดใช้งานตัวà¸à¸£à¸­à¸‡à¹€à¸‹à¹€à¸›à¸µà¸¢" firstPage = "หน้าà¹à¸£à¸" lastPage = "หน้าสุดท้าย" nextPage = "หน้าถัดไป" @@ -4470,6 +4873,22 @@ singlePageView = "มุมมองหน้าเดียว" unknownFile = "ไฟล์ไม่รู้จัà¸" zoomIn = "ซูมเข้า" zoomOut = "ซูมออà¸" +resetZoom = "รีเซ็ตà¸à¸²à¸£à¸‹à¸¹à¸¡" + +[viewer.nonPdf] +fileTypeBadge = "ไฟล์ {{type}}" +convertToPdf = "à¹à¸›à¸¥à¸‡à¹€à¸›à¹‡à¸™ PDF" +loading = "à¸à¸³à¸¥à¸±à¸‡à¹‚หลด..." +emptyFile = "ไฟล์ว่างเปล่า" +csvStats = "{{rows}} à¹à¸–ว · {{columns}} คอลัมน์ · {{size}}" +sortedBy = "เรียงตาม: {{column}}" +columnDefault = "คอลัมน์ {{index}}" +htmlPreviewWarning = "ตัวอย่าง HTML — อาจไม่โหลดทรัพยาà¸à¸£à¸ à¸²à¸¢à¸™à¸­à¸ · {{size}}" +htmlPreview = "ตัวอย่าง HTML" +invalidJson = "JSON ไม่ถูà¸à¸•้อง — à¹à¸ªà¸”งเนื้อหาดิบ" +textStats = "{{lines}} บรรทัด · {{size}}" +lineNumbers = "หมายเลขบรรทัด" +renderMarkdown = "à¹à¸ªà¸”งผล Markdown" [viewer.attachments] title = "ไฟล์à¹à¸™à¸š" @@ -4531,6 +4950,7 @@ toggleAttachments = "สลับไฟล์à¹à¸™à¸š" toggleTheme = "สลับธีม" language = "ภาษา" toggleAnnotations = "สลับà¸à¸²à¸£à¹à¸ªà¸”งคำอธิบายประà¸à¸­à¸š" +toggleLayers = "สลับเลเยอร์" search = "ค้นหาใน PDF" panMode = "โหมดเลื่อนดู" applyRedactionsFirst = "ใช้à¸à¸²à¸£à¸›à¸à¸›à¸´à¸”à¸à¹ˆà¸­à¸™" @@ -5407,20 +5827,72 @@ title = "พิมพ์ไฟล์" 2 = "ป้อนชื่อเครื่องพิมพ์" [quickAccess] +access = "à¸à¸²à¸£à¹€à¸‚้าถึง" +accessAddPerson = "เพิ่มบุคคลอีà¸à¸„น" +accessBack = "ย้อนà¸à¸¥à¸±à¸š" +accessCopyLink = "คัดลอà¸à¸¥à¸´à¸‡à¸à¹Œ" +accessEmail = "ที่อยู่อีเมล" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "ไฟล์" +accessGeneral = "à¸à¸²à¸£à¹€à¸‚้าถึงทั่วไป" +accessInviteTitle = "เชิà¸à¸šà¸¸à¸„คล" +accessOwner = "เจ้าของ" +accessPanel = "à¸à¸²à¸£à¹€à¸‚้าถึงเอà¸à¸ªà¸²à¸£" +accessPeople = "บุคคลที่มีสิทธิ์เข้าถึง" +accessRemove = "นำออà¸" +accessRestricted = "จำà¸à¸±à¸”" +accessRestrictedHint = "เฉพาะผู้ที่มีสิทธิ์เข้าถึงเท่านั้นที่สามารถเปิดได้" +accessRole = "บทบาท" +accessRoleCommenter = "ผู้à¹à¸ªà¸”งความคิดเห็น" +accessRoleEditor = "ผู้à¹à¸à¹‰à¹„ข" +accessRoleViewer = "ผู้ชม" +accessSelectedFile = "ไฟล์ที่เลือà¸" +accessSendInvite = "ส่งคำเชิà¸" +accessTitle = "à¸à¸²à¸£à¹€à¸‚้าถึงเอà¸à¸ªà¸²à¸£" +accessYou = "คุณ" account = "บัà¸à¸Šà¸µ" +activeSessions = "เซสชันที่ใช้งานอยู่" +activeTab = "ใช้งานอยู่" activity = "à¸à¸´à¸ˆà¸à¸£à¸£à¸¡" adminSettings = "ตั้งค่า à¹à¸­à¸”มิน" +allSessions = "ทุà¸à¹€à¸‹à¸ªà¸Šà¸±à¸™" allTools = "All Tools" automate = "ออโต้" +back = "ย้อนà¸à¸¥à¸±à¸š" +certSign = "ลงนามด้วยใบรับรอง" +completedSessions = "เซสชันที่เสร็จสิ้น" +completedTab = "เสร็จสิ้น" config = "คอนฟิà¸" +createNew = "สร้างคำขอใหม่" +createSession = "สร้างคำขอลงนาม" +dueDate = "วันครบà¸à¸³à¸«à¸™à¸” (ไม่บังคับ)" files = "ไฟล์" help = "วิธีใช้" +noActiveSessions = "ไม่มีคำขอลงนามที่ค้างอยู่หรือเซสชันที่ใช้งานอยู่" +noCompletedSessions = "ไม่มีเซสชันที่เสร็จสิ้น" +noFile = "ไม่ได้เลือà¸à¹„ฟล์" read = "อ่าน" reader = "ตัวอ่าน" +refresh = "รีเฟรช" +requestSignatures = "ขอลายเซ็น" +selectSingleFileToRequest = "เลือà¸à¹„ฟล์ PDF เพียงไฟล์เดียวเพื่อขอลายเซ็น" +selectedFile = "ไฟล์ที่เลือà¸" +selectUsers = "เลือà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¹€à¸žà¸·à¹ˆà¸­à¸¥à¸‡à¸™à¸²à¸¡" +selectUsersPlaceholder = "เลือà¸à¸œà¸¹à¹‰à¹€à¸‚้าร่วม..." +sendingRequest = "à¸à¸³à¸¥à¸±à¸‡à¸ªà¹ˆà¸‡..." settings = "ตั้งค่า" showMeAround = "พาชมรอบๆ" sign = "เซ็นชื่อ" +signatureRequests = "คำขอลายเซ็น" +signYourself = "ลงนามด้วยตนเอง" +newRequest = "คำขอใหม่" tours = "ทัวร์" +wetSign = "เพิ่มลายเซ็น" +filterMine = "ของฉัน" +filterOverdue = "เà¸à¸´à¸™à¸à¸³à¸«à¸™à¸”" +filterSigned = "ลงนามà¹à¸¥à¹‰à¸§" +filterDeclined = "ถูà¸à¸›à¸à¸´à¹€à¸ªà¸˜" +searchDocuments = "ค้นหาเอà¸à¸ªà¸²à¸£â€¦" [quickAccess.helpMenu] adminTour = "ทัวร์ผู้ดูà¹à¸¥" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "เซิร์ฟเวอร์ Stirling-PDF ขอ expired = "สถานะของคุณในระบบหมดอายุ à¸à¸£à¸¸à¸“ารีเฟรชหน้าà¹à¸¥à¸°à¸¥à¸­à¸‡à¹ƒà¸«à¸¡à¹ˆà¸­à¸µà¸à¸„รั้ง" refreshPage = "รีเฟรชหน้า" +[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 = "ลำดับลายเซ็น" + +[signatureSettings.tooltip] +header = "à¸à¸²à¸£à¸•ั้งค่าลัà¸à¸©à¸“ะลายเซ็น" + +[signatureSettings.tooltip.location] +bullet1 = "ตัวอย่าง: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "ไม่ใช่ตำà¹à¸«à¸™à¹ˆà¸‡à¸šà¸™à¸«à¸™à¹‰à¸²" +bullet3 = "อาจจำเป็นสำหรับบางเขตอำนาจศาลทางà¸à¸Žà¸«à¸¡à¸²à¸¢" +description = "สถานที่ทางภูมิศาสตร์ (ไม่บังคับ) ที่มีà¸à¸²à¸£à¸¥à¸‡à¸™à¸²à¸¡ จะถูà¸à¸ˆà¸±à¸”เà¸à¹‡à¸šà¹ƒà¸™à¹€à¸¡à¸—าดาต้าใบรับรอง" +title = "สถานที่ลายเซ็น" + +[signatureSettings.tooltip.logo] +bullet1 = "à¹à¸ªà¸”งเคียงข้างà¸à¸±à¸šà¸¥à¸²à¸¢à¹€à¸‹à¹‡à¸™à¹à¸¥à¸°à¸‚้อความ" +bullet2 = "รองรับรูปà¹à¸šà¸š PNG, JPG" +bullet3 = "ช่วยเพิ่มความเป็นมืออาชีพ" +description = "เพิ่มโลโà¸à¹‰à¸šà¸£à¸´à¸©à¸±à¸—ให้à¸à¸±à¸šà¸¥à¸²à¸¢à¹€à¸‹à¹‡à¸™à¸—ี่มองเห็นได้เพื่อสร้างà¹à¸šà¸£à¸™à¸”์à¹à¸¥à¸°à¹€à¸žà¸´à¹ˆà¸¡à¸„วามน่าเชื่อถือ" +title = "โลโà¸à¹‰à¸šà¸£à¸´à¸©à¸±à¸—" + +[signatureSettings.tooltip.reason] +bullet1 = "ตัวอย่าง: \"Approval\", \"Contract Agreement\", \"Review Complete\"" +bullet2 = "à¹à¸ªà¸”งในคุณสมบัติลายเซ็นของ PDF" +bullet3 = "มีประโยชน์สำหรับบันทึà¸à¸•รวจสอบà¹à¸¥à¸°à¸à¸²à¸£à¸›à¸à¸´à¸šà¸±à¸•ิตามข้อà¸à¸³à¸«à¸™à¸”" +description = "ข้อความอธิบายเหตุผลที่ลงนาม (ไม่บังคับ) จะถูà¸à¸ˆà¸±à¸”เà¸à¹‡à¸šà¹ƒà¸™à¹€à¸¡à¸—าดาต้าใบรับรอง" +title = "เหตุผลของลายเซ็น" + +[signatureSettings.tooltip.visibility] +bullet1 = "à¹à¸ªà¸”ง: ลายเซ็นปราà¸à¸à¸šà¸™ PDF พร้อมลัà¸à¸©à¸“ะเฉพาะ" +bullet2 = "ไม่à¹à¸ªà¸”ง: à¸à¸±à¸‡à¹ƒà¸šà¸£à¸±à¸šà¸£à¸­à¸‡à¹‚ดยไม่มีรอยภาพ" +bullet3 = "ลายเซ็นที่ไม่à¹à¸ªà¸”งยังคงให้à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸”้วยà¸à¸²à¸£à¹€à¸‚้ารหัส" +description = "ควบคุมว่าลายเซ็นจะà¹à¸ªà¸”งบนเอà¸à¸ªà¸²à¸£à¸«à¸£à¸·à¸­à¸à¸±à¸‡à¹à¸šà¸šà¹„ม่à¹à¸ªà¸”ง" +title = "à¸à¸²à¸£à¹à¸ªà¸”งผลของลายเซ็น" + [settings.configuration] advanced = "ขั้นสูง" database = "à¸à¸²à¸™à¸‚้อมูล" endpoints = "Endpoints" features = "ฟีเจอร์" +storageSharing = "à¸à¸²à¸£à¸ˆà¸±à¸”เà¸à¹‡à¸šà¹„ฟล์à¹à¸¥à¸°à¸à¸²à¸£à¹à¸Šà¸£à¹Œ" systemSettings = "à¸à¸²à¸£à¸•ั้งค่าระบบ" title = "à¸à¸²à¸£à¸à¸³à¸«à¸™à¸”ค่า" @@ -6332,10 +6868,13 @@ title = "ลงชื่อเข้าใช้ Stirling" [setup.selfhosted] link = "หรือเชื่อมต่อà¸à¸±à¸šà¸šà¸±à¸à¸Šà¸µà¹à¸šà¸š self-hosted" subtitle = "ป้อนข้อมูลรับรองของเซิร์ฟเวอร์ของคุณ" +changeServerLocked = "องค์à¸à¸£à¸‚องคุณจำà¸à¸±à¸”à¹à¸­à¸›à¸™à¸µà¹‰à¹„ว้à¸à¸±à¸šà¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸­à¸£à¹Œà¹€à¸‰à¸žà¸²à¸°" switchToLocal = "ใช้เครื่องมือภายในเครื่องà¹à¸—น" title = "ลงชื่อเข้าใช้เซิร์ฟเวอร์" [setup.selfhosted.unreachable] +changeServer = "เชื่อมต่อà¸à¸±à¸šà¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸­à¸£à¹Œà¸­à¸·à¹ˆà¸™" +changeServerLocked = "องค์à¸à¸£à¸‚องคุณจำà¸à¸±à¸”à¹à¸­à¸›à¸™à¸µà¹‰à¹„ว้à¸à¸±à¸šà¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸­à¸£à¹Œà¹€à¸‰à¸žà¸²à¸°" continueOffline = "ใช้เครื่องมือภายในเครื่องà¹à¸—น" message = "ไม่สามารถเข้าถึง {{url}} โปรดตรวจสอบว่าเซิร์ฟเวอร์à¸à¸³à¸¥à¸±à¸‡à¸—ำงานà¹à¸¥à¸°à¹€à¸‚้าถึงได้" retry = "ลองอีà¸à¸„รั้ง" @@ -6529,6 +7068,15 @@ saved = "ที่บันทึà¸à¹„ว้" text = "ข้อความ" title = "ประเภทลายเซ็น" +[signRequest] +declined = "คำขอลงนามถูà¸à¸›à¸à¸´à¹€à¸ªà¸˜" +fetchFailed = "ไม่สามารถโหลดคำขอลงนามได้" +signed = "ลงนามเอà¸à¸ªà¸²à¸£à¸ªà¸³à¹€à¸£à¹‡à¸ˆ" + +[signSession] +createFailed = "ไม่สามารถสร้างคำขอลงนามได้" +created = "ส่งคำขอลงนามà¹à¸¥à¹‰à¸§" + [signup] accountCreatedSuccessfully = "สร้างบัà¸à¸Šà¸µà¸ªà¸³à¹€à¸£à¹‡à¸ˆ! ตอนนี้คุณสามารถลงชื่อเข้าใช้ได้" alreadyHaveAccount = "มีบัà¸à¸Šà¸µà¸­à¸¢à¸¹à¹ˆà¹à¸¥à¹‰à¸§? ลงชื่อเข้าใช้" @@ -6807,6 +7355,106 @@ title = "à¹à¸šà¹ˆà¸‡à¹„ฟล์ PDF ตามหมวดหมู่" [splitPdfByChapters] tags = "à¹à¸¢à¸,บท,บุ๊à¸à¸¡à¸²à¸£à¹Œà¸,จัดระเบียบ" +[storageShare] +accessed = "เข้าถึงà¹à¸¥à¹‰à¸§" +accessDenied = "คุณไม่มีสิทธิ์เข้าถึงไฟล์ที่à¹à¸Šà¸£à¹Œà¸™à¸µà¹‰ โปรดขอให้เจ้าของà¹à¸Šà¸£à¹Œà¹ƒà¸«à¹‰à¸„ุณ" +accessFailed = "ไม่สามารถโหลดà¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¹„ด้" +accessDeniedBody = "คุณไม่มีสิทธิ์เข้าถึงไฟล์นี้ โปรดขอให้เจ้าของà¹à¸Šà¸£à¹Œà¹ƒà¸«à¹‰à¸„ุณ" +accessDeniedTitle = "ไม่มีสิทธิ์เข้าถึง" +accessLimitedCommenter = "สิทธิ์à¹à¸ªà¸”งความคิดเห็นจะมาเร็วๆ นี้ หาà¸à¸„ุณต้องà¸à¸²à¸£à¸”าวน์โหลด โปรดขอสิทธิ์ผู้à¹à¸à¹‰à¹„ขจาà¸à¹€à¸ˆà¹‰à¸²à¸‚อง" +accessLimitedTitle = "สิทธิ์เข้าถึงจำà¸à¸±à¸”" +accessLimitedViewer = "ลิงà¸à¹Œà¸™à¸µà¹‰à¹ƒà¸Šà¹‰à¹€à¸žà¸·à¹ˆà¸­à¸”ูเท่านั้น หาà¸à¸„ุณต้องà¸à¸²à¸£à¸”าวน์โหลด โปรดขอสิทธิ์ผู้à¹à¸à¹‰à¹„ขจาà¸à¹€à¸ˆà¹‰à¸²à¸‚อง" +createdAt = "สร้างเมื่อ" +download = "ดาวน์โหลด" +downloadFailed = "ไม่สามารถดาวน์โหลดไฟล์นี้ได้" +expiredBody = "ลิงà¸à¹Œà¹à¸Šà¸£à¹Œà¸™à¸µà¹‰à¹„ม่ถูà¸à¸•้องหรือหมดอายุà¹à¸¥à¹‰à¸§" +expiredTitle = "ลิงà¸à¹Œà¸«à¸¡à¸”อายุ" +goToLogin = "ไปที่หน้าเข้าสู่ระบบ" +loadFailed = "ไม่สามารถเปิดไฟล์ที่à¹à¸Šà¸£à¹Œà¹„ด้" +loading = "à¸à¸³à¸¥à¸±à¸‡à¹‚หลดลิงà¸à¹Œà¹à¸Šà¸£à¹Œ..." +loginPrompt = "ลงชื่อเข้าใช้เพื่อเข้าถึงไฟล์ที่à¹à¸Šà¸£à¹Œà¸™à¸µà¹‰" +loginRequired = "ต้องเข้าสู่ระบบ" +openInApp = "เปิดใน Stirling PDF" +ownerLabel = "เจ้าของ" +ownerUnknown = "ไม่ทราบ" +requiresLogin = "ไฟล์ที่à¹à¸Šà¸£à¹Œà¸™à¸µà¹‰à¸•้องเข้าสู่ระบบ" +roleCommenter = "ผู้à¹à¸ªà¸”งความคิดเห็น" +roleEditor = "ผู้à¹à¸à¹‰à¹„ข" +roleViewer = "ผู้ชม" +shareHeading = "ไฟล์ที่à¹à¸Šà¸£à¹Œ" +titleDefault = "ไฟล์ที่à¹à¸Šà¸£à¹Œ" +tryAgain = "โปรดลองใหม่ในภายหลัง" +addUser = "เพิ่ม" +commenterHint = "à¸à¸²à¸£à¹à¸ªà¸”งความคิดเห็นจะมาเร็วๆ นี้" +copied = "คัดลอà¸à¸¥à¸´à¸‡à¸à¹Œà¹„ปยังคลิปบอร์ดà¹à¸¥à¹‰à¸§" +copy = "คัดลอà¸" +copyFailed = "คัดลอà¸à¹„ม่สำเร็จ" +description = "สร้างลิงà¸à¹Œà¹à¸Šà¸£à¹Œà¸ªà¸³à¸«à¸£à¸±à¸šà¹„ฟล์นี้ ผู้ใช้ที่ลงชื่อเข้าใช้à¹à¸¥à¸°à¸¡à¸µà¸¥à¸´à¸‡à¸à¹Œà¸ªà¸²à¸¡à¸²à¸£à¸–เข้าถึงได้" +downloadsCount = "ดาวน์โหลด: {{count}}" +emailWarningBody = "นี่ดูเหมือนเป็นที่อยู่อีเมล หาà¸à¸šà¸¸à¸„คลนี้ยังไม่เป็นผู้ใช้ Stirling PDF พวà¸à¹€à¸‚าจะไม่สามารถเข้าถึงไฟล์ได้" +emailWarningConfirm = "à¹à¸Šà¸£à¹Œà¸•่อไป" +emailWarningTitle = "ที่อยู่อีเมล" +errorTitle = "à¹à¸Šà¸£à¹Œà¹„ม่สำเร็จ" +failure = "ไม่สามารถสร้างลิงà¸à¹Œà¹à¸Šà¸£à¹Œà¹„ด้ โปรดลองอีà¸à¸„รั้ง" +fileLabel = "ไฟล์" +generate = "สร้างลิงà¸à¹Œ" +generated = "สร้างลิงà¸à¹Œà¹à¸Šà¸£à¹Œà¹à¸¥à¹‰à¸§" +hideActivity = "ซ่อนà¸à¸´à¸ˆà¸à¸£à¸£à¸¡" +invalidUsername = "ป้อนชื่อผู้ใช้หรือที่อยู่อีเมลที่ถูà¸à¸•้อง" +lastAccessed = "เข้าถึงล่าสุด" +linkAccessTitle = "à¸à¸²à¸£à¹€à¸‚้าถึงผ่านลิงà¸à¹Œà¹à¸Šà¸£à¹Œ" +linkLabel = "ลิงà¸à¹Œà¹à¸Šà¸£à¹Œ" +linksDisabled = "ปิดใช้งานลิงà¸à¹Œà¹à¸Šà¸£à¹Œ" +linksDisabledBody = "ลิงà¸à¹Œà¹à¸Šà¸£à¹Œà¸–ูà¸à¸›à¸´à¸”ใช้งานโดยà¸à¸²à¸£à¸•ั้งค่าเซิร์ฟเวอร์ของคุณ" +manage = "จัดà¸à¸²à¸£à¸à¸²à¸£à¹à¸Šà¸£à¹Œ" +manageDescription = "สร้างà¹à¸¥à¸°à¸ˆà¸±à¸”à¸à¸²à¸£à¸¥à¸´à¸‡à¸à¹Œà¹€à¸žà¸·à¹ˆà¸­à¹à¸Šà¸£à¹Œà¹„ฟล์นี้" +manageLoadFailed = "ไม่สามารถโหลดลิงà¸à¹Œà¹à¸Šà¸£à¹Œà¹„ด้" +manageTitle = "จัดà¸à¸²à¸£à¸à¸²à¸£à¹à¸Šà¸£à¹Œ" +noActivity = "ยังไม่มีà¸à¸´à¸ˆà¸à¸£à¸£à¸¡" +noLinks = "ยังไม่มีลิงà¸à¹Œà¹à¸Šà¸£à¹Œà¸—ี่ใช้งานอยู่" +noSharedUsers = "ยังไม่มีผู้ใช้ที่มีสิทธิ์เข้าถึง" +removeLink = "นำลิงà¸à¹Œà¸­à¸­à¸" +removeUser = "นำออà¸" +revokeFailed = "ไม่สามารถลบลิงà¸à¹Œà¹à¸Šà¸£à¹Œà¹„ด้" +revoked = "ลบลิงà¸à¹Œà¹à¸Šà¸£à¹Œà¹à¸¥à¹‰à¸§" +roleLabel = "บทบาท" +sharingDisabled = "ปิดใช้งานà¸à¸²à¸£à¹à¸Šà¸£à¹Œà¹à¸¥à¹‰à¸§" +sharingDisabledBody = "à¸à¸²à¸£à¹à¸Šà¸£à¹Œà¸–ูà¸à¸›à¸´à¸”ใช้งานโดยà¸à¸²à¸£à¸•ั้งค่าเซิร์ฟเวอร์ของคุณ" +sharedUsersTitle = "ผู้ใช้ที่à¹à¸Šà¸£à¹Œà¹ƒà¸«à¹‰" +title = "à¹à¸Šà¸£à¹Œà¹„ฟล์" +unknownUser = "ผู้ใช้ที่ไม่รู้จัà¸" +userAddFailed = "ไม่สามารถà¹à¸Šà¸£à¹Œà¸à¸±à¸šà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸™à¸±à¹‰à¸™à¹„ด้" +userAdded = "เพิ่มผู้ใช้ไปยังรายà¸à¸²à¸£à¸à¸²à¸£à¹à¸Šà¸£à¹Œà¹à¸¥à¹‰à¸§" +usernameLabel = "ชื่อผู้ใช้หรืออีเมล" +usernamePlaceholder = "ป้อนชื่อผู้ใช้หรืออีเมล" +userRemoveFailed = "ไม่สามารถลบผู้ใช้นั้นได้" +userRemoved = "ลบผู้ใช้ออà¸à¸ˆà¸²à¸à¸£à¸²à¸¢à¸à¸²à¸£à¸à¸²à¸£à¹à¸Šà¸£à¹Œà¹à¸¥à¹‰à¸§" +viewActivity = "ดูà¸à¸´à¸ˆà¸à¸£à¸£à¸¡" +viewed = "ดูà¹à¸¥à¹‰à¸§" +viewsCount = "à¸à¸²à¸£à¸”ู: {{count}}" +downloaded = "ดาวน์โหลดà¹à¸¥à¹‰à¸§" +bulkDescription = "สร้างลิงà¸à¹Œà¹€à¸”ียวเพื่อà¹à¸Šà¸£à¹Œà¹„ฟล์ที่เลือà¸à¸—ั้งหมดà¸à¸±à¸šà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ลงชื่อเข้าใช้" +bulkTitle = "à¹à¸Šà¸£à¹Œà¹„ฟล์ที่เลือà¸" +copyLink = "คัดลอà¸à¸¥à¸´à¸‡à¸à¹Œà¹à¸Šà¸£à¹Œ" +fileCount = "{{count}} ไฟล์ที่เลือà¸" +ownerOnly = "มีเพียงเจ้าของเท่านั้นที่จัดà¸à¸²à¸£à¸à¸²à¸£à¹à¸Šà¸£à¹Œà¹„ด้" +selectSingleFile = "เลือà¸à¹„ฟล์เดียวเพื่อจัดà¸à¸²à¸£à¸à¸²à¸£à¹à¸Šà¸£à¹Œ" + +[storageUpload] +description = "อัปโหลดไฟล์ปัจจุบันไปยังพื้นที่จัดเà¸à¹‡à¸šà¸šà¸™à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸­à¸£à¹Œà¸ªà¸³à¸«à¸£à¸±à¸šà¸à¸²à¸£à¹€à¸‚้าถึงของคุณเอง" +errorTitle = "อัปโหลดล้มเหลว" +failure = "อัปโหลดล้มเหลว โปรดตรวจสอบà¸à¸²à¸£à¹€à¸‚้าสู่ระบบà¹à¸¥à¸°à¸à¸²à¸£à¸•ั้งค่าà¸à¸²à¸£à¸ˆà¸±à¸”เà¸à¹‡à¸šà¸‚องคุณ" +fileLabel = "ไฟล์" +hint = "ลิงà¸à¹Œà¸ªà¸²à¸˜à¸²à¸£à¸“ะà¹à¸¥à¸°à¹‚หมดà¸à¸²à¸£à¹€à¸‚้าถึงถูà¸à¸„วบคุมโดยà¸à¸²à¸£à¸•ั้งค่าเซิร์ฟเวอร์ของคุณ" +success = "อัปโหลดไปยังเซิร์ฟเวอร์à¹à¸¥à¹‰à¸§" +title = "อัปโหลดไปยังเซิร์ฟเวอร์" +updateButton = "อัปเดตบนเซิร์ฟเวอร์" +uploadButton = "อัปโหลดไปยังเซิร์ฟเวอร์" +bulkDescription = "นี่จะอัปโหลดไฟล์ที่เลือà¸à¹„ปยังพื้นที่จัดเà¸à¹‡à¸šà¸šà¸™à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸­à¸£à¹Œà¸‚องคุณ" +bulkTitle = "อัปโหลดไฟล์ที่เลือà¸" +fileCount = "{{count}} ไฟล์ที่เลือà¸" +more = " +{{count}} รายà¸à¸²à¸£à¹€à¸žà¸´à¹ˆà¸¡à¹€à¸•ิม" + [storage] approximateSize = "ขนาดโดยประมาณ" fileTooLarge = "ไฟล์มีขนาดใหà¸à¹ˆà¹€à¸à¸´à¸™à¹„ป ขนาดสูงสุดต่อไฟล์คือ" @@ -7153,6 +7801,30 @@ title = "ดู/à¹à¸à¹‰à¹„ข PDF" [warning] tooltipTitle = "คำเตือน" +[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 = "อัปโหลดรูปภาพลายเซ็น" + [watermark] completed = "เพิ่มวอเตอร์มาร์à¸à¹à¸¥à¹‰à¸§" desc = "เพิ่มวอเตอร์มาร์à¸à¸‚้อความหรือรูปภาพให้à¸à¸±à¸šà¹„ฟล์ PDF" @@ -7333,6 +8005,7 @@ activeSession = "เซสชันที่ใช้งานอยู่" addMembers = "เพิ่มสมาชิà¸" admin = "ผู้ดูà¹à¸¥" confirmDelete = "คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¸«à¸£à¸·à¸­à¹„ม่ว่าต้องà¸à¸²à¸£à¸¥à¸šà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸™à¸µà¹‰ à¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£à¸™à¸µà¹‰à¹„ม่สามารถย้อนà¸à¸¥à¸±à¸šà¹„ด้" +confirmUnlock = "คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¸«à¸£à¸·à¸­à¹„ม่ว่าต้องà¸à¸²à¸£à¸›à¸¥à¸”ล็อà¸à¸šà¸±à¸à¸Šà¸µà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸™à¸µà¹‰?" deleteUser = "ลบผู้ใช้" deleteUserError = "ลบผู้ใช้ไม่สำเร็จ" deleteUserSuccess = "ลบผู้ใช้เรียบร้อยà¹à¸¥à¹‰à¸§" @@ -7341,6 +8014,8 @@ disable = "ปิดใช้งาน" disabled = "ปิดà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™" editRole = "à¹à¸à¹‰à¹„ขบทบาท" enable = "เปิดใช้งาน" +locked = "ถูà¸à¸¥à¹‡à¸­à¸" +lockedBadge = "ถูà¸à¸¥à¹‡à¸­à¸" loading = "à¸à¸³à¸¥à¸±à¸‡à¹‚หลดรายชื่อ..." loginRequired = "เปิดโหมดล็อà¸à¸­à¸´à¸™à¸à¹ˆà¸­à¸™" member = "สมาชิà¸" @@ -7350,6 +8025,9 @@ searchMembers = "ค้นหาสมาชิà¸..." status = "สถานะ" team = "ทีม" title = "บุคคล" +unlockAccount = "ปลดล็อà¸à¸šà¸±à¸à¸Šà¸µ" +unlockUserError = "ปลดล็อà¸à¸šà¸±à¸à¸Šà¸µà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸¥à¹‰à¸¡à¹€à¸«à¸¥à¸§" +unlockUserSuccess = "ปลดล็อà¸à¸šà¸±à¸à¸Šà¸µà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸ªà¸³à¹€à¸£à¹‡à¸ˆà¹à¸¥à¹‰à¸§" user = "ผู้ใช้" [workspace.people.actions] diff --git a/frontend/public/locales/tr-TR/translation.toml b/frontend/public/locales/tr-TR/translation.toml index d835eef99d..56e74ebbf4 100644 --- a/frontend/public/locales/tr-TR/translation.toml +++ b/frontend/public/locales/tr-TR/translation.toml @@ -8,6 +8,7 @@ black = "Siyah" blue = "Mavi" bored = "Sıkıldınız mı?" cancel = "İptal" +confirm = "Onayla" changedCredsMessage = "Bilgiler deÄŸiÅŸtirildi!" chooseFile = "Dosya Seç" close = "Kapat" @@ -146,6 +147,7 @@ insufficientCredits = "Yetersiz kredi. Gerekli: {{requiredCredits}}, Mevcut: {{c loadingCredits = "Krediler kontrol ediliyor..." loadingProStatus = "Abonelik durumu kontrol ediliyor..." noticeTopUpOrPlan = "Yeterli kredi yok, lütfen bakiye yükleyin veya bir plana yükseltin" +accessInvite = "Davet Et" [account] accountSettings = "Hesap Ayarları" @@ -1427,6 +1429,34 @@ title = "İşleme" description = "Hata bildirmeden önce bir iÅŸin iÅŸlenmesi için beklenecek azami süre." label = "İşleme Zaman Aşımı (saniye)" +[admin.settings.storage] +description = "Sunucu depolama ve paylaşım seçeneklerini kontrol edin." +title = "Dosya Depolama ve PaylaÅŸma" + +[admin.settings.storage.enabled] +description = "Kullanıcıların dosyaları sunucuda depolamasına izin verin." +label = "Sunucu Dosya Depolamasını EtkinleÅŸtir" + +[admin.settings.storage.sharing.email] +description = "E-posta adresleriyle paylaÅŸmaya izin verin." +label = "E-posta ile Paylaşımı EtkinleÅŸtir" +mailLink = "Posta Ayarlarını Yapılandır" +mailNote = "Posta yapılandırması gerekir. " + +[admin.settings.storage.sharing.enabled] +description = "Kullanıcıların depolanan dosyaları paylaÅŸmasına izin verin." +label = "Paylaşımı EtkinleÅŸtir" + +[admin.settings.storage.sharing.links] +description = "Oturum açmayı gerektiren baÄŸlantılarla paylaÅŸmaya izin verin." +frontendUrlLink = "Sistem Ayarlarında Yapılandır" +frontendUrlNote = "Ön uç URL'si gerekir. " +label = "Paylaşım BaÄŸlantılarını EtkinleÅŸtir" + +[admin.settings.storage.signing.enabled] +description = "Kullanıcıların çok katılımcılı belge imzalama oturumları oluÅŸturmasına izin verin. Sunucu dosya depolamasının etkin olmasını gerektirir." +label = "Grup İmzalama'yı EtkinleÅŸtir (Alfa)" + [admin.settings.unsavedChanges] cancel = "Düzenlemeye Devam Et" discard = "DeÄŸiÅŸikliklerden Vazgeç" @@ -2059,7 +2089,19 @@ numbers = "Sayılar/aralıklar: 5, 10-20" progressions = "İlerlemeler: 3n, 4n+1" [certSign] +allSigned = "Tüm katılımcılar imzaladı. Sonlandırmaya hazır." +awaitingSignatures = "İmzalar bekleniyor" +signatureProgress = "{{signedCount}}/{{totalCount}} imza" chooseCertificate = "Sertifika Dosyası Seç" +declined = "Reddedildi" +fetchFailed = "İmzalama verileri yüklenemedi" +finalized = "Sonlandırıldı" +notified = "Beklemede" +partialNote = "Mevcut imzalarla erken sonlandırabilirsiniz. İmzalamayan katılımcılar hariç tutulacaktır." +pending = "Beklemede" +readyToFinalize = "Sonlandırmaya hazır" +signed = "İmzalandı" +viewed = "Görüntülendi" chooseJksFile = "JKS Dosyası Seç" chooseP12File = "PKCS12 Dosyası Seç" choosePfxFile = "PFX Dosyası Seç" @@ -2082,6 +2124,7 @@ title = "Sertifika İmzalama" invisible = "Görünmez" stepTitle = "İmza Görünümü" visible = "Görünür" +visibility = "Görünürlük" [certSign.appearance.options] title = "İmza Ayrıntıları" @@ -2188,6 +2231,252 @@ 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" +[certSign.collab.finalize] +button = "Sonlandır ve İmzalı PDF'yi Yükle" +early = "Mevcut İmzalarla Sonlandır" + +[certSign.collab.sessionDetail] +addButton = "Katılımcı Ekle" +addParticipants = "Katılımcı Ekle" +addParticipantsError = "Katılımcılar eklenemedi" +backToList = "Oturumlara Geri Dön" +deleteConfirm = "Emin misiniz? Bu iÅŸlem geri alınamaz." +deleteError = "Oturum silinemedi" +deleted = "Oturum silindi" +deleteSession = "Oturumu Sil" +dueDate = "Son Tarih" +finalizeError = "Oturum sonlandırılamadı" +loadPdfError = "İmzalı PDF yüklenemedi" +loadSignedPdf = "İmzalı PDF'yi Aktif Dosyalara Yükle" +messageLabel = "Mesaj" +noAdditionalInfo = "Ek bilgi yok" +owner = "Sahip" +participantRemoved = "Katılımcı kaldırıldı" +participants = "Katılımcılar" +participantsAdded = "Katılımcılar baÅŸarıyla eklendi" +removeParticipant = "Kaldır" +removeParticipantError = "Katılımcı kaldırılamadı" +selectUsers = "Kullanıcı seçin..." +sessionInfo = "Oturum Bilgileri" +workbenchTitle = "Oturum Yönetimi" + +[certSign.collab.signRequest] +addedToFiles = "Belge aktif dosyalara eklendi" +addSignature = "İmzanızı Ekleyin" +addToFiles = "Aktif Dosyalara Ekle" +advancedSettings = "GeliÅŸmiÅŸ Ayarlar" +backToList = "İmza İsteklerine Geri Dön" +certificateChoice = "İmzalamak için bir sertifika seçin" +changeSignature = "İmzayı deÄŸiÅŸtir" +clearSignature = "İmzayı Temizle" +completeAndSign = "Tamamla ve İmzala" +createNewSignature = "Yeni İmza OluÅŸtur" +declineButton = "Reddet" +decline = "İsteÄŸi Reddet" +deleteSelected = "Seçili imzayı sil" +drawSignature = "İmzanızı aÅŸağıda çizin" +dueDate = "Son Tarih" +fileTooLarge = "Dosya boyutu 5MB'den küçük olmalıdır" +fontFamily = "Yazı Tipi Ailesi" +fontSize = "Yazı Boyutu: {{size}}px" +fontSizePlaceholder = "Boyut" +from = "Kimden" +invalidCertFile = "Lütfen bir P12 veya PFX sertifika dosyası seçin" +invalidFileType = "Lütfen bir resim dosyası seçin" +location = "Konum (İsteÄŸe baÄŸlı)" +locationPlaceholder = "Nereden imzalıyorsunuz?" +message = "Mesaj" +noCertificate = "Lütfen bir sertifika dosyası seçin" +noSignatures = "Lütfen PDF üzerine en az bir imza yerleÅŸtirin" +p12File = "P12/PFX Sertifika Dosyası" +password = "Sertifika Parolası" +passwordPlaceholder = "Parolayı girin..." +penColor = "Kalem Rengi" +penSize = "Kalem Boyutu: {{size}}px" +placementActive = "YerleÅŸtirmek için PDF'ye tıklayın" +placeSignatureButton = "İmzayı PDF'ye YerleÅŸtir" +reason = "Neden (İsteÄŸe baÄŸlı)" +reasonPlaceholder = "Neden imzalıyorsunuz?" +removeImage = "Görseli Kaldır" +removeCertFile = "Dosyayı Kaldır" +savedSignatures = "KaydedilmiÅŸ İmzalar" +selectFile = "Görsel Dosya Seç" +selectSignatureTitle = "İmza Seçin veya OluÅŸturun" +signButton = "Belgeyi İmzala" +signatureInfo = "Bu ayarlar belge sahibi tarafından yapılandırılmıştır" +signaturePlaced = "İmza sayfaya yerleÅŸtirildi" +signatureSettings = "İmza Ayarları" +signatureText = "İmza Metni" +signatureTextPlaceholder = "Adınızı girin..." +signatureTypeLabel = "İmza Türü" +signingTitle = "İmzalama" +textColor = "Metin Rengi" +typeSignature = "İmza oluÅŸturmak için adınızı yazın" +uploadCert = "Özel Sertifika" +uploadCertDesc = "Kendi P12/PFX sertifikanızı kullanın" +uploadSignature = "İmza görselinizi yükleyin" +usePersonalCert = "KiÅŸisel Sertifika" +usePersonalCertDesc = "Hesabınız için otomatik oluÅŸturulur" +useServerCert = "KuruluÅŸ Sertifikası" +useServerCertDesc = "Paylaşılan kuruluÅŸ sertifikası" +workbenchTitle = "İmza İsteÄŸi" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "VuruÅŸ rengini seçin" +continue = "Devam et" + +[certSign.collab.signRequest.certModal] +description = "{{count}} imza yerleÅŸtirdiniz. İmzalamayı tamamlamak için sertifikanızı seçin." +sign = "Belgeyi İmzala" +certValidating = "Sertifika doÄŸrulanıyor..." +certValidUntil = "Sertifika {{date}} tarihine kadar geçerli" +certInvalid = "Sertifika geçersiz: {{error}}" +certInvalidFallback = "Geçersiz sertifika" +certNetworkError = "Sertifika doÄŸrulanamadı" +title = "Sertifikayı Yapılandır" + +[certSign.collab.signRequest.image] +hint = "İmzanızın PNG veya JPG görselini yükleyin" + +[certSign.collab.signRequest.mode] +move = "İmzayı Taşı" +place = "İmzayı YerleÅŸtir" +title = "İmzalama veya taşıma modu" + +[certSign.collab.signRequest.modeTabs] +draw = "Çiz" +image = "Yükle" +text = "Yaz" + +[certSign.collab.signRequest.placeSignature] +message = "İmzanızı yerleÅŸtirmek için PDF'ye tıklayın" +title = "İmzayı YerleÅŸtir" + +[certSign.collab.signRequest.preview] +imageAlt = "Seçilen imza" +missing = "Önizleme yok" +textFallback = "İmza" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Çizim imzası" +defaultImageLabel = "Yüklenen imza" +defaultLabel = "İmza" +defaultTextLabel = "Yazılı imza" +delete = "İmzayı sil" +none = "Kayıtlı imza yok" + +[certSign.collab.signRequest.signatureType] +draw = "Çiz" +type = "Yaz" +upload = "Yükle" + +[certSign.collab.signRequest.steps] +back = "Geri" +cancelPlacement = "YerleÅŸtirmeyi İptal Et" +certificate = "Sertifika" +clickMultipleTimes = "İmzaları yerleÅŸtirmek için PDF'ye birden çok kez tıklayın. Taşımak veya yeniden boyutlandırmak için imzayı sürükleyin." +clickToPlace = "İmzanızın görünmesini istediÄŸiniz yere PDF üzerinde tıklayın." +continue = "Sertifika Seçimine Devam Et" +continueToPlacement = "YerleÅŸtirmeye Devam Et" +continueToReview = "Gözden Geçirmeye Devam Et" +createSignature = "İmza OluÅŸtur" +invisible = "Görünmez" +location = "Konum:" +multipleSignatures = "{{count}} imza PDF'ye uygulanacak" +oneSignature = "1 imza PDF'ye uygulanacak" +placeOnPdf = "PDF'ye YerleÅŸtir" +reason = "Neden:" +reviewTitle = "İmzalamadan Önce Gözden Geçirin" +signaturePlaced = "İmza {{page}}. sayfaya yerleÅŸtirildi. Tekrar tıklayarak konumu ayarlayabilir veya gözden geçirmeye devam edebilirsiniz." +visible = "Görünür" +visibility = "Görünürlük:" +yourSignatures = "İmzalarınız ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Renk" +fontLabel = "Yazı Tipi" +fontSizeLabel = "Boyut" +fontSizePlaceholder = "16" +label = "İmza Metni" +modalHint = "Adınızı girin, ardından PDF'ye yerleÅŸtirmek için Devam et'e tıklayın." +placeholder = "Adınızı girin..." + +[certSign.collab.participant] +certValidating = "Sertifika doÄŸrulanıyor..." +certValid = "✓ Sertifika geçerli" +certValidUntil = " {{date}} tarihine kadar" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Geçersiz sertifika" +certNetworkError = "Sertifika doÄŸrulanamadı" + +[certSign.collab.addParticipants] +add = "{{count}} Katılımcı Ekle" +back = "Geri" +configureSignatures = "İmza Ayarlarını Yapılandır" +continue = "İmza Ayarlarına Devam Et" +reasonHelp = "Bu katılımcılar için önceden bir imzalama nedeni belirleyin (isteÄŸe baÄŸlı, imzalarken deÄŸiÅŸtirebilirler)" +reasonPlaceholder = "örn. Onay, İnceleme..." +selectUsers = "Kullanıcıları Seç" + +[certSign.collab.sessionCreation] +includeSummaryPage = "İmza Özeti Sayfasını Dahil Et" +includeSummaryPageHelp = "Tüm imza metaverilerini içeren bir özet sayfası sona eklenecek. Tek tek sayfalardaki dijital sertifika imza kutuları gizlenecek (ıslak imzalar etkilenmez)." + +[certSign.collab.sessionList] +active = "Aktif" +finalized = "Sonlandırıldı" + +[certSign.collab.signatureSettings] +description = "Tüm katılımcılar için imzaların nasıl görüneceÄŸini yapılandırın" +title = "İmza Görünümü" + +[certSign.collab.userSelector] +inviteUsers = "Kullanıcı Ekle" +loadError = "Kullanıcılar yüklenemedi" +noTeam = "Ekip Yok" +noUsers = "BaÅŸka kullanıcı bulunamadı." +placeholder = "Kullanıcı seçin..." + +[certSign.mobile] +panelActions = "İşlemler" +panelDocument = "Belge" +panelPeople = "KiÅŸiler" + +[certSign.sessions] +deleted = "Oturum silindi" +fetchFailed = "Oturum ayrıntıları yüklenemedi" +finalized = "Oturum sonlandırıldı" +loaded = "İmzalı PDF yüklendi" +pdfNotReady = "PDF Hazır DeÄŸil" +pdfNotReadyDesc = "İmzalı PDF oluÅŸturuluyor. Lütfen kısa süre sonra tekrar deneyin." + +[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" + [changeCreds] changePassword = "Varsayılan giriÅŸ bilgilerini kullanıyorsunuz. Lütfen yeni bir ÅŸifre girin." changeUsername = "Kullanıcı adınızı güncelleyin. Güncellemeden sonra oturumunuz kapatılacak." @@ -3242,6 +3531,46 @@ totalSelected = "Toplam Seçilen" unsupported = "Desteklenmiyor" unzip = "Zip'ten Çıkar" uploadError = "Bazı dosyalar yüklenemedi." +copyCreated = "Kopya bu cihaza kaydedildi." +copyFailed = "Kopya oluÅŸturulamadı." +leaveShare = "Listemden kaldır" +leaveShareFailed = "Paylaşılan dosya kaldırılamadı." +leaveShareSuccess = "Paylaşılan listenizden kaldırıldı." +removeBoth = "Her ikisinden de kaldır" +removeFilePrompt = "Bu dosya hem bu cihazda hem de sunucunuzda kayıtlı. Nereden kaldırmak istersiniz?" +removeFileTitle = "Dosyayı kaldır" +removeLocalOnly = "Yalnızca bu cihaz" +removeServerFailed = "Dosya sunucudan kaldırılamadı." +removeServerOnly = "Yalnızca sunucu" +removeServerOnlyPrompt = "Bu dosya yalnızca sunucunuzda saklanıyor. Sunucudan kaldırmak ister misiniz?" +removeServerSuccess = "Sunucudan kaldırıldı." +removeSharedPrompt = "Bu dosya sizinle paylaşıldı. Bunu bu cihazdan veya paylaşılan listenizden kaldırabilirsiniz." +removeSharedServerOnlyBlockedPrompt = "Bu dosya sizinle paylaşıldı ve yalnızca sunucuda saklanıyor." +removeSharedServerOnlyPrompt = "Bu dosya sizinle paylaşıldı ve yalnızca sunucuda saklanıyor. Listeden kaldırmak ister misiniz?" +changesNotUploaded = "DeÄŸiÅŸiklikler yüklenmedi" +cloudFile = "Bulut dosyası" +filterAll = "Tümü" +filterLocal = "Yerel" +filterSharedByMe = "Benim paylaÅŸtıklarım" +filterSharedWithMe = "Benimle paylaşılanlar" +lastSynced = "Son eÅŸitleme" +localOnly = "Yalnızca yerel" +makeCopy = "Kopya oluÅŸtur" +owner = "Sahip" +ownerUnknown = "Bilinmiyor" +share = "PaylaÅŸ" +shareSelected = "Seçileni PaylaÅŸ" +sharedByYou = "Sizin paylaÅŸtıklarınız" +sharedEditNoticeBody = "Bu dosyanın sunucudaki sürümünde düzenleme hakkınız yok. Yapacağınız düzenlemeler yerel bir kopya olarak kaydedilir." +sharedEditNoticeConfirm = "Anladım" +sharedEditNoticeTitle = "Salt okunur sunucu kopyası" +sharedWithYou = "Sizinle paylaşılanlar" +sharing = "Paylaşım" +storageState = "Depolama" +synced = "EÅŸitlendi" +updateOnServer = "Sunucuda Güncelle" +uploadSelected = "Seçileni Yükle" +uploadToServer = "Sunucuya Yükle" [files] addFiles = "Dosya ekle" @@ -3367,6 +3696,77 @@ title = "PDF'leri DüzleÅŸtirme Hakkında" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Grup İmzalama Hakkında" + +[groupSigning.tooltip.finalization] +bullet1 = "Tüm imzalar belirttiÄŸiniz katılımcı sırasıyla uygulanır" +bullet2 = "GerektiÄŸinde kısmi imzalarla sonlandırabilirsiniz" +bullet3 = "Sonlandırıldıktan sonra oturum deÄŸiÅŸtirilemez" +description = "Tüm katılımcılar imzaladığında (veya erken sonlandırmayı seçtiÄŸinizde), nihai imzalı PDF'yi oluÅŸturabilirsiniz." +title = "Sonlandırma Süreci" + +[groupSigning.tooltip.roles] +bullet1 = "Sahip (siz): Oturumu oluÅŸturur, imza varsayılanlarını yapılandırır, belgeyi sonlandırır" +bullet2 = "Katılımcılar: İmzalarını oluÅŸturur, sertifika seçer, PDF'ye yerleÅŸtirir" +bullet3 = "Katılımcılar imza görünürlüğü, nedeni veya konum ayarlarını deÄŸiÅŸtiremez" +description = "Tüm katılımcılar için imza görünümü ayarlarını siz kontrol edersiniz." +title = "Katılımcı Rolleri" + +[groupSigning.tooltip.sequential] +bullet1 = "İlk katılımcı imzalamadan ikinci kiÅŸi belgeye eriÅŸemez" +bullet2 = "Hukuki uyum için doÄŸru imzalama sırasını saÄŸlar" +bullet3 = "Listedeki sıralarını sürükleyerek katılımcıları yeniden sıralayabilirsiniz" +description = "Katılımcılar belirttiÄŸiniz sırada belgeleri imzalar. Her imzalayan, sırası geldiÄŸinde bir bildirim alır." +title = "Sıralı İmzalama" + +[groupSigning.steps] +back = "Geri" +completed = "Tamamlandı" +current = "Mevcut" +stepLabel = "Adım {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Gözden Geçirmeye Devam Et" +invisible = "İmzalar görünmez olacak (yalnızca metaveri)" +locationLabel = "Konum:" +preview = "Önizleme" +reasonLabel = "Neden:" +title = "İmza Ayarlarını Yapılandır" +visible = "İmzalar {{page}}. sayfada görünür olacak" + +[groupSigning.steps.review] +document = "Belge" +dueDate = "Son Tarih (İsteÄŸe baÄŸlı)" +dueDatePlaceholder = "Son tarihi seçin..." +invisible = "Görünmez (yalnızca metaveri)" +location = "Konum:" +logo = "Logo:" +logoHidden = "Logo yok" +logoShown = "Stirling PDF logosu gösteriliyor" +participants = "Katılımcılar" +reason = "Neden:" +send = "İmzalama İsteklerini Gönder" +signatureSettings = "İmza Ayarları" +title = "Oturum Ayrıntılarını Gözden Geçir" +titleShort = "Gözden Geçir ve Gönder" +visibility = "Görünürlük:" +visible = "{{page}}. sayfada görünür" +participantCount = "{{count}} katılımcı sırayla imzalayacak" + +[groupSigning.steps.selectDocument] +continue = "Katılımcı Seçimine Devam Et" +noFile = "Bir imzalama oturumu oluÅŸturmak için lütfen aktif dosyalarınızdan tek bir PDF dosyası seçin." +selectedFile = "Seçilen belge" +title = "Belge Seç" + +[groupSigning.steps.selectParticipants] +continue = "İmza Ayarlarına Devam Et" +count = "{{count}} katılımcı seçildi" +label = "Katılımcıları seçin" +placeholder = "İmzalamaları için katılımcıları seçin..." +title = "Katılımcıları Seç" + [getPdfInfo] downloadJson = "JSON İndir" downloads = "İndirmeler" @@ -4460,7 +4860,10 @@ zoomOut = "UzaklaÅŸtır" [viewer] cannotPreviewFile = "Dosya önizlenemiyor" +disableColorFilter = "Renk Filtresini Devre Dışı Bırak" dualPageView = "Çift Sayfa Görünümü" +enableDarkFilter = "Karanlık Filtresini EtkinleÅŸtir" +enableSepiaFilter = "Sepya Filtresini EtkinleÅŸtir" firstPage = "İlk Sayfa" lastPage = "Son Sayfa" nextPage = "Sonraki Sayfa" @@ -4470,6 +4873,22 @@ singlePageView = "Tek Sayfa Görünümü" unknownFile = "Bilinmeyen dosya" zoomIn = "YakınlaÅŸtır" zoomOut = "UzaklaÅŸtır" +resetZoom = "YakınlaÅŸtırmayı sıfırla" + +[viewer.nonPdf] +fileTypeBadge = "{{type}} Dosyası" +convertToPdf = "PDF'ye Dönüştür" +loading = "Yükleniyor..." +emptyFile = "BoÅŸ dosya" +csvStats = "{{rows}} satır · {{columns}} sütun · {{size}}" +sortedBy = "Sıralama ölçütü: {{column}}" +columnDefault = "Sütun {{index}}" +htmlPreviewWarning = "HTML önizleme — harici kaynaklar yüklenmeyebilir · {{size}}" +htmlPreview = "HTML önizleme" +invalidJson = "Geçersiz JSON — ham içerik gösteriliyor" +textStats = "{{lines}} satır · {{size}}" +lineNumbers = "Satır numaraları" +renderMarkdown = "Markdown'u iÅŸle" [viewer.attachments] title = "Ekler" @@ -4531,6 +4950,7 @@ toggleAttachments = "Ekleri Göster/Gizle" toggleTheme = "Temayı DeÄŸiÅŸtir" language = "Dil" toggleAnnotations = "Açıklamaların Görünürlüğünü DeÄŸiÅŸtir" +toggleLayers = "Katmanları Aç/Kapat" search = "PDF Ara" panMode = "Kaydırma Modu" applyRedactionsFirst = "Önce karartmaları uygula" @@ -5407,20 +5827,72 @@ title = "Dosya Yazdır" 2 = "Yazıcı Adını Girin" [quickAccess] +access = "EriÅŸim" +accessAddPerson = "BaÅŸka bir kiÅŸi ekle" +accessBack = "Geri" +accessCopyLink = "BaÄŸlantıyı kopyala" +accessEmail = "E-posta Adresi" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Dosya" +accessGeneral = "Genel EriÅŸim" +accessInviteTitle = "KiÅŸi Davet Et" +accessOwner = "Sahip" +accessPanel = "Belge eriÅŸimi" +accessPeople = "EriÅŸimi olan kiÅŸiler" +accessRemove = "Kaldır" +accessRestricted = "Kısıtlı" +accessRestrictedHint = "Yalnızca eriÅŸimi olan kiÅŸiler açabilir" +accessRole = "Rol" +accessRoleCommenter = "Yorumcu" +accessRoleEditor = "Düzenleyici" +accessRoleViewer = "Görüntüleyici" +accessSelectedFile = "Seçilen dosya" +accessSendInvite = "Davet Gönder" +accessTitle = "Belge EriÅŸimi" +accessYou = "Siz" account = "Hesap" +activeSessions = "Aktif Oturumlar" +activeTab = "Aktif" activity = "Etkinlik" adminSettings = "Admin Ayarları" +allSessions = "Tüm Oturumlar" allTools = "All Tools" automate = "Otomatik" +back = "Geri" +certSign = "Sertifika İmzası" +completedSessions = "Tamamlanan Oturumlar" +completedTab = "Tamamlandı" config = "Ayarlar" +createNew = "Yeni İstek OluÅŸtur" +createSession = "İmzalama İsteÄŸi OluÅŸtur" +dueDate = "Son tarih (isteÄŸe baÄŸlı)" files = "Dosyalar" help = "Yardım" +noActiveSessions = "Bekleyen imza isteÄŸi veya aktif oturum yok" +noCompletedSessions = "Tamamlanan oturum yok" +noFile = "Dosya seçilmedi" read = "Oku" reader = "Okuyucu" +refresh = "Yenile" +requestSignatures = "İmza İste" +selectSingleFileToRequest = "İmza istemek için tek bir PDF dosyası seçin" +selectedFile = "Seçilen dosya" +selectUsers = "İmzalamaları için kullanıcıları seçin" +selectUsersPlaceholder = "Katılımcıları seçin..." +sendingRequest = "Gönderiliyor..." settings = "Ayarlar" showMeAround = "Bana etrafı göster" sign = "İmzala" +signatureRequests = "İmza İstekleri" +signYourself = "Kendin İmzala" +newRequest = "Yeni İstek" tours = "Turlar" +wetSign = "İmza Ekle" +filterMine = "Benim" +filterOverdue = "GecikmiÅŸ" +filterSigned = "İmzalandı" +filterDeclined = "Reddedildi" +searchDocuments = "Belgelerde ara…" [quickAccess.helpMenu] adminTour = "Yönetici Turu" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Stirling-PDF sunucunuz çevrimdışı ve \"{{endpoint expired = "Oturumunuzun süresi doldu. Lütfen sayfayı yenileyip tekrar deneyin." refreshPage = "Sayfayı Yenile" +[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ı" + +[signatureSettings.tooltip] +header = "İmza Görünümü Ayarları" + +[signatureSettings.tooltip.location] +bullet1 = "Örnekler: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Sayfa konumuyla aynı deÄŸildir" +bullet3 = "Bazı hukuki yargı alanları için gerekli olabilir" +description = "İmzanın uygulandığı isteÄŸe baÄŸlı coÄŸrafi konum. Sertifika metaverisine kaydedilir." +title = "İmza Konumu" + +[signatureSettings.tooltip.logo] +bullet1 = "İmza ve metnin yanında gösterilir" +bullet2 = "PNG, JPG formatlarını destekler" +bullet3 = "Profesyonel görünümü artırır" +description = "Görünür imzalara marka ve doÄŸruluk için ÅŸirket logosu ekleyin." +title = "Åžirket Logosu" + +[signatureSettings.tooltip.reason] +bullet1 = "Örnekler: \"Onay\", \"SözleÅŸme AnlaÅŸması\", \"İnceleme Tamamlandı\"" +bullet2 = "PDF imza özelliklerinde görünür" +bullet3 = "Denetim izleri ve uyumluluk için faydalıdır" +description = "Belgenin neden imzalandığını açıklayan isteÄŸe baÄŸlı metin. Sertifika metaverisine kaydedilir." +title = "İmza Nedeni" + +[signatureSettings.tooltip.visibility] +bullet1 = "Görünür: İmza, özel görünümle PDF üzerinde görünür" +bullet2 = "Görünmez: Görsel iÅŸaret olmadan sertifika gömülür" +bullet3 = "Görünmez imzalar yine de kriptografik doÄŸrulama saÄŸlar" +description = "İmzanın belgede görünür olup olmayacağını veya görünmez ÅŸekilde gömüleceÄŸini kontrol eder." +title = "İmza Görünürlüğü" + [settings.configuration] advanced = "GeliÅŸmiÅŸ" database = "Veritabanı" endpoints = "Uç Noktalar" features = "Özellikler" +storageSharing = "Dosya Depolama ve PaylaÅŸma" systemSettings = "Sistem Ayarları" title = "Yapılandırma" @@ -6332,10 +6868,13 @@ title = "Stirling'de Oturum Açın" [setup.selfhosted] link = "veya kendi barındırdığınız bir hesaba baÄŸlanın" subtitle = "Sunucu kimlik bilgilerinizi girin" +changeServerLocked = "KuruluÅŸunuz bu uygulamayı belirli bir sunucuyla sınırlamıştır" switchToLocal = "Bunun yerine yerel araçları kullan" title = "Sunucuda Oturum Açın" [setup.selfhosted.unreachable] +changeServer = "Farklı bir sunucuya baÄŸlan" +changeServerLocked = "KuruluÅŸunuz bu uygulamayı belirli bir sunucuyla sınırlamıştır" continueOffline = "Bunun yerine yerel araçları kullan" message = "{{url}} adresine ulaşılamadı. Sunucunun çalıştığını ve eriÅŸilebilir olduÄŸunu kontrol edin." retry = "Yeniden dene" @@ -6529,6 +7068,15 @@ saved = "Kaydedildi" text = "Metin" title = "İmza Türü" +[signRequest] +declined = "İmza isteÄŸi reddedildi" +fetchFailed = "İmza isteÄŸi yüklenemedi" +signed = "Belge baÅŸarıyla imzalandı" + +[signSession] +createFailed = "İmzalama isteÄŸi oluÅŸturulamadı" +created = "İmzalama isteÄŸi gönderildi" + [signup] accountCreatedSuccessfully = "Hesap baÅŸarıyla oluÅŸturuldu! Artık giriÅŸ yapabilirsiniz." alreadyHaveAccount = "Zaten bir hesabınız var mı? GiriÅŸ yapın" @@ -6807,6 +7355,106 @@ title = "PDF'yi Bölümlere Ayır" [splitPdfByChapters] tags = "böl, bölümler, yer imleri, düzenle" +[storageShare] +accessed = "EriÅŸildi" +accessDenied = "Bu paylaşılan dosyaya eriÅŸiminiz yok. Sahipten sizinle paylaÅŸmasını isteyin." +accessFailed = "Etkinlik yüklenemedi." +accessDeniedBody = "Bu dosyaya eriÅŸiminiz yok. Sahipten sizinle paylaÅŸmasını isteyin." +accessDeniedTitle = "EriÅŸim yok" +accessLimitedCommenter = "Yorum eriÅŸimi yakında geliyor. İndirmeniz gerekiyorsa sahibinden düzenleyici eriÅŸimi isteyin." +accessLimitedTitle = "Sınırlı eriÅŸim" +accessLimitedViewer = "Bu baÄŸlantı yalnızca görüntüleme içindir. İndirmeniz gerekiyorsa sahibinden düzenleyici eriÅŸimi isteyin." +createdAt = "OluÅŸturuldu" +download = "İndir" +downloadFailed = "Bu dosya indirilemedi." +expiredBody = "Bu paylaşım baÄŸlantısı geçersiz veya süresi dolmuÅŸ." +expiredTitle = "BaÄŸlantının süresi doldu" +goToLogin = "GiriÅŸe git" +loadFailed = "Paylaşılan dosya açılamadı." +loading = "Paylaşım baÄŸlantısı yükleniyor..." +loginPrompt = "Bu paylaşılan dosyaya eriÅŸmek için oturum açın." +loginRequired = "GiriÅŸ gerekli" +openInApp = "Stirling PDF'de Aç" +ownerLabel = "Sahip" +ownerUnknown = "Bilinmiyor" +requiresLogin = "Bu paylaşılan dosya oturum açmayı gerektirir." +roleCommenter = "Yorumcu" +roleEditor = "Düzenleyici" +roleViewer = "Görüntüleyici" +shareHeading = "Paylaşılan dosya" +titleDefault = "Paylaşılan dosya" +tryAgain = "Lütfen daha sonra tekrar deneyin." +addUser = "Ekle" +commenterHint = "Yorum yapma özelliÄŸi yakında geliyor." +copied = "BaÄŸlantı panoya kopyalandı" +copy = "Kopyala" +copyFailed = "Kopyalama baÅŸarısız" +description = "Bu dosya için bir paylaşım baÄŸlantısı oluÅŸturun. BaÄŸlantıya sahip ve oturum açmış kullanıcılar eriÅŸebilir." +downloadsCount = "İndirme: {{count}}" +emailWarningBody = "Bu bir e-posta adresine benziyor. Bu kiÅŸi henüz bir Stirling PDF kullanıcısı deÄŸilse dosyaya eriÅŸemez." +emailWarningConfirm = "Yine de paylaÅŸ" +emailWarningTitle = "E-posta adresi" +errorTitle = "Paylaşım baÅŸarısız" +failure = "Paylaşım baÄŸlantısı oluÅŸturulamadı. Lütfen tekrar deneyin." +fileLabel = "Dosya" +generate = "BaÄŸlantı OluÅŸtur" +generated = "Paylaşım baÄŸlantısı oluÅŸturuldu" +hideActivity = "EtkinliÄŸi gizle" +invalidUsername = "Geçerli bir kullanıcı adı veya e-posta adresi girin." +lastAccessed = "Son eriÅŸim" +linkAccessTitle = "Paylaşım baÄŸlantısı eriÅŸimi" +linkLabel = "Paylaşım baÄŸlantısı" +linksDisabled = "Paylaşım baÄŸlantıları devre dışı bırakıldı." +linksDisabledBody = "Paylaşım baÄŸlantıları sunucu ayarlarınız tarafından devre dışı bırakıldı." +manage = "Paylaşımı yönet" +manageDescription = "Bu dosyayı paylaÅŸmak için baÄŸlantılar oluÅŸturun ve yönetin." +manageLoadFailed = "Paylaşım baÄŸlantıları yüklenemedi." +manageTitle = "Paylaşımı Yönet" +noActivity = "Henüz etkinlik yok." +noLinks = "Henüz etkin paylaşım baÄŸlantısı yok." +noSharedUsers = "Henüz hiçbir kullanıcının eriÅŸimi yok." +removeLink = "BaÄŸlantıyı kaldır" +removeUser = "Kaldır" +revokeFailed = "Paylaşım baÄŸlantısı kaldırılamadı." +revoked = "Paylaşım baÄŸlantısı kaldırıldı" +roleLabel = "Rol" +sharingDisabled = "Paylaşım devre dışı bırakıldı." +sharingDisabledBody = "Paylaşım, sunucu ayarlarınız tarafından devre dışı bırakıldı." +sharedUsersTitle = "Paylaşılan kullanıcılar" +title = "Dosyayı PaylaÅŸ" +unknownUser = "Bilinmeyen kullanıcı" +userAddFailed = "Bu kullanıcıyla paylaşım yapılamadı." +userAdded = "Kullanıcı paylaşım listesine eklendi." +usernameLabel = "Kullanıcı adı veya e-posta" +usernamePlaceholder = "Bir kullanıcı adı veya e-posta girin" +userRemoveFailed = "Bu kullanıcı kaldırılamadı." +userRemoved = "Kullanıcı paylaşım listesinden kaldırıldı." +viewActivity = "EtkinliÄŸi görüntüle" +viewed = "Görüntülendi" +viewsCount = "Görüntüleme: {{count}}" +downloaded = "İndirildi" +bulkDescription = "Oturum açmış kullanıcılarla seçili tüm dosyaları paylaÅŸmak için tek bir baÄŸlantı oluÅŸturun." +bulkTitle = "Seçili dosyaları paylaÅŸ" +copyLink = "Paylaşım baÄŸlantısını kopyala" +fileCount = "{{count}} dosya seçildi" +ownerOnly = "Paylaşımı yalnızca sahibi yönetebilir." +selectSingleFile = "Paylaşımı yönetmek için tek bir dosya seçin." + +[storageUpload] +description = "Bu, geçerli dosyayı kendi eriÅŸiminiz için sunucu depolamasına yükler." +errorTitle = "Yükleme baÅŸarısız" +failure = "Yükleme baÅŸarısız. Lütfen oturum açma ve depolama ayarlarınızı kontrol edin." +fileLabel = "Dosya" +hint = "Genel baÄŸlantılar ve eriÅŸim modları, sunucu ayarlarınız tarafından kontrol edilir." +success = "Sunucuya yüklendi" +title = "Sunucuya Yükle" +updateButton = "Sunucuda Güncelle" +uploadButton = "Sunucuya Yükle" +bulkDescription = "Bu, seçili dosyaları sunucu depolamanıza yükler." +bulkTitle = "Seçili dosyaları yükle" +fileCount = "{{count}} dosya seçildi" +more = " +{{count}} daha" + [storage] approximateSize = "Yaklaşık boyut" fileTooLarge = "Dosya çok büyük. Dosya başına maksimum boyut" @@ -7153,6 +7801,30 @@ title = "PDF Görüntüle/Düzenle" [warning] tooltipTitle = "Uyarı" +[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" + [watermark] completed = "Filigran eklendi" desc = "PDF dosyalarına metin veya resim filigranları ekleyin" @@ -7333,6 +8005,7 @@ activeSession = "Aktif oturum" addMembers = "Üye Ekle" admin = "Yönetici" confirmDelete = "Bu kullanıcıyı silmek istediÄŸinizden emin misiniz? Bu iÅŸlem geri alınamaz." +confirmUnlock = "Bu kullanıcı hesabının kilidini açmak istediÄŸinizden emin misiniz?" deleteUser = "Kullanıcıyı Sil" deleteUserError = "Kullanıcı silme baÅŸarısız" deleteUserSuccess = "Kullanıcı baÅŸarıyla silindi" @@ -7341,6 +8014,8 @@ disable = "Devre Dışı Bırak" disabled = "Devre dışı" editRole = "Rolü Düzenle" enable = "EtkinleÅŸtir" +locked = "kilitli" +lockedBadge = "Kilitli" loading = "KiÅŸiler yükleniyor..." loginRequired = "Önce oturum açma modunu etkinleÅŸtirin" member = "Üye" @@ -7350,6 +8025,9 @@ searchMembers = "Üyeleri ara..." status = "Durum" team = "Takım" title = "KiÅŸiler" +unlockAccount = "Hesabın Kilidini Aç" +unlockUserError = "Kullanıcı hesabının kilidi açılamadı" +unlockUserSuccess = "Kullanıcı hesabının kilidi baÅŸarıyla açıldı" user = "Kullanıcı" [workspace.people.actions] diff --git a/frontend/public/locales/uk-UA/translation.toml b/frontend/public/locales/uk-UA/translation.toml index a5c8832a1f..b07a3617df 100644 --- a/frontend/public/locales/uk-UA/translation.toml +++ b/frontend/public/locales/uk-UA/translation.toml @@ -8,6 +8,7 @@ black = "Чорний" blue = "Синій" bored = "Ðудно чекати?" cancel = "СкаÑувати" +confirm = "Підтвердити" changedCredsMessage = "Облікові дані змінено!" chooseFile = "Вибрати файл" close = "Закрити" @@ -146,6 +147,7 @@ insufficientCredits = "ÐедоÑтатньо кредитів. Потрібно loadingCredits = "Перевірка кредитів..." loadingProStatus = "Перевірка ÑтатуÑу підпиÑки..." noticeTopUpOrPlan = "ÐедоÑтатньо кредитів, поповніть Ð±Ð°Ð»Ð°Ð½Ñ Ð°Ð±Ð¾ перейдіть на тарифний план" +accessInvite = "ЗапроÑити" [account] accountSettings = "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð°ÐºÐ°ÑƒÐ½Ñ‚Ð°" @@ -1427,6 +1429,34 @@ title = "Обробка" description = "МакÑимальний Ñ‡Ð°Ñ Ð¾Ñ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð½Ð° Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð¾Ð±Ñ€Ð¾Ð±ÐºÐ¸ перед повідомленнÑм про помилку." label = "Тайм-аут обробки (Ñекунди)" +[admin.settings.storage] +description = "Керуйте параметрами Ð·Ð±ÐµÑ€Ñ–Ð³Ð°Ð½Ð½Ñ Ð½Ð° Ñервері та Ñпільного доÑтупу." +title = "Ð—Ð±ÐµÑ€Ñ–Ð³Ð°Ð½Ð½Ñ Ñ„Ð°Ð¹Ð»Ñ–Ð² Ñ– Ñпільний доÑтуп" + +[admin.settings.storage.enabled] +description = "Дозволити кориÑтувачам зберігати файли на Ñервері." +label = "Увімкнути Ð·Ð±ÐµÑ€Ñ–Ð³Ð°Ð½Ð½Ñ Ñ„Ð°Ð¹Ð»Ñ–Ð² на Ñервері" + +[admin.settings.storage.sharing.email] +description = "Дозволити Ð½Ð°Ð´Ð°Ð½Ð½Ñ Ð´Ð¾Ñтупу за адреÑами електронної пошти." +label = "Увімкнути Ñпільний доÑтуп за електронною поштою" +mailLink = "Ðалаштувати параметри пошти" +mailNote = "Потрібна ÐºÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ Ð¿Ð¾ÑˆÑ‚Ð¸. " + +[admin.settings.storage.sharing.enabled] +description = "Дозволити кориÑтувачам ділитиÑÑ Ð·Ð±ÐµÑ€ÐµÐ¶ÐµÐ½Ð¸Ð¼Ð¸ файлами." +label = "Увімкнути Ñпільний доÑтуп" + +[admin.settings.storage.sharing.links] +description = "Дозволити Ñпільний доÑтуп через поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð¸Ð·Ð¾Ð²Ð°Ð½Ð¸Ñ… кориÑтувачів." +frontendUrlLink = "Ðалаштувати в ÑиÑтемних налаштуваннÑÑ…" +frontendUrlNote = "Потрібен Frontend URL. " +label = "Увімкнути поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ñпільного доÑтупу" + +[admin.settings.storage.signing.enabled] +description = "Дозволити кориÑтувачам Ñтворювати ÑеанÑи підпиÑÐ°Ð½Ð½Ñ Ð´Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ñ–Ð² із кількома учаÑниками. Потрібно ввімкнути Ð·Ð±ÐµÑ€Ñ–Ð³Ð°Ð½Ð½Ñ Ñ„Ð°Ð¹Ð»Ñ–Ð² на Ñервері." +label = "Увімкнути групове підпиÑÐ°Ð½Ð½Ñ (Alpha)" + [admin.settings.unsavedChanges] cancel = "Продовжити редагуваннÑ" discard = "Відхилити зміни" @@ -2059,7 +2089,19 @@ numbers = "ЧиÑла/діапазони: 5, 10-20" progressions = "ПрогреÑÑ–Ñ—: 3n, 4n+1" [certSign] +allSigned = "УÑÑ– учаÑники підпиÑали. Готово до завершеннÑ." +awaitingSignatures = "ÐžÑ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñів" +signatureProgress = "{{signedCount}}/{{totalCount}} підпиÑів" chooseCertificate = "Вибрати файл Ñертифіката" +declined = "Відхилено" +fetchFailed = "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ дані підпиÑаннÑ" +finalized = "Завершено" +notified = "ОчікуєтьÑÑ" +partialNote = "Ви можете завершити раніше з поточними підпиÑами. ÐепідпиÑані учаÑники будуть виключені." +pending = "ОчікуєтьÑÑ" +readyToFinalize = "Готово до завершеннÑ" +signed = "ПідпиÑано" +viewed = "ПереглÑнуто" chooseJksFile = "Вибрати файл JKS" chooseP12File = "Вибрати файл PKCS12" choosePfxFile = "Вибрати файл PFX" @@ -2082,6 +2124,7 @@ title = "ÐŸÑ–Ð´Ð¿Ð¸Ñ Ñертифікатом" invisible = "Ðевидимий" stepTitle = "ВиглÑд підпиÑу" visible = "Видимий" +visibility = "ВидиміÑть" [certSign.appearance.options] title = "Деталі підпиÑу" @@ -2188,6 +2231,252 @@ bullet4 = "Може викориÑтовувати кориÑтувацькі Ñ text = "Під Ñ‡Ð°Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸ інÑтрумент повідомлÑÑ”, чи дійÑні підпиÑи, хто підпиÑав документ, коли його підпиÑано та чи змінювавÑÑ Ð´Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚ піÑÐ»Ñ Ð¿Ñ–Ð´Ð¿Ð¸ÑаннÑ." title = "Перевірка підпиÑів" +[certSign.collab.finalize] +button = "Завершити та завантажити підпиÑаний PDF" +early = "Завершити з поточними підпиÑами" + +[certSign.collab.sessionDetail] +addButton = "Додати учаÑників" +addParticipants = "Додати учаÑників" +addParticipantsError = "Ðе вдалоÑÑ Ð´Ð¾Ð´Ð°Ñ‚Ð¸ учаÑників" +backToList = "Ðазад до ÑеанÑів" +deleteConfirm = "Ви впевнені? Це неможливо ÑкаÑувати." +deleteError = "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ ÑеанÑ" +deleted = "Ð¡ÐµÐ°Ð½Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð¾" +deleteSession = "Видалити ÑеанÑ" +dueDate = "Кінцевий термін" +finalizeError = "Ðе вдалоÑÑ Ð·Ð°Ð²ÐµÑ€ÑˆÐ¸Ñ‚Ð¸ ÑеанÑ" +loadPdfError = "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ підпиÑаний PDF" +loadSignedPdf = "Завантажити підпиÑаний PDF до активних файлів" +messageLabel = "ПовідомленнÑ" +noAdditionalInfo = "Ðемає додаткової інформації" +owner = "ВлаÑник" +participantRemoved = "УчаÑника видалено" +participants = "УчаÑники" +participantsAdded = "УчаÑників уÑпішно додано" +removeParticipant = "Видалити" +removeParticipantError = "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ учаÑника" +selectUsers = "Виберіть кориÑтувачів..." +sessionInfo = "Ð†Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¾ ÑеанÑ" +workbenchTitle = "ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ ÑеанÑом" + +[certSign.collab.signRequest] +addedToFiles = "Документ додано до активних файлів" +addSignature = "Додайте Ñвій підпиÑ" +addToFiles = "Додати до активних файлів" +advancedSettings = "Розширені налаштуваннÑ" +backToList = "Ðазад до запитів на підпиÑ" +certificateChoice = "Виберіть Ñертифікат Ð´Ð»Ñ Ð¿Ñ–Ð´Ð¿Ð¸ÑаннÑ" +changeSignature = "Змінити підпиÑ" +clearSignature = "ОчиÑтити підпиÑ" +completeAndSign = "Завершити й підпиÑати" +createNewSignature = "Створити новий підпиÑ" +declineButton = "Відхилити" +decline = "Відхилити запит" +deleteSelected = "Видалити вибраний підпиÑ" +drawSignature = "Ðамалюйте Ñвій Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð½Ð¸Ð¶Ñ‡Ðµ" +dueDate = "Кінцевий термін" +fileTooLarge = "Розмір файлу має бути меншим за 5 МБ" +fontFamily = "Шрифт" +fontSize = "Розмір шрифту: {{size}}px" +fontSizePlaceholder = "Розмір" +from = "Від" +invalidCertFile = "Виберіть файл Ñертифіката P12 або PFX" +invalidFileType = "Виберіть файл зображеннÑ" +location = "МіÑÑ†ÐµÐ·Ð½Ð°Ñ…Ð¾Ð´Ð¶ÐµÐ½Ð½Ñ (необов’Ñзково)" +locationPlaceholder = "Звідки ви підпиÑуєте?" +message = "ПовідомленнÑ" +noCertificate = "Виберіть файл Ñертифіката" +noSignatures = "РозміÑтіть щонайменше один Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð½Ð° PDF" +p12File = "Файл Ñертифіката P12/PFX" +password = "Пароль Ñертифіката" +passwordPlaceholder = "Введіть пароль..." +penColor = "Колір пера" +penSize = "Розмір пера: {{size}}px" +placementActive = "Клацніть по PDF, щоб розміÑтити" +placeSignatureButton = "РозміÑтити Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð½Ð° PDF" +reason = "Причина (необов’Ñзково)" +reasonPlaceholder = "Чому ви підпиÑуєте?" +removeImage = "Видалити зображеннÑ" +removeCertFile = "Видалити файл" +savedSignatures = "Збережені підпиÑи" +selectFile = "Виберіть файл зображеннÑ" +selectSignatureTitle = "Виберіть або Ñтворіть підпиÑ" +signButton = "ПідпиÑати документ" +signatureInfo = "Ці Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð·Ð°Ð´Ð°Ñ” влаÑник документа" +signaturePlaced = "ÐŸÑ–Ð´Ð¿Ð¸Ñ Ñ€Ð¾Ð·Ð¼Ñ–Ñ‰ÐµÐ½Ð¾ на Ñторінці" +signatureSettings = "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñу" +signatureText = "ТекÑÑ‚ підпиÑу" +signatureTextPlaceholder = "Введіть Ñвоє ім’Ñ..." +signatureTypeLabel = "Тип підпиÑу" +signingTitle = "ПідпиÑаннÑ" +textColor = "Колір текÑту" +typeSignature = "Введіть Ñвоє ім’Ñ, щоб Ñтворити підпиÑ" +uploadCert = "ВлаÑний Ñертифікат" +uploadCertDesc = "ВикориÑтовуйте влаÑний Ñертифікат P12/PFX" +uploadSignature = "Завантажте Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ñвого підпиÑу" +usePersonalCert = "ПерÑональний Ñертифікат" +usePersonalCertDesc = "Ðвтоматично Ñтворений Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ облікового запиÑу" +useServerCert = "Сертифікат організації" +useServerCertDesc = "Спільний Ñертифікат організації" +workbenchTitle = "Запит на підпиÑ" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Виберіть колір штриха" +continue = "Продовжити" + +[certSign.collab.signRequest.certModal] +description = "Ви розміÑтили {{count}} підпиÑ(и). Виберіть Ñвій Ñертифікат, щоб завершити підпиÑаннÑ." +sign = "ПідпиÑати документ" +certValidating = "Перевірка Ñертифіката..." +certValidUntil = "Сертифікат дійÑний до {{date}}" +certInvalid = "Сертифікат недійÑний: {{error}}" +certInvalidFallback = "ÐедійÑний Ñертифікат" +certNetworkError = "Ðе вдалоÑÑ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€Ð¸Ñ‚Ð¸ Ñертифікат" +title = "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñертифіката" + +[certSign.collab.signRequest.image] +hint = "Завантажте Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ PNG або JPG вашого підпиÑу" + +[certSign.collab.signRequest.mode] +move = "ПереміÑтити підпиÑ" +place = "РозміÑтити підпиÑ" +title = "Режим підпиÑу або переміщеннÑ" + +[certSign.collab.signRequest.modeTabs] +draw = "МалюваннÑ" +image = "Завантажити" +text = "ТекÑÑ‚" + +[certSign.collab.signRequest.placeSignature] +message = "Клацніть по PDF, щоб розміÑтити Ñвій підпиÑ" +title = "Ð Ð¾Ð·Ð¼Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñу" + +[certSign.collab.signRequest.preview] +imageAlt = "Вибраний підпиÑ" +missing = "Попередній переглÑд відÑутній" +textFallback = "ПідпиÑ" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Ðамальований підпиÑ" +defaultImageLabel = "Завантажений підпиÑ" +defaultLabel = "ПідпиÑ" +defaultTextLabel = "Введений підпиÑ" +delete = "Видалити підпиÑ" +none = "Ðемає збережених підпиÑів" + +[certSign.collab.signRequest.signatureType] +draw = "МалюваннÑ" +type = "ТекÑÑ‚" +upload = "ЗавантаженнÑ" + +[certSign.collab.signRequest.steps] +back = "Ðазад" +cancelPlacement = "СкаÑувати розміщеннÑ" +certificate = "Сертифікат" +clickMultipleTimes = "Клацніть по PDF кілька разів, щоб розміÑтити підпиÑи. ПеретÑгніть будь-Ñкий підпиÑ, щоб переміÑтити або змінити його розмір." +clickToPlace = "Клацніть по PDF, де має з’ÑвитиÑÑ Ð²Ð°Ñˆ підпиÑ." +continue = "Продовжити до вибору Ñертифіката" +continueToPlacement = "Продовжити до розміщеннÑ" +continueToReview = "Продовжити до переглÑду" +createSignature = "Створити підпиÑ" +invisible = "Ðевидимий" +location = "МіÑцезнаходженнÑ:" +multipleSignatures = "{{count}} підпиÑів буде заÑтоÑовано до PDF" +oneSignature = "1 Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð±ÑƒÐ´Ðµ заÑтоÑовано до PDF" +placeOnPdf = "РозміÑтити на PDF" +reason = "Причина:" +reviewTitle = "ПереглÑньте перед підпиÑаннÑм" +signaturePlaced = "ÐŸÑ–Ð´Ð¿Ð¸Ñ Ñ€Ð¾Ð·Ð¼Ñ–Ñ‰ÐµÐ½Ð¾ на Ñторінці {{page}}. Ви можете Ñкоригувати позицію, клацнувши ще раз, або перейти до переглÑду." +visible = "Видимий" +visibility = "ВидиміÑть:" +yourSignatures = "Ваші підпиÑи ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Колір" +fontLabel = "Шрифт" +fontSizeLabel = "Розмір" +fontSizePlaceholder = "16" +label = "ТекÑÑ‚ підпиÑу" +modalHint = "Введіть Ñвоє ім’Ñ, потім натиÑніть «Продовжити», щоб розміÑтити його на PDF." +placeholder = "Введіть Ñвоє ім’Ñ..." + +[certSign.collab.participant] +certValidating = "Перевірка Ñертифіката..." +certValid = "✓ Сертифікат дійÑний" +certValidUntil = " до {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "ÐедійÑний Ñертифікат" +certNetworkError = "Ðе вдалоÑÑ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€Ð¸Ñ‚Ð¸ Ñертифікат" + +[certSign.collab.addParticipants] +add = "Додати {{count}} учаÑника" +back = "Ðазад" +configureSignatures = "Ðалаштувати параметри підпиÑу" +continue = "Продовжити до налаштувань підпиÑу" +reasonHelp = "Попередньо вÑтановіть причину підпиÑÐ°Ð½Ð½Ñ Ð´Ð»Ñ Ñ†Ð¸Ñ… учаÑників (необов’Ñзково, вони можуть змінити під Ñ‡Ð°Ñ Ð¿Ñ–Ð´Ð¿Ð¸ÑаннÑ)" +reasonPlaceholder = "напр., ЗатвердженнÑ, ПереглÑд..." +selectUsers = "Виберіть кориÑтувачів" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Додати Ñторінку з підÑумком підпиÑів" +includeSummaryPageHelp = "Ðаприкінці буде додано Ñторінку з уÑіма метаданими підпиÑів. ÐŸÐ¾Ð»Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñів цифрових Ñертифікатів на окремих Ñторінках будуть приховані (влаÑноручні підпиÑи не зачіпаютьÑÑ)." + +[certSign.collab.sessionList] +active = "Ðктивні" +finalized = "Завершені" + +[certSign.collab.signatureSettings] +description = "Ðалаштуйте виглÑд підпиÑів Ð´Ð»Ñ Ð²ÑÑ–Ñ… учаÑників" +title = "ВиглÑд підпиÑу" + +[certSign.collab.userSelector] +inviteUsers = "Додати кориÑтувачів" +loadError = "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ кориÑтувачів" +noTeam = "Без команди" +noUsers = "Інших кориÑтувачів не знайдено." +placeholder = "Виберіть кориÑтувачів..." + +[certSign.mobile] +panelActions = "Дії" +panelDocument = "Документ" +panelPeople = "Люди" + +[certSign.sessions] +deleted = "Ð¡ÐµÐ°Ð½Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð¾" +fetchFailed = "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ деталі ÑеанÑу" +finalized = "Ð¡ÐµÐ°Ð½Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¾" +loaded = "ПідпиÑаний PDF завантажено" +pdfNotReady = "PDF не готовий" +pdfNotReadyDesc = "ГенеруєтьÑÑ Ð¿Ñ–Ð´Ð¿Ð¸Ñаний PDF. Повторіть Ñпробу згодом." + +[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" + [changeCreds] changePassword = "Ви викориÑтовуєте заводÑькі облікові дані Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñƒ. Будь лаÑка, введіть новий пароль" changeUsername = "Оновіть Ñ–Ð¼â€™Ñ ÐºÐ¾Ñ€Ð¸Ñтувача. ПіÑÐ»Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð²Ð°Ñ Ð±ÑƒÐ´Ðµ виведено із ÑиÑтеми." @@ -3242,6 +3531,46 @@ totalSelected = "УÑього вибрано" unsupported = "Ðепідтримуваний" unzip = "Розпакувати" uploadError = "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ деÑкі файли." +copyCreated = "Копію збережено на цьому приÑтрої." +copyFailed = "Ðе вдалоÑÑ Ñтворити копію." +leaveShare = "Видалити з мого ÑпиÑку" +leaveShareFailed = "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ Ñпільний файл." +leaveShareSuccess = "Видалено з вашого ÑпиÑку Ñпільного доÑтупу." +removeBoth = "Видалити в обох міÑцÑÑ…" +removeFilePrompt = "Цей файл збережено на цьому приÑтрої та на вашому Ñервері. Де ви хочете його видалити?" +removeFileTitle = "Видалити файл" +removeLocalOnly = "Лише з цього приÑтрою" +removeServerFailed = "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ файл із Ñервера." +removeServerOnly = "Лише із Ñервера" +removeServerOnlyPrompt = "Цей файл зберігаєтьÑÑ Ð»Ð¸ÑˆÐµ на вашому Ñервері. Видалити його із Ñервера?" +removeServerSuccess = "Видалено із Ñервера." +removeSharedPrompt = "Цей файл надано вам у Ñпільний доÑтуп. Ви можете видалити його з цього приÑтрою або зі Ñвого ÑпиÑку Ñпільного доÑтупу." +removeSharedServerOnlyBlockedPrompt = "Цей файл надано вам у Ñпільний доÑтуп Ñ– зберігаєтьÑÑ Ð»Ð¸ÑˆÐµ на Ñервері." +removeSharedServerOnlyPrompt = "Цей файл надано вам у Ñпільний доÑтуп Ñ– зберігаєтьÑÑ Ð»Ð¸ÑˆÐµ на Ñервері. Видалити його зі Ñвого ÑпиÑку?" +changesNotUploaded = "Зміни не завантажено" +cloudFile = "Хмарний файл" +filterAll = "УÑÑ–" +filterLocal = "Локальні" +filterSharedByMe = "Ðадані мною" +filterSharedWithMe = "Ðадані мені" +lastSynced = "ВоÑтаннє Ñинхронізовано" +localOnly = "Лише локально" +makeCopy = "Створити копію" +owner = "ВлаÑник" +ownerUnknown = "Ðевідомо" +share = "ПоділитиÑÑ" +shareSelected = "ПоділитиÑÑ Ð²Ð¸Ð±Ñ€Ð°Ð½Ð¸Ð¼Ð¸" +sharedByYou = "Ðадано вами" +sharedEditNoticeBody = "У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” прав Ñ€ÐµÐ´Ð°Ð³ÑƒÐ²Ð°Ð½Ð½Ñ Ñерверної верÑÑ–Ñ— цього файлу. Будь-Ñкі зміни буде збережено Ñк локальну копію." +sharedEditNoticeConfirm = "Зрозуміло" +sharedEditNoticeTitle = "ÐšÐ¾Ð¿Ñ–Ñ Ð½Ð° Ñервері лише Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ" +sharedWithYou = "Ðадано вам" +sharing = "Спільний доÑтуп" +storageState = "Сховище" +synced = "Синхронізовано" +updateOnServer = "Оновити на Ñервері" +uploadSelected = "Завантажити вибрані" +uploadToServer = "Завантажити на Ñервер" [files] addFiles = "Додати файли" @@ -3367,6 +3696,77 @@ title = "Про ÑÐ¿Ð»ÑŽÑ‰ÐµÐ½Ð½Ñ PDF" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Про групове підпиÑаннÑ" + +[groupSigning.tooltip.finalization] +bullet1 = "УÑÑ– підпиÑи заÑтоÑовуютьÑÑ Ñƒ вказаному вами порÑдку учаÑників" +bullet2 = "За потреби ви можете завершити з чаÑтиною підпиÑів" +bullet3 = "ПіÑÐ»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ ÑÐµÐ°Ð½Ñ Ð½Ðµ можна змінити" +description = "ПіÑÐ»Ñ Ñ‚Ð¾Ð³Ð¾ Ñк уÑÑ– учаÑники підпиÑали (або ви вирішили завершити раніше), ви можете згенерувати оÑтаточний підпиÑаний PDF." +title = "ÐŸÑ€Ð¾Ñ†ÐµÑ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ" + +[groupSigning.tooltip.roles] +bullet1 = "ВлаÑник (ви): Ñтворює ÑеанÑ, налаштовує параметри підпиÑу за замовчуваннÑм, завершує документ" +bullet2 = "УчаÑники: Ñтворюють Ñвій підпиÑ, обирають Ñертифікат, розміщують на PDF" +bullet3 = "УчаÑники не можуть змінювати Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ð¸Ð´Ð¸Ð¼Ð¾Ñті, причини чи міÑÑ†ÐµÐ·Ð½Ð°Ñ…Ð¾Ð´Ð¶ÐµÐ½Ð½Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñу" +description = "Ви керуєте налаштуваннÑм виглÑду підпиÑу Ð´Ð»Ñ Ð²ÑÑ–Ñ… учаÑників." +title = "Ролі учаÑників" + +[groupSigning.tooltip.sequential] +bullet1 = "Перший учаÑник має підпиÑати, перш ніж другий отримає доÑтуп до документа" +bullet2 = "Забезпечує належний порÑдок підпиÑÐ°Ð½Ð½Ñ Ð´Ð»Ñ ÑŽÑ€Ð¸Ð´Ð¸Ñ‡Ð½Ð¾Ñ— відповідноÑті" +bullet3 = "Ви можете змінити порÑдок учаÑників, перетÑгуючи Ñ—Ñ… у ÑпиÑку" +description = "УчаÑники підпиÑують документи у визначеному вами порÑдку. Кожен підпиÑант отримує ÑповіщеннÑ, коли наÑтає його черга." +title = "ПоÑлідовне підпиÑаннÑ" + +[groupSigning.steps] +back = "Ðазад" +completed = "Завершено" +current = "Поточний" +stepLabel = "Крок {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Продовжити до переглÑду" +invisible = "ПідпиÑи будуть невидимими (лише метадані)" +locationLabel = "МіÑцезнаходженнÑ:" +preview = "ПереглÑд" +reasonLabel = "Причина:" +title = "Ðалаштувати параметри підпиÑу" +visible = "ПідпиÑи будуть видимими на Ñторінці {{page}}" + +[groupSigning.steps.review] +document = "Документ" +dueDate = "Кінцевий термін (необов’Ñзково)" +dueDatePlaceholder = "Виберіть кінцеву дату..." +invisible = "Ðевидимий (лише метадані)" +location = "МіÑцезнаходженнÑ:" +logo = "Логотип:" +logoHidden = "Без логотипа" +logoShown = "Показано логотип Stirling PDF" +participants = "УчаÑники" +reason = "Причина:" +send = "ÐадіÑлати запити на підпиÑаннÑ" +signatureSettings = "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñу" +title = "ПереглÑнути деталі ÑеанÑу" +titleShort = "ПереглÑд Ñ– надÑиланнÑ" +visibility = "ВидиміÑть:" +visible = "Видимий на Ñторінці {{page}}" +participantCount = "{{count}} учаÑник(и) підпиÑуватимуть по порÑдку" + +[groupSigning.steps.selectDocument] +continue = "Продовжити до вибору учаÑників" +noFile = "Виберіть один файл PDF зі Ñвоїх активних файлів, щоб Ñтворити ÑÐµÐ°Ð½Ñ Ð¿Ñ–Ð´Ð¿Ð¸ÑаннÑ." +selectedFile = "Вибраний документ" +title = "Виберіть документ" + +[groupSigning.steps.selectParticipants] +continue = "Продовжити до налаштувань підпиÑу" +count = "Вибрано {{count}} учаÑник(ів)" +label = "Виберіть учаÑників" +placeholder = "Виберіть учаÑників Ð´Ð»Ñ Ð¿Ñ–Ð´Ð¿Ð¸ÑаннÑ..." +title = "Виберіть учаÑників" + [getPdfInfo] downloadJson = "Завантажити JSON" downloads = "ЗавантаженнÑ" @@ -4460,7 +4860,10 @@ zoomOut = "Зменшити" [viewer] cannotPreviewFile = "Ðе вдаєтьÑÑ Ð¿ÐµÑ€ÐµÐ³Ð»Ñнути файл" +disableColorFilter = "Вимкнути кольоровий фільтр" dualPageView = "Парний переглÑд" +enableDarkFilter = "Увімкнути темний фільтр" +enableSepiaFilter = "Увімкнути фільтр Ñепії" firstPage = "Перша Ñторінка" lastPage = "ОÑÑ‚Ð°Ð½Ð½Ñ Ñторінка" nextPage = "ÐаÑтупна Ñторінка" @@ -4470,6 +4873,22 @@ singlePageView = "Одинарний переглÑд" unknownFile = "Ðевідомий файл" zoomIn = "Збільшити" zoomOut = "Зменшити" +resetZoom = "Скинути маÑштаб" + +[viewer.nonPdf] +fileTypeBadge = "Файл {{type}}" +convertToPdf = "Перетворити в PDF" +loading = "ЗавантаженнÑ..." +emptyFile = "Порожній файл" +csvStats = "{{rows}} Ñ€Ñдків · {{columns}} Ñтовпців · {{size}}" +sortedBy = "ВідÑортовано за: {{column}}" +columnDefault = "Стовпець {{index}}" +htmlPreviewWarning = "Попередній переглÑд HTML — зовнішні реÑурÑи можуть не завантажитиÑÑ Â· {{size}}" +htmlPreview = "Попередній переглÑд HTML" +invalidJson = "Ðекоректний JSON — показ вміÑту Ñк Ñ”" +textStats = "{{lines}} Ñ€Ñдків · {{size}}" +lineNumbers = "Ðомери Ñ€Ñдків" +renderMarkdown = "Відображати Markdown" [viewer.attachments] title = "ВкладеннÑ" @@ -4531,6 +4950,7 @@ toggleAttachments = "Показати/приховати вкладеннÑ" toggleTheme = "Перемкнути тему" language = "Мова" toggleAnnotations = "Перемкнути видиміÑть анотацій" +toggleLayers = "Перемкнути шари" search = "Пошук у PDF" panMode = "Режим переміщеннÑ" applyRedactionsFirst = "Спочатку заÑтоÑуйте зачорненнÑ" @@ -5407,20 +5827,72 @@ title = "Роздрукувати файл" 2 = "Обрати назву прінтера" [quickAccess] +access = "ДоÑтуп" +accessAddPerson = "Додати ще одну оÑобу" +accessBack = "Ðазад" +accessCopyLink = "Копіювати поÑиланнÑ" +accessEmail = "Електронна адреÑа" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Файл" +accessGeneral = "Загальний доÑтуп" +accessInviteTitle = "ЗапроÑити людей" +accessOwner = "ВлаÑник" +accessPanel = "ДоÑтуп до документа" +accessPeople = "ОÑоби з доÑтупом" +accessRemove = "Видалити" +accessRestricted = "Обмежений" +accessRestrictedHint = "Лише оÑоби з доÑтупом можуть відкривати" +accessRole = "Роль" +accessRoleCommenter = "Коментатор" +accessRoleEditor = "Редактор" +accessRoleViewer = "ПереглÑдач" +accessSelectedFile = "Вибраний файл" +accessSendInvite = "ÐадіÑлати запрошеннÑ" +accessTitle = "ДоÑтуп до документа" +accessYou = "Ви" account = "Профіль" +activeSessions = "Ðктивні ÑеанÑи" +activeTab = "Ðктивні" activity = "Журнал" adminSettings = "Ðалашт. адміна" +allSessions = "УÑÑ– ÑеанÑи" allTools = "All Tools" automate = "Ðвтомат." +back = "Ðазад" +certSign = "ÐŸÑ–Ð´Ð¿Ð¸Ñ Ñертифікатом" +completedSessions = "Завершені ÑеанÑи" +completedTab = "Завершені" config = "Конфіг" +createNew = "Створити новий запит" +createSession = "Створити запит на підпиÑаннÑ" +dueDate = "Кінцевий термін (необов’Ñзково)" files = "Файли" help = "Довідка" +noActiveSessions = "Ðемає очікуваних запитів на Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð°Ð±Ð¾ активних ÑеанÑів" +noCompletedSessions = "Ðемає завершених ÑеанÑів" +noFile = "Файл не вибрано" read = "Читати" reader = "ПереглÑд" +refresh = "Оновити" +requestSignatures = "ЗапроÑити підпиÑи" +selectSingleFileToRequest = "Виберіть один файл PDF, щоб запроÑити підпиÑи" +selectedFile = "Вибраний файл" +selectUsers = "Виберіть кориÑтувачів Ð´Ð»Ñ Ð¿Ñ–Ð´Ð¿Ð¸ÑаннÑ" +selectUsersPlaceholder = "Виберіть учаÑників..." +sendingRequest = "ÐадÑиланнÑ..." settings = "Ðалашт." showMeAround = "Проведіть екÑкурÑÑ–ÑŽ" sign = "ПідпиÑ" +signatureRequests = "Запити на підпиÑаннÑ" +signYourself = "ПідпиÑати ÑамоÑтійно" +newRequest = "Ðовий запит" tours = "Тури" +wetSign = "Додати підпиÑ" +filterMine = "Мої" +filterOverdue = "ПроÑтрочені" +filterSigned = "ПідпиÑані" +filterDeclined = "Відхилені" +searchDocuments = "Пошук документів…" [quickAccess.helpMenu] adminTour = "ОглÑд адмініÑтратора" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Ваш Ñервер Stirling-PDF офлайн Ñ– \"{{ expired = "Ваш ÑÐµÐ°Ð½Ñ Ð·Ð°ÐºÑ–Ð½Ñ‡Ð¸Ð²ÑÑ. Будь лаÑка, оновіть Ñторінку та повторіть Ñпробу." refreshPage = "Оновити Ñторінку" +[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 = "ПорÑдок підпиÑів" + +[signatureSettings.tooltip] +header = "Параметри виглÑду підпиÑу" + +[signatureSettings.tooltip.location] +bullet1 = "Приклади: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Ðе те Ñаме, що Ñ€Ð¾Ð·Ñ‚Ð°ÑˆÑƒÐ²Ð°Ð½Ð½Ñ Ð½Ð° Ñторінці" +bullet3 = "Може бути обов’Ñзковим у деÑких юриÑдикціÑÑ…" +description = "Додаткове географічне міÑцезнаходженнÑ, де було заÑтоÑовано підпиÑ. ЗберігаєтьÑÑ Ð² метаданих Ñертифіката." +title = "МіÑÑ†ÐµÐ·Ð½Ð°Ñ…Ð¾Ð´Ð¶ÐµÐ½Ð½Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñу" + +[signatureSettings.tooltip.logo] +bullet1 = "ПоказуєтьÑÑ Ð¿Ð¾Ñ€ÑƒÑ‡ із підпиÑом Ñ– текÑтом" +bullet2 = "Підтримує формати PNG, JPG" +bullet3 = "ПідÑилює профеÑійний виглÑд" +description = "Додайте логотип компанії до видимих підпиÑів Ð´Ð»Ñ Ð±Ñ€ÐµÐ½Ð´ÑƒÐ²Ð°Ð½Ð½Ñ Ñ‚Ð° автентичноÑті." +title = "Логотип компанії" + +[signatureSettings.tooltip.reason] +bullet1 = "Приклади: \"Approval\", \"Contract Agreement\", \"Review Complete\"" +bullet2 = "Видимо у влаÑтивоÑÑ‚ÑÑ… підпиÑу PDF" +bullet3 = "КориÑно Ð´Ð»Ñ Ð¶ÑƒÑ€Ð½Ð°Ð»Ñ–Ð² аудиту та відповідноÑті" +description = "Додатковий текÑÑ‚, Ñкий поÑÑнює, чому документ підпиÑуєтьÑÑ. ЗберігаєтьÑÑ Ð² метаданих Ñертифіката." +title = "Причина підпиÑу" + +[signatureSettings.tooltip.visibility] +bullet1 = "Видимий: ÐŸÑ–Ð´Ð¿Ð¸Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶Ð°Ñ”Ñ‚ÑŒÑÑ Ð½Ð° PDF із наÑтроюваним виглÑдом" +bullet2 = "Ðевидимий: Сертифікат вбудовано без візуальної позначки" +bullet3 = "Ðевидимі підпиÑи вÑе одно забезпечують криптографічну перевірку" +description = "Керує тим, чи Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð²Ð¸Ð´Ð¸Ð¼Ð¸Ð¹ у документі або вбудований невидимо." +title = "ВидиміÑть підпиÑу" + [settings.configuration] advanced = "Додатково" database = "База даних" endpoints = "Кінцеві точки" features = "Функції" +storageSharing = "Ð—Ð±ÐµÑ€Ñ–Ð³Ð°Ð½Ð½Ñ Ñ„Ð°Ð¹Ð»Ñ–Ð² Ñ– Ñпільний доÑтуп" systemSettings = "СиÑтемні налаштуваннÑ" title = "КонфігураціÑ" @@ -6332,10 +6868,13 @@ title = "Увійдіть до Stirling" [setup.selfhosted] link = "або підключітьÑÑ Ð´Ð¾ ÑамохоÑтингового облікового запиÑу" subtitle = "Введіть облікові дані Ñервера" +changeServerLocked = "Ваша Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ Ð¾Ð±Ð¼ÐµÐ¶Ð¸Ð»Ð° цей заÑтоÑунок певним Ñервером" switchToLocal = "ÐатоміÑть викориÑтовувати локальні інÑтрументи" title = "Увійдіть на Ñервер" [setup.selfhosted.unreachable] +changeServer = "ПідключитиÑÑ Ð´Ð¾ іншого Ñервера" +changeServerLocked = "Ваша Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ Ð¾Ð±Ð¼ÐµÐ¶Ð¸Ð»Ð° цей заÑтоÑунок певним Ñервером" continueOffline = "ÐатоміÑть викориÑтовувати локальні інÑтрументи" message = "Ðе вдалоÑÑ Ð¿Ñ–Ð´ÐºÐ»ÑŽÑ‡Ð¸Ñ‚Ð¸ÑÑ Ð´Ð¾ {{url}}. ПереконайтеÑÑ, що Ñервер запущено й він доÑтупний." retry = "Повторити Ñпробу" @@ -6529,6 +7068,15 @@ saved = "Збережені" text = "ТекÑÑ‚" title = "Тип підпиÑу" +[signRequest] +declined = "Запит на Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð²Ñ–Ð´Ñ…Ð¸Ð»ÐµÐ½Ð¾" +fetchFailed = "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ запит на підпиÑ" +signed = "Документ уÑпішно підпиÑано" + +[signSession] +createFailed = "Ðе вдалоÑÑ Ñтворити запит на підпиÑаннÑ" +created = "Запит на підпиÑÐ°Ð½Ð½Ñ Ð½Ð°Ð´Ñ–Ñлано" + [signup] accountCreatedSuccessfully = "Обліковий Ð·Ð°Ð¿Ð¸Ñ ÑƒÑпішно Ñтворено! Тепер ви можете увійти." alreadyHaveAccount = "Вже маєте обліковий запиÑ? Увійдіть" @@ -6807,6 +7355,106 @@ title = "Розділити PDF по главам" [splitPdfByChapters] tags = "поділ,глави,закладки,організаціÑ" +[storageShare] +accessed = "Отримано доÑтуп" +accessDenied = "У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” доÑтупу до цього Ñпільного файлу. ПопроÑіть влаÑника надати вам доÑтуп." +accessFailed = "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ активніÑть." +accessDeniedBody = "У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” доÑтупу до цього файлу. ПопроÑіть влаÑника надати вам доÑтуп." +accessDeniedTitle = "Ðемає доÑтупу" +accessLimitedCommenter = "ДоÑтуп Ð´Ð»Ñ ÐºÐ¾Ð¼ÐµÐ½Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð·â€™ÑвитьÑÑ Ð½Ð°Ð¹Ð±Ð»Ð¸Ð¶Ñ‡Ð¸Ð¼ чаÑом. ПопроÑіть у влаÑника права редактора, Ñкщо потрібно завантажити." +accessLimitedTitle = "Обмежений доÑтуп" +accessLimitedViewer = "Це поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð»Ð¸ÑˆÐµ Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ³Ð»Ñду. ПопроÑіть у влаÑника права редактора, Ñкщо потрібно завантажити." +createdAt = "Створено" +download = "Завантажити" +downloadFailed = "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ цей файл." +expiredBody = "Це поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ñпільного доÑтупу недійÑне або Ñтрок його дії минув." +expiredTitle = "Термін дії поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð¼Ð¸Ð½ÑƒÐ²" +goToLogin = "Перейти до входу" +loadFailed = "Ðе вдалоÑÑ Ð²Ñ–Ð´ÐºÑ€Ð¸Ñ‚Ð¸ Ñпільний файл." +loading = "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð¿Ð¾ÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° Ñпільний доÑтуп..." +loginPrompt = "Увійдіть, щоб отримати доÑтуп до цього Ñпільного файлу." +loginRequired = "Потрібен вхід" +openInApp = "Відкрити в Stirling PDF" +ownerLabel = "ВлаÑник" +ownerUnknown = "Ðевідомо" +requiresLogin = "Ð”Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Ñпільного файлу потрібен вхід." +roleCommenter = "Коментатор" +roleEditor = "Редактор" +roleViewer = "ПереглÑдач" +shareHeading = "Спільний файл" +titleDefault = "Спільний файл" +tryAgain = "Повторіть Ñпробу пізніше." +addUser = "Додати" +commenterHint = "Функцію ÐºÐ¾Ð¼ÐµÐ½Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð±ÑƒÐ´Ðµ додано найближчим чаÑом." +copied = "ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ñкопійовано в буфер обміну" +copy = "Копіювати" +copyFailed = "Ðе вдалоÑÑ Ñкопіювати" +description = "Створіть поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ñпільного доÑтупу до цього файлу. Ðвторизовані кориÑтувачі з поÑиланнÑм зможуть отримати доÑтуп." +downloadsCount = "Завантажень: {{count}}" +emailWarningBody = "Це Ñхоже на адреÑу електронної пошти. Якщо Ñ†Ñ Ð¾Ñоба ще не Ñ” кориÑтувачем Stirling PDF, вона не зможе отримати доÑтуп до файлу." +emailWarningConfirm = "Ðадати доÑтуп уÑе одно" +emailWarningTitle = "Електронна адреÑа" +errorTitle = "Ðе вдалоÑÑ Ð½Ð°Ð´Ð°Ñ‚Ð¸ доÑтуп" +failure = "Ðе вдалоÑÑ Ð·Ð³ÐµÐ½ÐµÑ€ÑƒÐ²Ð°Ñ‚Ð¸ поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ñпільного доÑтупу. Спробуйте ще раз." +fileLabel = "Файл" +generate = "Згенерувати поÑиланнÑ" +generated = "ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ñпільного доÑтупу згенеровано" +hideActivity = "Приховати активніÑть" +invalidUsername = "Введіть дійÑне Ñ–Ð¼â€™Ñ ÐºÐ¾Ñ€Ð¸Ñтувача або адреÑу електронної пошти." +lastAccessed = "ОÑтанній доÑтуп" +linkAccessTitle = "ДоÑтуп за поÑиланнÑм" +linkLabel = "ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ñпільного доÑтупу" +linksDisabled = "ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ñпільного доÑтупу вимкнено." +linksDisabledBody = "ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ñпільного доÑтупу вимкнено вашими налаштуваннÑми Ñервера." +manage = "Керувати Ñпільним доÑтупом" +manageDescription = "Створюйте та керуйте поÑиланнÑми Ð´Ð»Ñ Ñпільного доÑтупу до цього файлу." +manageLoadFailed = "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ñпільного доÑтупу." +manageTitle = "ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ñпільним доÑтупом" +noActivity = "ÐктивноÑті ще немає." +noLinks = "Ще немає активних поÑилань Ð´Ð»Ñ Ñпільного доÑтупу." +noSharedUsers = "Ще немає кориÑтувачів із доÑтупом." +removeLink = "Видалити поÑиланнÑ" +removeUser = "Видалити" +revokeFailed = "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ñпільного доÑтупу." +revoked = "ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ñпільного доÑтупу видалено" +roleLabel = "Роль" +sharingDisabled = "Спільний доÑтуп вимкнено." +sharingDisabledBody = "Спільний доÑтуп вимкнено налаштуваннÑми вашого Ñервера." +sharedUsersTitle = "КориÑтувачі з доÑтупом" +title = "ПоділитиÑÑ Ñ„Ð°Ð¹Ð»Ð¾Ð¼" +unknownUser = "Ðевідомий кориÑтувач" +userAddFailed = "Ðе вдалоÑÑ Ð½Ð°Ð´Ð°Ñ‚Ð¸ доÑтуп цьому кориÑтувачеві." +userAdded = "КориÑтувача додано до ÑпиÑку Ñпільного доÑтупу." +usernameLabel = "Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача або електронна пошта" +usernamePlaceholder = "Введіть ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача або електронну пошту" +userRemoveFailed = "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ цього кориÑтувача." +userRemoved = "КориÑтувача видалено зі ÑпиÑку Ñпільного доÑтупу." +viewActivity = "ПереглÑнути активніÑть" +viewed = "ПереглÑнуто" +viewsCount = "ПереглÑди: {{count}}" +downloaded = "Завантажено" +bulkDescription = "Створіть одне поÑиланнÑ, щоб надати Ñпільний доÑтуп до вÑÑ–Ñ… вибраних файлів Ð´Ð»Ñ ÐºÐ¾Ñ€Ð¸Ñтувачів, Ñкі ввійшли в ÑиÑтему." +bulkTitle = "ПоділитиÑÑ Ð²Ð¸Ð±Ñ€Ð°Ð½Ð¸Ð¼Ð¸ файлами" +copyLink = "Копіювати поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ñпільного доÑтупу" +fileCount = "Вибрано файлів: {{count}}" +ownerOnly = "Лише влаÑник може керувати Ñпільним доÑтупом." +selectSingleFile = "Виберіть один файл, щоб керувати Ñпільним доÑтупом." + +[storageUpload] +description = "Це завантажить поточний файл у Ñерверне Ñховище Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ доÑтупу." +errorTitle = "Помилка завантаженнÑ" +failure = "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸. Перевірте Ñвої облікові дані входу та Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñховища." +fileLabel = "Файл" +hint = "Публічні поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ñ‚Ð° режими доÑтупу керуютьÑÑ Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñми вашого Ñервера." +success = "Завантажено на Ñервер" +title = "Завантажити на Ñервер" +updateButton = "Оновити на Ñервері" +uploadButton = "Завантажити на Ñервер" +bulkDescription = "Це завантажить вибрані файли у Ñерверне Ñховище." +bulkTitle = "Завантажити вибрані файли" +fileCount = "Вибрано файлів: {{count}}" +more = " +{{count}} ще" + [storage] approximateSize = "Приблизний розмір" fileTooLarge = "Файл завеликий. МакÑимальний розмір файлу Ñтановить" @@ -7153,6 +7801,30 @@ title = "ПереглÑд/Ñ€ÐµÐ´Ð°Ð³ÑƒÐ²Ð°Ð½Ð½Ñ PDF" [warning] tooltipTitle = "ПопередженнÑ" +[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 = "Завантажити Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñу" + [watermark] completed = "ВодÑний знак додано" desc = "Додайте текÑтові або графічні водÑні знаки до файлів PDF" @@ -7333,6 +8005,7 @@ activeSession = "Ðктивна ÑеÑÑ–Ñ" addMembers = "Додати учаÑників" admin = "ÐдмініÑтратор" confirmDelete = "Ви впевнені, що хочете видалити цього кориÑтувача? Цю дію не можна ÑкаÑувати." +confirmUnlock = "Ви впевнені, що хочете розблокувати цей обліковий Ð·Ð°Ð¿Ð¸Ñ ÐºÐ¾Ñ€Ð¸Ñтувача?" deleteUser = "Видалити кориÑтувача" deleteUserError = "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ кориÑтувача" deleteUserSuccess = "КориÑтувача уÑпішно видалено" @@ -7341,6 +8014,8 @@ disable = "Вимкнути" disabled = "Вимкнено" editRole = "Редагувати роль" enable = "Увімкнути" +locked = "заблоковано" +lockedBadge = "Заблоковано" loading = "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ ÑƒÑ‡Ð°Ñників..." loginRequired = "Спочатку увімкніть режим входу" member = "УчаÑник" @@ -7350,6 +8025,9 @@ searchMembers = "Пошук учаÑників..." status = "СтатуÑ" team = "Команда" title = "УчаÑники" +unlockAccount = "Розблокувати обліковий запиÑ" +unlockUserError = "Ðе вдалоÑÑ Ñ€Ð¾Ð·Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ñ‚Ð¸ обліковий Ð·Ð°Ð¿Ð¸Ñ ÐºÐ¾Ñ€Ð¸Ñтувача" +unlockUserSuccess = "Обліковий Ð·Ð°Ð¿Ð¸Ñ ÐºÐ¾Ñ€Ð¸Ñтувача уÑпішно розблоковано" user = "КориÑтувач" [workspace.people.actions] diff --git a/frontend/public/locales/vi-VN/translation.toml b/frontend/public/locales/vi-VN/translation.toml index d451da4aa0..8c0d891b89 100644 --- a/frontend/public/locales/vi-VN/translation.toml +++ b/frontend/public/locales/vi-VN/translation.toml @@ -8,6 +8,7 @@ black = "Äen" blue = "Xanh dương" bored = "Chán phải chỠđợi?" cancel = "Há»§y" +confirm = "Xác nhận" changedCredsMessage = "Thông tin đăng nhập đã thay đổi!" chooseFile = "Chá»n tệp" close = "Äóng" @@ -146,6 +147,7 @@ insufficientCredits = "Không đủ tín dụng. Cần: {{requiredCredits}}, Hi loadingCredits = "Äang kiểm tra tín dụng..." loadingProStatus = "Äang kiểm tra trạng thái đăng ký..." noticeTopUpOrPlan = "Không đủ tín dụng, vui lòng nạp thêm hoặc nâng cấp lên má»™t gói" +accessInvite = "Má»i" [account] accountSettings = "Cài đặt tài khoản" @@ -1427,6 +1429,34 @@ title = "Xá»­ lý" description = "Thá»i gian tối Ä‘a chá» má»™t tác vụ xá»­ lý trước khi báo lá»—i." label = "Thá»i gian chá» xá»­ lý (giây)" +[admin.settings.storage] +description = "Kiểm soát tùy chá»n lưu trữ và chia sẻ trên máy chá»§." +title = "Lưu trữ & Chia sẻ Tệp" + +[admin.settings.storage.enabled] +description = "Cho phép ngưá»i dùng lưu trữ tệp trên máy chá»§." +label = "Bật Lưu trữ Tệp trên Máy chá»§" + +[admin.settings.storage.sharing.email] +description = "Cho phép chia sẻ qua địa chỉ email." +label = "Bật Chia sẻ qua Email" +mailLink = "Cấu hình Cài đặt Thư" +mailNote = "Yêu cầu cấu hình thư. " + +[admin.settings.storage.sharing.enabled] +description = "Cho phép ngưá»i dùng chia sẻ các tệp đã lưu trữ." +label = "Bật Chia sẻ" + +[admin.settings.storage.sharing.links] +description = "Cho phép chia sẻ qua liên kết yêu cầu đăng nhập." +frontendUrlLink = "Cấu hình trong Cài đặt Hệ thống" +frontendUrlNote = "Yêu cầu Frontend URL. " +label = "Bật Liên kết Chia sẻ" + +[admin.settings.storage.signing.enabled] +description = "Cho phép ngưá»i dùng tạo phiên ký tài liệu nhiá»u ngưá»i tham gia. Cần bật lưu trữ tệp trên máy chá»§." +label = "Bật Ký Nhóm (Alpha)" + [admin.settings.unsavedChanges] cancel = "Tiếp tục chỉnh sá»­a" discard = "Bá» thay đổi" @@ -2059,7 +2089,19 @@ numbers = "Số/khoảng: 5, 10-20" progressions = "Cấp số: 3n, 4n+1" [certSign] +allSigned = "Tất cả ngưá»i tham gia đã ký. Sẵn sàng hoàn tất." +awaitingSignatures = "Äang chá» chữ ký" +signatureProgress = "{{signedCount}}/{{totalCount}} chữ ký" chooseCertificate = "Chá»n tệp chứng chỉ" +declined = "Äã từ chối" +fetchFailed = "Không tải được dữ liệu ký" +finalized = "Äã hoàn tất" +notified = "Äang chá»" +partialNote = "Bạn có thể hoàn tất sá»›m vá»›i các chữ ký hiện có. Những ngưá»i chưa ký sẽ bị loại khá»i tài liệu." +pending = "Äang chá»" +readyToFinalize = "Sẵn sàng hoàn tất" +signed = "Äã ký" +viewed = "Äã xem" chooseJksFile = "Chá»n tệp JKS" chooseP12File = "Chá»n tệp PKCS12" choosePfxFile = "Chá»n tệp PFX" @@ -2082,6 +2124,7 @@ title = "Ký bằng chứng chỉ" invisible = "Ẩn" stepTitle = "Hiển thị chữ ký" visible = "Hiển thị" +visibility = "Mức hiển thị" [certSign.appearance.options] title = "Chi tiết chữ ký" @@ -2188,6 +2231,252 @@ 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ý" +[certSign.collab.finalize] +button = "Hoàn tất và Tải PDF đã ký" +early = "Hoàn tất vá»›i các chữ ký hiện có" + +[certSign.collab.sessionDetail] +addButton = "Thêm ngưá»i tham gia" +addParticipants = "Thêm ngưá»i tham gia" +addParticipantsError = "Không thể thêm ngưá»i tham gia" +backToList = "Quay lại Các phiên" +deleteConfirm = "Bạn có chắc không? Thao tác này không thể hoàn tác." +deleteError = "Không thể xóa phiên" +deleted = "Äã xóa phiên" +deleteSession = "Xóa phiên" +dueDate = "Ngày đến hạn" +finalizeError = "Không thể hoàn tất phiên" +loadPdfError = "Không tải được PDF đã ký" +loadSignedPdf = "Tải PDF đã ký vào Tệp Ä‘ang hoạt động" +messageLabel = "Tin nhắn" +noAdditionalInfo = "Không có thông tin bổ sung" +owner = "Chá»§ sở hữu" +participantRemoved = "Äã xóa ngưá»i tham gia" +participants = "Ngưá»i tham gia" +participantsAdded = "Äã thêm ngưá»i tham gia thành công" +removeParticipant = "Xóa" +removeParticipantError = "Không thể xóa ngưá»i tham gia" +selectUsers = "Chá»n ngưá»i dùng..." +sessionInfo = "Thông tin phiên" +workbenchTitle = "Quản lý phiên" + +[certSign.collab.signRequest] +addedToFiles = "Tài liệu đã được thêm vào tệp Ä‘ang hoạt động" +addSignature = "Thêm chữ ký cá»§a bạn" +addToFiles = "Thêm vào Tệp Ä‘ang hoạt động" +advancedSettings = "Cài đặt nâng cao" +backToList = "Quay lại Yêu cầu ký" +certificateChoice = "Chá»n má»™t chứng chỉ để ký" +changeSignature = "Thay đổi chữ ký" +clearSignature = "Xóa chữ ký" +completeAndSign = "Hoàn tất & Ký" +createNewSignature = "Tạo chữ ký má»›i" +declineButton = "Từ chối" +decline = "Từ chối yêu cầu" +deleteSelected = "Xóa chữ ký đã chá»n" +drawSignature = "Vẽ chữ ký cá»§a bạn bên dưới" +dueDate = "Ngày đến hạn" +fileTooLarge = "Kích thước tệp phải nhá» hÆ¡n 5MB" +fontFamily = "Phông chữ" +fontSize = "Cỡ chữ: {{size}}px" +fontSizePlaceholder = "Cỡ" +from = "Từ" +invalidCertFile = "Vui lòng chá»n tệp chứng chỉ P12 hoặc PFX" +invalidFileType = "Vui lòng chá»n tệp hình ảnh" +location = "Vị trí (Tùy chá»n)" +locationPlaceholder = "Bạn Ä‘ang ký từ đâu?" +message = "Tin nhắn" +noCertificate = "Vui lòng chá»n tệp chứng chỉ" +noSignatures = "Vui lòng đặt ít nhất má»™t chữ ký lên PDF" +p12File = "Tệp chứng chỉ P12/PFX" +password = "Mật khẩu chứng chỉ" +passwordPlaceholder = "Nhập mật khẩu..." +penColor = "Màu bút" +penSize = "Cỡ bút: {{size}}px" +placementActive = "Nhấp vào PDF để đặt" +placeSignatureButton = "Äặt chữ ký lên PDF" +reason = "Lý do (Tùy chá»n)" +reasonPlaceholder = "Tại sao bạn ký?" +removeImage = "Xóa hình ảnh" +removeCertFile = "Xóa tệp" +savedSignatures = "Chữ ký đã lưu" +selectFile = "Chá»n tệp hình ảnh" +selectSignatureTitle = "Chá»n hoặc Tạo chữ ký" +signButton = "Ký tài liệu" +signatureInfo = "Các cài đặt này do chá»§ sở hữu tài liệu cấu hình" +signaturePlaced = "Äã đặt chữ ký trên trang" +signatureSettings = "Cài đặt chữ ký" +signatureText = "Văn bản chữ ký" +signatureTextPlaceholder = "Nhập tên cá»§a bạn..." +signatureTypeLabel = "Loại chữ ký" +signingTitle = "Ký" +textColor = "Màu văn bản" +typeSignature = "Nhập tên cá»§a bạn để tạo chữ ký" +uploadCert = "Chứng chỉ tùy chỉnh" +uploadCertDesc = "Sá»­ dụng chứng chỉ P12/PFX cá»§a bạn" +uploadSignature = "Tải lên hình ảnh chữ ký cá»§a bạn" +usePersonalCert = "Chứng chỉ cá nhân" +usePersonalCertDesc = "Tá»± động tạo cho tài khoản cá»§a bạn" +useServerCert = "Chứng chỉ tổ chức" +useServerCertDesc = "Chứng chỉ tổ chức dùng chung" +workbenchTitle = "Yêu cầu ký" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "Chá»n màu nét vẽ" +continue = "Tiếp tục" + +[certSign.collab.signRequest.certModal] +description = "Bạn đã đặt {{count}} chữ ký. Chá»n chứng chỉ cá»§a bạn để hoàn tất việc ký." +sign = "Ký tài liệu" +certValidating = "Äang xác thá»±c chứng chỉ..." +certValidUntil = "Chứng chỉ hợp lệ đến {{date}}" +certInvalid = "Chứng chỉ không hợp lệ: {{error}}" +certInvalidFallback = "Chứng chỉ không hợp lệ" +certNetworkError = "Không thể xác thá»±c chứng chỉ" +title = "Cấu hình chứng chỉ" + +[certSign.collab.signRequest.image] +hint = "Tải lên hình ảnh PNG hoặc JPG cá»§a chữ ký cá»§a bạn" + +[certSign.collab.signRequest.mode] +move = "Di chuyển chữ ký" +place = "Äặt chữ ký" +title = "Chế độ ký hoặc di chuyển" + +[certSign.collab.signRequest.modeTabs] +draw = "Vẽ" +image = "Tải lên" +text = "Nhập" + +[certSign.collab.signRequest.placeSignature] +message = "Nhấp vào PDF để đặt chữ ký cá»§a bạn" +title = "Äặt chữ ký" + +[certSign.collab.signRequest.preview] +imageAlt = "Chữ ký đã chá»n" +missing = "Không có xem trước" +textFallback = "Chữ ký" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "Chữ ký vẽ tay" +defaultImageLabel = "Chữ ký đã tải lên" +defaultLabel = "Chữ ký" +defaultTextLabel = "Chữ ký nhập liệu" +delete = "Xóa chữ ký" +none = "Không có chữ ký đã lưu" + +[certSign.collab.signRequest.signatureType] +draw = "Vẽ" +type = "Nhập" +upload = "Tải lên" + +[certSign.collab.signRequest.steps] +back = "Quay lại" +cancelPlacement = "Há»§y đặt" +certificate = "Chứng chỉ" +clickMultipleTimes = "Nhấp vào PDF nhiá»u lần để đặt chữ ký. Kéo bất kỳ chữ ký nào để di chuyển hoặc thay đổi kích thước." +clickToPlace = "Nhấp vào PDF tại vị trí bạn muốn chữ ký xuất hiện." +continue = "Tiếp tục đến chá»n chứng chỉ" +continueToPlacement = "Tiếp tục đến bước đặt" +continueToReview = "Tiếp tục đến xem lại" +createSignature = "Tạo chữ ký" +invisible = "Ẩn" +location = "Vị trí:" +multipleSignatures = "{{count}} chữ ký sẽ được áp dụng vào PDF" +oneSignature = "1 chữ ký sẽ được áp dụng vào PDF" +placeOnPdf = "Äặt lên PDF" +reason = "Lý do:" +reviewTitle = "Xem lại trước khi ký" +signaturePlaced = "Äã đặt chữ ký trên trang {{page}}. Bạn có thể Ä‘iá»u chỉnh vị trí bằng cách nhấp lại hoặc tiếp tục xem lại." +visible = "Hiển thị" +visibility = "Mức hiển thị:" +yourSignatures = "Chữ ký cá»§a bạn ({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "Màu" +fontLabel = "Phông chữ" +fontSizeLabel = "Cỡ" +fontSizePlaceholder = "16" +label = "Văn bản chữ ký" +modalHint = "Nhập tên cá»§a bạn, sau đó nhấp Tiếp tục để đặt nó lên PDF." +placeholder = "Nhập tên cá»§a bạn..." + +[certSign.collab.participant] +certValidating = "Äang xác thá»±c chứng chỉ..." +certValid = "✓ Chứng chỉ hợp lệ" +certValidUntil = " đến {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "Chứng chỉ không hợp lệ" +certNetworkError = "Không thể xác thá»±c chứng chỉ" + +[certSign.collab.addParticipants] +add = "Thêm {{count}} ngưá»i tham gia" +back = "Quay lại" +configureSignatures = "Cấu hình cài đặt chữ ký" +continue = "Tiếp tục đến cài đặt chữ ký" +reasonHelp = "Äặt sẵn lý do ký cho những ngưá»i tham gia này (tùy chá»n, há» có thể thay đổi khi ký)" +reasonPlaceholder = "ví dụ: Phê duyệt, Xem xét..." +selectUsers = "Chá»n ngưá»i dùng" + +[certSign.collab.sessionCreation] +includeSummaryPage = "Bao gồm Trang tóm tắt chữ ký" +includeSummaryPageHelp = "Má»™t trang tóm tắt sẽ được thêm vào cuối vá»›i tất cả siêu dữ liệu chữ ký. Các há»™p chữ ký chứng chỉ số trên từng trang sẽ bị ẩn (chữ ký tay không bị ảnh hưởng)." + +[certSign.collab.sessionList] +active = "Äang hoạt động" +finalized = "Äã hoàn tất" + +[certSign.collab.signatureSettings] +description = "Cấu hình cách chữ ký hiển thị cho tất cả ngưá»i tham gia" +title = "Giao diện chữ ký" + +[certSign.collab.userSelector] +inviteUsers = "Thêm ngưá»i dùng" +loadError = "Không tải được danh sách ngưá»i dùng" +noTeam = "Không có Nhóm" +noUsers = "Không tìm thấy ngưá»i dùng nào khác." +placeholder = "Chá»n ngưá»i dùng..." + +[certSign.mobile] +panelActions = "Hành động" +panelDocument = "Tài liệu" +panelPeople = "Má»i ngưá»i" + +[certSign.sessions] +deleted = "Äã xóa phiên" +fetchFailed = "Không tải được chi tiết phiên" +finalized = "Phiên đã hoàn tất" +loaded = "Äã tải PDF đã ký" +pdfNotReady = "PDF chưa sẵn sàng" +pdfNotReadyDesc = "PDF đã ký Ä‘ang được tạo. Vui lòng thá»­ lại sau." + +[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" + [changeCreds] changePassword = "Bạn Ä‘ang sá»­ dụng thông tin đăng nhập mặc định. Vui lòng nhập mật khẩu má»›i" changeUsername = "Cập nhật tên ngưá»i dùng. Bạn sẽ bị đăng xuất sau khi cập nhật." @@ -3242,6 +3531,46 @@ totalSelected = "Tổng đã chá»n" unsupported = "Không được há»— trợ" unzip = "Giải nén" uploadError = "Không thể tải lên má»™t số tệp." +copyCreated = "Äã lưu bản sao vào thiết bị này." +copyFailed = "Không thể tạo bản sao." +leaveShare = "Xóa khá»i danh sách cá»§a tôi" +leaveShareFailed = "Không thể xóa tệp được chia sẻ." +leaveShareSuccess = "Äã xóa khá»i danh sách chia sẻ cá»§a bạn." +removeBoth = "Xóa ở cả hai nÆ¡i" +removeFilePrompt = "Tệp này được lưu trên thiết bị này và trên máy chá»§ cá»§a bạn. Bạn muốn xóa khá»i đâu?" +removeFileTitle = "Xóa tệp" +removeLocalOnly = "Chỉ thiết bị này" +removeServerFailed = "Không thể xóa tệp khá»i máy chá»§." +removeServerOnly = "Chỉ máy chá»§" +removeServerOnlyPrompt = "Tệp này chỉ được lưu trên máy chá»§ cá»§a bạn. Bạn có muốn xóa khá»i máy chá»§ không?" +removeServerSuccess = "Äã xóa khá»i máy chá»§." +removeSharedPrompt = "Tệp này được chia sẻ vá»›i bạn. Bạn có thể xóa khá»i thiết bị này hoặc khá»i danh sách chia sẻ cá»§a bạn." +removeSharedServerOnlyBlockedPrompt = "Tệp này được chia sẻ vá»›i bạn và chỉ được lưu trên máy chá»§." +removeSharedServerOnlyPrompt = "Tệp này được chia sẻ vá»›i bạn và chỉ được lưu trên máy chá»§. Xóa khá»i danh sách cá»§a bạn?" +changesNotUploaded = "Thay đổi chưa được tải lên" +cloudFile = "Tệp đám mây" +filterAll = "Tất cả" +filterLocal = "Cục bá»™" +filterSharedByMe = "Tôi đã chia sẻ" +filterSharedWithMe = "ÄÆ°á»£c chia sẻ vá»›i tôi" +lastSynced = "Äồng bá»™ lần cuối" +localOnly = "Chỉ cục bá»™" +makeCopy = "Tạo bản sao" +owner = "Chá»§ sở hữu" +ownerUnknown = "Không xác định" +share = "Chia sẻ" +shareSelected = "Chia sẻ mục đã chá»n" +sharedByYou = "Bạn đã chia sẻ" +sharedEditNoticeBody = "Bạn không có quyá»n chỉnh sá»­a bản trên máy chá»§ cá»§a tệp này. Má»i chỉnh sá»­a bạn thá»±c hiện sẽ được lưu thành bản sao cục bá»™." +sharedEditNoticeConfirm = "Äã hiểu" +sharedEditNoticeTitle = "Bản trên máy chá»§ chỉ Ä‘á»c" +sharedWithYou = "ÄÆ°á»£c chia sẻ vá»›i bạn" +sharing = "Chia sẻ" +storageState = "Lưu trữ" +synced = "Äã đồng bá»™" +updateOnServer = "Cập nhật trên Máy chá»§" +uploadSelected = "Tải lên mục đã chá»n" +uploadToServer = "Tải lên Máy chá»§" [files] addFiles = "Thêm tệp" @@ -3367,6 +3696,77 @@ title = "Vá» việc làm phẳng PDF" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "Giá»›i thiệu vá» Ký nhóm" + +[groupSigning.tooltip.finalization] +bullet1 = "Tất cả chữ ký được áp dụng theo thứ tá»± ngưá»i tham gia bạn đã chỉ định" +bullet2 = "Bạn có thể hoàn tất vá»›i má»™t phần chữ ký nếu cần" +bullet3 = "Sau khi hoàn tất, phiên không thể sá»­a đổi" +description = "Khi tất cả ngưá»i tham gia đã ký (hoặc bạn chá»n hoàn tất sá»›m), bạn có thể tạo PDF đã ký cuối cùng." +title = "Quy trình hoàn tất" + +[groupSigning.tooltip.roles] +bullet1 = "Chá»§ sở hữu (bạn): Tạo phiên, cấu hình mặc định chữ ký, hoàn tất tài liệu" +bullet2 = "Ngưá»i tham gia: Tạo chữ ký, chá»n chứng chỉ, đặt lên PDF" +bullet3 = "Ngưá»i tham gia không thể sá»­a cài đặt mức hiển thị, lý do hoặc vị trí chữ ký" +description = "Bạn kiểm soát cài đặt giao diện chữ ký cho tất cả ngưá»i tham gia." +title = "Vai trò ngưá»i tham gia" + +[groupSigning.tooltip.sequential] +bullet1 = "Ngưá»i tham gia đầu tiên phải ký trước khi ngưá»i thứ hai có thể truy cập tài liệu" +bullet2 = "Äảm bảo thứ tá»± ký phù hợp cho tuân thá»§ pháp lý" +bullet3 = "Bạn có thể sắp xếp lại ngưá»i tham gia bằng cách kéo trong danh sách" +description = "Ngưá»i tham gia ký tài liệu theo thứ tá»± bạn chỉ định. Má»—i ngưá»i ký nhận thông báo khi đến lượt há»." +title = "Ký tuần tá»±" + +[groupSigning.steps] +back = "Quay lại" +completed = "Hoàn tất" +current = "Hiện tại" +stepLabel = "Bước {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "Tiếp tục đến xem lại" +invisible = "Chữ ký sẽ ẩn (chỉ siêu dữ liệu)" +locationLabel = "Vị trí:" +preview = "Xem trước" +reasonLabel = "Lý do:" +title = "Cấu hình cài đặt chữ ký" +visible = "Chữ ký sẽ hiển thị trên trang {{page}}" + +[groupSigning.steps.review] +document = "Tài liệu" +dueDate = "Ngày đến hạn (Tùy chá»n)" +dueDatePlaceholder = "Chá»n ngày đến hạn..." +invisible = "Ẩn (chỉ siêu dữ liệu)" +location = "Vị trí:" +logo = "Logo:" +logoHidden = "Không có logo" +logoShown = "Hiển thị logo Stirling PDF" +participants = "Ngưá»i tham gia" +reason = "Lý do:" +send = "Gá»­i yêu cầu ký" +signatureSettings = "Cài đặt chữ ký" +title = "Xem lại chi tiết phiên" +titleShort = "Xem lại & Gá»­i" +visibility = "Mức hiển thị:" +visible = "Hiển thị trên trang {{page}}" +participantCount = "{{count}} ngưá»i tham gia sẽ ký theo thứ tá»±" + +[groupSigning.steps.selectDocument] +continue = "Tiếp tục đến chá»n ngưá»i tham gia" +noFile = "Vui lòng chá»n má»™t tệp PDF duy nhất từ các tệp Ä‘ang hoạt động cá»§a bạn để tạo phiên ký." +selectedFile = "Tài liệu đã chá»n" +title = "Chá»n tài liệu" + +[groupSigning.steps.selectParticipants] +continue = "Tiếp tục đến cài đặt chữ ký" +count = "Äã chá»n {{count}} ngưá»i tham gia" +label = "Chá»n ngưá»i tham gia" +placeholder = "Chá»n ngưá»i tham gia để ký..." +title = "Chá»n ngưá»i tham gia" + [getPdfInfo] downloadJson = "Tải xuống JSON" downloads = "Tải xuống" @@ -4460,7 +4860,10 @@ zoomOut = "Thu nhá»" [viewer] cannotPreviewFile = "Không thể xem trước tệp" +disableColorFilter = "Tắt bá»™ lá»c màu" dualPageView = "Chế độ xem hai trang" +enableDarkFilter = "Bật bá»™ lá»c tối" +enableSepiaFilter = "Bật bá»™ lá»c sepia" firstPage = "Trang đầu" lastPage = "Trang cuối" nextPage = "Trang tiếp" @@ -4470,6 +4873,22 @@ singlePageView = "Chế độ xem trang đơn" unknownFile = "Tệp không xác định" zoomIn = "Phóng to" zoomOut = "Thu nhá»" +resetZoom = "Äặt lại thu phóng" + +[viewer.nonPdf] +fileTypeBadge = "Tệp {{type}}" +convertToPdf = "Chuyển thành PDF" +loading = "Äang tải..." +emptyFile = "Tệp trống" +csvStats = "{{rows}} hàng · {{columns}} cá»™t · {{size}}" +sortedBy = "Sắp xếp theo: {{column}}" +columnDefault = "Cá»™t {{index}}" +htmlPreviewWarning = "Xem trước HTML — tài nguyên bên ngoài có thể không tải · {{size}}" +htmlPreview = "Xem trước HTML" +invalidJson = "JSON không hợp lệ — Ä‘ang hiển thị ná»™i dung thô" +textStats = "{{lines}} dòng · {{size}}" +lineNumbers = "Äánh số dòng" +renderMarkdown = "Kết xuất markdown" [viewer.attachments] title = "Tệp đính kèm" @@ -4531,6 +4950,7 @@ toggleAttachments = "Bật/Tắt tệp đính kèm" toggleTheme = "Chuyển đổi chá»§ Ä‘á»" language = "Ngôn ngữ" toggleAnnotations = "Chuyển đổi hiển thị chú thích" +toggleLayers = "Chuyển đổi Lá»›p" search = "Tìm kiếm PDF" panMode = "Chế độ kéo" applyRedactionsFirst = "Ãp dụng bôi Ä‘en trước" @@ -5407,20 +5827,72 @@ title = "In tệp" 2 = "Nhập tên máy in" [quickAccess] +access = "Truy cập" +accessAddPerson = "Thêm ngưá»i khác" +accessBack = "Quay lại" +accessCopyLink = "Sao chép liên kết" +accessEmail = "Äịa chỉ email" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "Tệp" +accessGeneral = "Quyá»n truy cập chung" +accessInviteTitle = "Má»i má»i ngưá»i" +accessOwner = "Chá»§ sở hữu" +accessPanel = "Quyá»n truy cập tài liệu" +accessPeople = "Ngưá»i có quyá»n truy cập" +accessRemove = "Xóa" +accessRestricted = "Bị hạn chế" +accessRestrictedHint = "Chỉ những ngưá»i có quyá»n truy cập má»›i có thể mở" +accessRole = "Vai trò" +accessRoleCommenter = "Ngưá»i bình luận" +accessRoleEditor = "Ngưá»i chỉnh sá»­a" +accessRoleViewer = "Ngưá»i xem" +accessSelectedFile = "Tệp đã chá»n" +accessSendInvite = "Gá»­i lá»i má»i" +accessTitle = "Quyá»n truy cập tài liệu" +accessYou = "Bạn" account = "Tài khoản" +activeSessions = "Phiên Ä‘ang hoạt động" +activeTab = "Äang hoạt động" activity = "Hoạt động" adminSettings = "Cài đặt quản trị" +allSessions = "Tất cả phiên" allTools = "All Tools" automate = "Tá»± động hóa" +back = "Quay lại" +certSign = "Ký bằng chứng chỉ" +completedSessions = "Phiên đã hoàn tất" +completedTab = "Äã hoàn tất" config = "Cấu hình" +createNew = "Tạo yêu cầu má»›i" +createSession = "Tạo yêu cầu ký" +dueDate = "Ngày đến hạn (tùy chá»n)" files = "Tệp" help = "Trợ giúp" +noActiveSessions = "Không có yêu cầu ký Ä‘ang chá» hoặc phiên Ä‘ang hoạt động" +noCompletedSessions = "Không có phiên đã hoàn tất" +noFile = "Chưa chá»n tệp" read = "Äá»c" reader = "Trình Ä‘á»c" +refresh = "Làm má»›i" +requestSignatures = "Yêu cầu chữ ký" +selectSingleFileToRequest = "Chá»n má»™t tệp PDF duy nhất để yêu cầu chữ ký" +selectedFile = "Tệp đã chá»n" +selectUsers = "Chá»n ngưá»i dùng để ký" +selectUsersPlaceholder = "Chá»n ngưá»i tham gia..." +sendingRequest = "Äang gá»­i..." settings = "Cài đặt" showMeAround = "Dẫn tôi tham quan" sign = "Ký" +signatureRequests = "Yêu cầu ký" +signYourself = "Tá»± ký" +newRequest = "Yêu cầu má»›i" tours = "Hướng dẫn" +wetSign = "Thêm chữ ký" +filterMine = "Cá»§a tôi" +filterOverdue = "Quá hạn" +filterSigned = "Äã ký" +filterDeclined = "Äã từ chối" +searchDocuments = "Tìm kiếm tài liệu…" [quickAccess.helpMenu] adminTour = "Hướng dẫn quản trị" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "Máy chá»§ Stirling-PDF cá»§a bạn Ä‘ang ngoại tuy expired = "Phiên cá»§a bạn đã hết hạn. Vui lòng làm má»›i trang và thá»­ lại." refreshPage = "Làm má»›i trang" +[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ý" + +[signatureSettings.tooltip] +header = "Cài đặt giao diện chữ ký" + +[signatureSettings.tooltip.location] +bullet1 = "Ví dụ: \"New York, USA\", \"London Office\", \"Remote\"" +bullet2 = "Không giống vị trí trên trang" +bullet3 = "Có thể bắt buá»™c trong má»™t số khu vá»±c pháp lý" +description = "Vị trí địa lý tùy chá»n nÆ¡i áp dụng chữ ký. ÄÆ°á»£c lưu trong siêu dữ liệu chứng chỉ." +title = "Vị trí chữ ký" + +[signatureSettings.tooltip.logo] +bullet1 = "Hiển thị cùng chữ ký và văn bản" +bullet2 = "Há»— trợ định dạng PNG, JPG" +bullet3 = "Tăng tính chuyên nghiệp" +description = "Thêm logo công ty vào chữ ký hiển thị để tăng nhận diện và tính xác thá»±c." +title = "Logo công ty" + +[signatureSettings.tooltip.reason] +bullet1 = "Ví dụ: \"Approval\", \"Contract Agreement\", \"Review Complete\"" +bullet2 = "Hiển thị trong thuá»™c tính chữ ký cá»§a PDF" +bullet3 = "Hữu ích cho kiểm toán và tuân thá»§" +description = "Văn bản tùy chá»n giải thích lý do ký tài liệu. ÄÆ°á»£c lưu trong siêu dữ liệu chứng chỉ." +title = "Lý do ký" + +[signatureSettings.tooltip.visibility] +bullet1 = "Hiển thị: Chữ ký xuất hiện trên PDF vá»›i giao diện tùy chỉnh" +bullet2 = "Ẩn: Chứng chỉ được nhúng mà không có dấu hiệu trá»±c quan" +bullet3 = "Chữ ký ẩn vẫn cung cấp xác thá»±c mật mã" +description = "Kiểm soát chữ ký có hiển thị trên tài liệu hay được nhúng ẩn." +title = "Mức hiển thị chữ ký" + [settings.configuration] advanced = "Nâng cao" database = "CÆ¡ sở dữ liệu" endpoints = "Endpoints" features = "Tính năng" +storageSharing = "Lưu trữ & Chia sẻ Tệp" systemSettings = "Cài đặt hệ thống" title = "Cấu hình" @@ -6332,10 +6868,13 @@ title = "Äăng nhập vào Stirling" [setup.selfhosted] link = "hoặc kết nối tá»›i tài khoản tá»± lưu trữ" subtitle = "Nhập thông tin đăng nhập server" +changeServerLocked = "Tổ chức cá»§a bạn đã giá»›i hạn ứng dụng này vào má»™t máy chá»§ cụ thể" switchToLocal = "Thay vào đó dùng công cụ cục bá»™" title = "Äăng nhập vào server" [setup.selfhosted.unreachable] +changeServer = "Kết nối tá»›i máy chá»§ khác" +changeServerLocked = "Tổ chức cá»§a bạn đã giá»›i hạn ứng dụng này vào má»™t máy chá»§ cụ thể" continueOffline = "Thay vào đó dùng công cụ cục bá»™" message = "Không thể truy cập {{url}}. Kiểm tra xem máy chá»§ có Ä‘ang chạy và có thể truy cập hay không." retry = "Thá»­ lại" @@ -6529,6 +7068,15 @@ saved = "Äã lưu" text = "Văn bản" title = "Loại chữ ký" +[signRequest] +declined = "Yêu cầu ký đã bị từ chối" +fetchFailed = "Không tải được yêu cầu ký" +signed = "Ký tài liệu thành công" + +[signSession] +createFailed = "Không tạo được yêu cầu ký" +created = "Äã gá»­i yêu cầu ký" + [signup] accountCreatedSuccessfully = "Tạo tài khoản thành công! Bạn có thể đăng nhập ngay bây giá»." alreadyHaveAccount = "Äã có tài khoản? Äăng nhập" @@ -6807,6 +7355,106 @@ title = "Tách PDF theo chương" [splitPdfByChapters] tags = "tách,chương,dấu trang,sắp xếp" +[storageShare] +accessed = "Äã truy cập" +accessDenied = "Bạn không có quyá»n truy cập tệp được chia sẻ này. Hãy yêu cầu chá»§ sở hữu chia sẻ vá»›i bạn." +accessFailed = "Không thể tải hoạt động." +accessDeniedBody = "Bạn không có quyá»n truy cập tệp này. Hãy yêu cầu chá»§ sở hữu chia sẻ vá»›i bạn." +accessDeniedTitle = "Không có quyá»n truy cập" +accessLimitedCommenter = "Quyá»n bình luận sắp ra mắt. Hãy yêu cầu quyá»n chỉnh sá»­a nếu bạn cần tải xuống." +accessLimitedTitle = "Quyá»n truy cập hạn chế" +accessLimitedViewer = "Liên kết này chỉ cho phép xem. Hãy yêu cầu quyá»n chỉnh sá»­a nếu bạn cần tải xuống." +createdAt = "Äã tạo" +download = "Tải xuống" +downloadFailed = "Không thể tải xuống tệp này." +expiredBody = "Liên kết chia sẻ này không hợp lệ hoặc đã hết hạn." +expiredTitle = "Liên kết đã hết hạn" +goToLogin = "Äi tá»›i đăng nhập" +loadFailed = "Không thể mở tệp được chia sẻ." +loading = "Äang tải liên kết chia sẻ..." +loginPrompt = "Äăng nhập để truy cập tệp được chia sẻ này." +loginRequired = "Yêu cầu đăng nhập" +openInApp = "Mở trong Stirling PDF" +ownerLabel = "Chá»§ sở hữu" +ownerUnknown = "Không xác định" +requiresLogin = "Tệp được chia sẻ này yêu cầu đăng nhập." +roleCommenter = "Ngưá»i bình luận" +roleEditor = "Ngưá»i chỉnh sá»­a" +roleViewer = "Ngưá»i xem" +shareHeading = "Tệp được chia sẻ" +titleDefault = "Tệp được chia sẻ" +tryAgain = "Vui lòng thá»­ lại sau." +addUser = "Thêm" +commenterHint = "Tính năng bình luận sắp ra mắt." +copied = "Äã sao chép liên kết vào bá»™ nhá»› tạm" +copy = "Sao chép" +copyFailed = "Sao chép thất bại" +description = "Tạo liên kết chia sẻ cho tệp này. Ngưá»i dùng đã đăng nhập có liên kết có thể truy cập." +downloadsCount = "Lượt tải xuống: {{count}}" +emailWarningBody = "Có vẻ đây là má»™t địa chỉ email. Nếu ngưá»i này chưa là ngưá»i dùng Stirling PDF, há» sẽ không thể truy cập tệp." +emailWarningConfirm = "Vẫn chia sẻ" +emailWarningTitle = "Äịa chỉ email" +errorTitle = "Chia sẻ thất bại" +failure = "Không thể tạo liên kết chia sẻ. Vui lòng thá»­ lại." +fileLabel = "Tệp" +generate = "Tạo liên kết" +generated = "Äã tạo liên kết chia sẻ" +hideActivity = "Ẩn hoạt động" +invalidUsername = "Nhập tên ngưá»i dùng hoặc địa chỉ email hợp lệ." +lastAccessed = "Truy cập lần cuối" +linkAccessTitle = "Quyá»n truy cập bằng liên kết chia sẻ" +linkLabel = "Liên kết chia sẻ" +linksDisabled = "Liên kết chia sẻ đã bị tắt." +linksDisabledBody = "Liên kết chia sẻ bị tắt bởi cài đặt máy chá»§ cá»§a bạn." +manage = "Quản lý chia sẻ" +manageDescription = "Tạo và quản lý các liên kết để chia sẻ tệp này." +manageLoadFailed = "Không thể tải các liên kết chia sẻ." +manageTitle = "Quản lý chia sẻ" +noActivity = "Chưa có hoạt động." +noLinks = "Chưa có liên kết chia sẻ Ä‘ang hoạt động." +noSharedUsers = "Chưa có ngưá»i dùng nào có quyá»n truy cập." +removeLink = "Xóa liên kết" +removeUser = "Xóa" +revokeFailed = "Không thể xóa liên kết chia sẻ." +revoked = "Liên kết chia sẻ đã bị xóa" +roleLabel = "Vai trò" +sharingDisabled = "Chia sẻ đã bị tắt." +sharingDisabledBody = "Chia sẻ đã bị tắt bởi cài đặt máy chá»§ cá»§a bạn." +sharedUsersTitle = "Ngưá»i dùng được chia sẻ" +title = "Chia sẻ tệp" +unknownUser = "Ngưá»i dùng không xác định" +userAddFailed = "Không thể chia sẻ vá»›i ngưá»i dùng đó." +userAdded = "Äã thêm ngưá»i dùng vào danh sách chia sẻ." +usernameLabel = "Tên ngưá»i dùng hoặc email" +usernamePlaceholder = "Nhập tên ngưá»i dùng hoặc email" +userRemoveFailed = "Không thể xóa ngưá»i dùng đó." +userRemoved = "Äã xóa ngưá»i dùng khá»i danh sách chia sẻ." +viewActivity = "Xem hoạt động" +viewed = "Äã xem" +viewsCount = "Lượt xem: {{count}}" +downloaded = "Äã tải xuống" +bulkDescription = "Tạo má»™t liên kết để chia sẻ tất cả tệp đã chá»n vá»›i ngưá»i dùng đã đăng nhập." +bulkTitle = "Chia sẻ các tệp đã chá»n" +copyLink = "Sao chép liên kết chia sẻ" +fileCount = "Äã chá»n {{count}} tệp" +ownerOnly = "Chỉ chá»§ sở hữu má»›i có thể quản lý chia sẻ." +selectSingleFile = "Chá»n má»™t tệp để quản lý chia sẻ." + +[storageUpload] +description = "Thao tác này sẽ tải tệp hiện tại lên lưu trữ trên máy chá»§ để bạn truy cập." +errorTitle = "Tải lên không thành công" +failure = "Tải lên không thành công. Vui lòng kiểm tra đăng nhập và cài đặt lưu trữ cá»§a bạn." +fileLabel = "Tệp" +hint = "Liên kết công khai và chế độ truy cập được kiểm soát bởi cài đặt máy chá»§ cá»§a bạn." +success = "Äã tải lên máy chá»§" +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á»§" +bulkDescription = "Thao tác này sẽ tải các tệp đã chá»n lên lưu trữ trên máy chá»§ cá»§a bạn." +bulkTitle = "Tải lên các tệp đã chá»n" +fileCount = "Äã chá»n {{count}} tệp" +more = " +{{count}} nữa" + [storage] approximateSize = "Kích thước xấp xỉ" fileTooLarge = "Tệp quá lá»›n. Kích thước tối Ä‘a má»—i tệp là" @@ -7153,6 +7801,30 @@ title = "Xem/Chỉnh sá»­a PDF" [warning] tooltipTitle = "Cảnh báo" +[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ý" + [watermark] completed = "Äã thêm hình má»" desc = "Thêm hình má» bằng văn bản hoặc hình ảnh vào tệp PDF" @@ -7333,6 +8005,7 @@ activeSession = "Phiên hoạt động" addMembers = "Thêm thành viên" admin = "Quản trị viên" confirmDelete = "Bạn có chắc muốn xóa ngưá»i dùng này? Thao tác này không thể hoàn tác." +confirmUnlock = "Bạn có chắc muốn mở khóa tài khoản ngưá»i dùng này không?" deleteUser = "Xóa ngưá»i dùng" deleteUserError = "Xóa ngưá»i dùng thất bại" deleteUserSuccess = "Xóa ngưá»i dùng thành công" @@ -7341,6 +8014,8 @@ disable = "Tắt" disabled = "Äã tắt" editRole = "Chỉnh sá»­a vai trò" enable = "Bật" +locked = "đã khóa" +lockedBadge = "Äã khóa" loading = "Äang tải danh sách..." loginRequired = "Hãy bật chế độ đăng nhập trước" member = "Thành viên" @@ -7350,6 +8025,9 @@ searchMembers = "Tìm thành viên..." status = "Trạng thái" team = "Nhóm" title = "Má»i ngưá»i" +unlockAccount = "Mở khóa tài khoản" +unlockUserError = "Mở khóa tài khoản ngưá»i dùng không thành công" +unlockUserSuccess = "Äã mở khóa tài khoản ngưá»i dùng thành công" user = "Ngưá»i dùng" [workspace.people.actions] diff --git a/frontend/public/locales/zh-BO/translation.toml b/frontend/public/locales/zh-BO/translation.toml index 498d5b4d1e..54a440abe8 100644 --- a/frontend/public/locales/zh-BO/translation.toml +++ b/frontend/public/locales/zh-BO/translation.toml @@ -8,6 +8,7 @@ black = "ནག་པོ" blue = "སྔོན་པོ" bored = "སྒུག་སྡོད་སà¾à¾±à½²à½‘་པོ་མི་འདུག་གམà¼" cancel = "å–æ¶ˆ" +confirm = "确认" changedCredsMessage = "ངོ་སྤྲོད་ལག་à½à¾±à½ºà½¢à¼‹à½–སྒྱུར་ཟིནà¼" chooseFile = "选择文件" close = "སྒོ་རིགà¼" @@ -146,6 +147,7 @@ insufficientCredits = "点数ä¸è¶³ã€‚需è¦ï¼š{{requiredCredits}},å¯ç”¨ï¼š{{ loadingCredits = "正在检查点数..." loadingProStatus = "正在检查订阅状æ€..." noticeTopUpOrPlan = "点数ä¸è¶³ï¼Œè¯·å……值或å‡çº§åˆ°å¥—é¤" +accessInvite = "邀请" [account] accountSettings = "à½à½¼à¼‹à½˜à½²à½„་སྒྲིག་འགོདà¼" @@ -1427,6 +1429,34 @@ title = "处ç†" description = "在报告错误å‰ç­‰å¾…处ç†ä½œä¸šçš„æœ€é•¿æ—¶é—´ã€‚" label = "处ç†è¶…时(秒)" +[admin.settings.storage] +description = "控制æœåŠ¡å™¨å­˜å‚¨å’Œå…±äº«é€‰é¡¹ã€‚" +title = "文件存储与共享" + +[admin.settings.storage.enabled] +description = "å…许用户在æœåŠ¡å™¨ä¸Šå­˜å‚¨æ–‡ä»¶ã€‚" +label = "å¯ç”¨æœåŠ¡å™¨æ–‡ä»¶å­˜å‚¨" + +[admin.settings.storage.sharing.email] +description = "å…许通过电å­é‚®ç®±å…±äº«ã€‚" +label = "å¯ç”¨é‚®ç®±å…±äº«" +mailLink = "é…置邮件设置" +mailNote = "需è¦é‚®ä»¶é…置。 " + +[admin.settings.storage.sharing.enabled] +description = "å…许用户共享已存储的文件。" +label = "å¯ç”¨å…±äº«" + +[admin.settings.storage.sharing.links] +description = "å…许通过需登录的链接共享。" +frontendUrlLink = "在系统设置中é…ç½®" +frontendUrlNote = "需è¦å‰ç«¯ URL。 " +label = "å¯ç”¨å…±äº«é“¾æŽ¥" + +[admin.settings.storage.signing.enabled] +description = "å…许用户创建多人å‚与的文档签署会è¯ã€‚需è¦å¯ç”¨æœåŠ¡å™¨æ–‡ä»¶å­˜å‚¨ã€‚" +label = "å¯ç”¨ç¾¤ç»„签署(Alpha)" + [admin.settings.unsavedChanges] cancel = "继续编辑" discard = "放弃更改" @@ -2059,7 +2089,19 @@ numbers = "æ•°å­—/范围:5, 10-20" progressions = "等差å¼ï¼š3n, 4n+1" [certSign] +allSigned = "所有å‚与者å‡å·²ç­¾ç½²ã€‚坿œ€ç»ˆå®šç¨¿ã€‚" +awaitingSignatures = "等待签署" +signatureProgress = "{{signedCount}}/{{totalCount}} 个签å" chooseCertificate = "选择è¯ä¹¦æ–‡ä»¶" +declined = "已拒ç»" +fetchFailed = "加载签署数æ®å¤±è´¥" +finalized = "已定稿" +notified = "待处ç†" +partialNote = "您å¯ä»¥æå‰ä»¥å½“å‰ç­¾å最终定稿。未签署的å‚与者将被排除。" +pending = "待处ç†" +readyToFinalize = "坿œ€ç»ˆå®šç¨¿" +signed = "已签署" +viewed = "已查看" chooseJksFile = "选择 JKS 文件" chooseP12File = "选择 PKCS12 文件" choosePfxFile = "选择 PFX 文件" @@ -2082,6 +2124,7 @@ title = "ལག་à½à¾±à½ºà½¢à¼‹à½˜à½²à½„་རྟགསà¼" invisible = "ä¸å¯è§" stepTitle = "ç­¾å外观" visible = "å¯è§" +visibility = "å¯è§æ€§" [certSign.appearance.options] title = "ç­¾å详情" @@ -2188,6 +2231,252 @@ bullet4 = "å¯ä½¿ç”¨è‡ªå®šä¹‰è¯ä¹¦è¿›è¡ŒéªŒè¯" text = "åœ¨æ£€æŸ¥ç­¾åæ—¶ï¼Œå·¥å…·ä¼šå‘Šè¯‰æ‚¨å®ƒä»¬æ˜¯å¦æœ‰æ•ˆã€è°ç­¾ç½²äº†æ–‡æ¡£ã€ç­¾ç½²æ—¶é—´ï¼Œä»¥åŠæ–‡æ¡£åœ¨ç­¾ååŽæ˜¯å¦å·²è¢«æ›´æ”¹ã€‚" title = "检查签å" +[certSign.collab.finalize] +button = "完æˆå¹¶åŠ è½½å·²ç­¾ç½²çš„ PDF" +early = "以当å‰ç­¾å完æˆå®šç¨¿" + +[certSign.collab.sessionDetail] +addButton = "添加å‚与者" +addParticipants = "添加å‚与者" +addParticipantsError = "添加å‚与者失败" +backToList = "返回会è¯" +deleteConfirm = "确定å—?此æ“作无法撤销。" +deleteError = "删除会è¯å¤±è´¥" +deleted = "会è¯å·²åˆ é™¤" +deleteSession = "删除会è¯" +dueDate = "截止日期" +finalizeError = "会è¯å®šç¨¿å¤±è´¥" +loadPdfError = "加载已签署的 PDF 失败" +loadSignedPdf = "将已签署的 PDF 加载到活动文件" +messageLabel = "消æ¯" +noAdditionalInfo = "æ— å…¶ä»–ä¿¡æ¯" +owner = "所有者" +participantRemoved = "已移除å‚与者" +participants = "å‚与者" +participantsAdded = "å·²æˆåŠŸæ·»åŠ å‚与者" +removeParticipant = "移除" +removeParticipantError = "移除å‚与者失败" +selectUsers = "选择用户…" +sessionInfo = "会è¯ä¿¡æ¯" +workbenchTitle = "会è¯ç®¡ç†" + +[certSign.collab.signRequest] +addedToFiles = "文档已添加到活动文件" +addSignature = "添加您的签å" +addToFiles = "添加到活动文件" +advancedSettings = "高级设置" +backToList = "返回签署请求" +certificateChoice = "选择用于签署的è¯ä¹¦" +changeSignature = "更改签å" +clearSignature = "清除签å" +completeAndSign = "完æˆå¹¶ç­¾ç½²" +createNewSignature = "创建新签å" +declineButton = "æ‹’ç»" +decline = "æ‹’ç»è¯·æ±‚" +deleteSelected = "删除所选签å" +drawSignature = "在下方绘制您的签å" +dueDate = "截止日期" +fileTooLarge = "文件大å°å¿…é¡»å°äºŽ 5MB" +fontFamily = "字体" +fontSize = "å­—å·ï¼š{{size}}px" +fontSizePlaceholder = "大å°" +from = "æ¥è‡ª" +invalidCertFile = "请选择 P12 或 PFX è¯ä¹¦æ–‡ä»¶" +invalidFileType = "请选择图片文件" +location = "ä½ç½®ï¼ˆå¯é€‰ï¼‰" +locationPlaceholder = "您从哪里签署?" +message = "消æ¯" +noCertificate = "请选择è¯ä¹¦æ–‡ä»¶" +noSignatures = "请在 PDF 上至少放置一个签å" +p12File = "P12/PFX è¯ä¹¦æ–‡ä»¶" +password = "è¯ä¹¦å¯†ç " +passwordPlaceholder = "输入密ç â€¦" +penColor = "画笔颜色" +penSize = "画笔大å°ï¼š{{size}}px" +placementActive = "点击 PDF 放置" +placeSignatureButton = "在 PDF 上放置签å" +reason = "ç†ç”±ï¼ˆå¯é€‰ï¼‰" +reasonPlaceholder = "您为何签署?" +removeImage = "移除图片" +removeCertFile = "移除文件" +savedSignatures = "å·²ä¿å­˜çš„ç­¾å" +selectFile = "选择图片文件" +selectSignatureTitle = "选择或创建签å" +signButton = "签署文档" +signatureInfo = "这些设置由文档所有者é…ç½®" +signaturePlaced = "已在页é¢ä¸Šæ”¾ç½®ç­¾å" +signatureSettings = "ç­¾å设置" +signatureText = "ç­¾åæ–‡å­—" +signatureTextPlaceholder = "输入您的姓å…" +signatureTypeLabel = "ç­¾å类型" +signingTitle = "签署" +textColor = "文字颜色" +typeSignature = "è¾“å…¥æ‚¨çš„å§“åæ¥åˆ›å»ºç­¾å" +uploadCert = "自定义è¯ä¹¦" +uploadCertDesc = "使用您自己的 P12/PFX è¯ä¹¦" +uploadSignature = "上传您的签å图片" +usePersonalCert = "个人è¯ä¹¦" +usePersonalCertDesc = "为您的账户自动生æˆ" +useServerCert = "组织è¯ä¹¦" +useServerCertDesc = "共享的组织è¯ä¹¦" +workbenchTitle = "签署请求" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "选择笔画颜色" +continue = "ç»§ç»­" + +[certSign.collab.signRequest.certModal] +description = "您已放置 {{count}} 个签å。请选择è¯ä¹¦ä»¥å®Œæˆç­¾ç½²ã€‚" +sign = "签署文档" +certValidating = "正在验è¯è¯ä¹¦â€¦" +certValidUntil = "è¯ä¹¦æœ‰æ•ˆæœŸè‡³ {{date}}" +certInvalid = "è¯ä¹¦æ— æ•ˆï¼š{{error}}" +certInvalidFallback = "è¯ä¹¦æ— æ•ˆ" +certNetworkError = "无法验è¯è¯ä¹¦" +title = "é…ç½®è¯ä¹¦" + +[certSign.collab.signRequest.image] +hint = "上传您的签å PNG 或 JPG 图片" + +[certSign.collab.signRequest.mode] +move = "移动签å" +place = "放置签å" +title = "签署或移动模å¼" + +[certSign.collab.signRequest.modeTabs] +draw = "绘制" +image = "上传" +text = "输入" + +[certSign.collab.signRequest.placeSignature] +message = "点击 PDF 放置您的签å" +title = "放置签å" + +[certSign.collab.signRequest.preview] +imageAlt = "已选签å" +missing = "无预览" +textFallback = "ç­¾å" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "手绘签å" +defaultImageLabel = "已上传签å" +defaultLabel = "ç­¾å" +defaultTextLabel = "键入签å" +delete = "删除签å" +none = "暂无已ä¿å­˜çš„ç­¾å" + +[certSign.collab.signRequest.signatureType] +draw = "绘制" +type = "输入" +upload = "上传" + +[certSign.collab.signRequest.steps] +back = "返回" +cancelPlacement = "å–æ¶ˆæ”¾ç½®" +certificate = "è¯ä¹¦" +clickMultipleTimes = "在 PDF 上多次点击以放置多个签å。拖动任æ„ç­¾åå¯ç§»åŠ¨æˆ–è°ƒæ•´å¤§å°ã€‚" +clickToPlace = "点击 PDF 中您希望显示签åçš„ä½ç½®ã€‚" +continue = "继续到è¯ä¹¦é€‰æ‹©" +continueToPlacement = "继续到放置" +continueToReview = "继续到审阅" +createSignature = "创建签å" +invisible = "ä¸å¯è§" +location = "ä½ç½®ï¼š" +multipleSignatures = "å°†å‘ PDF 应用 {{count}} 个签å" +oneSignature = "å°†å‘ PDF 应用 1 个签å" +placeOnPdf = "放置到 PDF" +reason = "ç†ç”±ï¼š" +reviewTitle = "签署å‰å®¡é˜…" +signaturePlaced = "已在第 {{page}} 页放置签å。您å¯ä»¥å†æ¬¡ç‚¹å‡»ä»¥è°ƒæ•´ä½ç½®ï¼Œæˆ–继续进行审阅。" +visible = "å¯è§" +visibility = "å¯è§æ€§ï¼š" +yourSignatures = "您的签å({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "颜色" +fontLabel = "字体" +fontSizeLabel = "大å°" +fontSizePlaceholder = "16" +label = "ç­¾åæ–‡å­—" +modalHint = "输入您的姓å,然åŽç‚¹å‡»â€œç»§ç»­â€ä»¥å°†å…¶æ”¾ç½®åˆ° PDF 上。" +placeholder = "输入您的姓å…" + +[certSign.collab.participant] +certValidating = "正在验è¯è¯ä¹¦â€¦" +certValid = "✓ è¯ä¹¦æœ‰æ•ˆ" +certValidUntil = " 有效期至 {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "è¯ä¹¦æ— æ•ˆ" +certNetworkError = "无法验è¯è¯ä¹¦" + +[certSign.collab.addParticipants] +add = "添加 {{count}} åå‚与者" +back = "返回" +configureSignatures = "é…置签å设置" +continue = "继续到签å设置" +reasonHelp = "为这些å‚与者预设签署ç†ç”±ï¼ˆå¯é€‰ï¼Œç­¾ç½²æ—¶å¯è¦†ç›–)" +reasonPlaceholder = "例如:审批ã€å®¡æ ¸â€¦" +selectUsers = "选择用户" + +[certSign.collab.sessionCreation] +includeSummaryPage = "包å«ç­¾å摘è¦é¡µ" +includeSummaryPageHelp = "å°†åœ¨æ–‡æ¡£æœ«å°¾æ·»åŠ åŒ…å«æ‰€æœ‰ç­¾å元数æ®çš„æ‘˜è¦é¡µã€‚å„页的数字è¯ä¹¦ç­¾å框将被éšè—(手写签åä¸å—å½±å“)。" + +[certSign.collab.sessionList] +active = "活跃" +finalized = "已定稿" + +[certSign.collab.signatureSettings] +description = "é…置所有å‚ä¸Žè€…çš„ç­¾åæ˜¾ç¤ºæ–¹å¼" +title = "ç­¾å外观" + +[certSign.collab.userSelector] +inviteUsers = "添加用户" +loadError = "加载用户失败" +noTeam = "无团队" +noUsers = "未找到其他用户。" +placeholder = "选择用户…" + +[certSign.mobile] +panelActions = "æ“作" +panelDocument = "文档" +panelPeople = "人员" + +[certSign.sessions] +deleted = "会è¯å·²åˆ é™¤" +fetchFailed = "加载会è¯è¯¦æƒ…失败" +finalized = "会è¯å·²å®šç¨¿" +loaded = "已加载已签署的 PDF" +pdfNotReady = "PDF 尚未就绪" +pdfNotReadyDesc = "正在生æˆå·²ç­¾ç½²çš„ PDF。请ç¨åŽé‡è¯•。" + +[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" + [changeCreds] changePassword = "à½à¾±à½ºà½‘་ཀྱིས་སྔོན་སྒྲིག་ནང་འཛུལ་ངོ་སྤྲོད་བེད་སྤྱོད་བྱེད་བཞིན་ཡོད༠གསང་ཚིག་གསར་པ་འཇུག་རོགསà¼" changeUsername = "更新您的用户åã€‚æ›´æ–°åŽæ‚¨å°†è¢«ç™»å‡ºã€‚" @@ -3242,6 +3531,46 @@ totalSelected = "已选总数" unsupported = "䏿”¯æŒ" unzip = "解压" uploadError = "部分文件上传失败。" +copyCreated = "已将副本ä¿å­˜åˆ°æ­¤è®¾å¤‡ã€‚" +copyFailed = "无法创建副本。" +leaveShare = "从我的列表中移除" +leaveShareFailed = "无法移除此共享文件。" +leaveShareSuccess = "已从您的共享列表中移除。" +removeBoth = "åŒæ—¶ç§»é™¤" +removeFilePrompt = "此文件已ä¿å­˜åœ¨æ­¤è®¾å¤‡ä¸Žæ‚¨çš„æœåŠ¡å™¨ä¸Šã€‚æ‚¨æƒ³ä»Žå“ªé‡Œç§»é™¤ï¼Ÿ" +removeFileTitle = "移除文件" +removeLocalOnly = "仅此设备" +removeServerFailed = "无法从æœåŠ¡å™¨ç§»é™¤è¯¥æ–‡ä»¶ã€‚" +removeServerOnly = "ä»…æœåС噍" +removeServerOnlyPrompt = "此文件仅存储在您的æœåŠ¡å™¨ä¸Šã€‚æ˜¯å¦ä»ŽæœåŠ¡å™¨ç§»é™¤ï¼Ÿ" +removeServerSuccess = "已从æœåŠ¡å™¨ç§»é™¤ã€‚" +removeSharedPrompt = "此文件与您共享。您å¯ä»¥å°†å…¶ä»Žæ­¤è®¾å¤‡æˆ–您的共享列表中移除。" +removeSharedServerOnlyBlockedPrompt = "此文件与您共享,且仅存储在æœåŠ¡å™¨ä¸Šã€‚" +removeSharedServerOnlyPrompt = "此文件与您共享,且仅存储在æœåŠ¡å™¨ä¸Šã€‚è¦ä»Žæ‚¨çš„列表中移除å—?" +changesNotUploaded = "更改未上传" +cloudFile = "云端文件" +filterAll = "全部" +filterLocal = "本地" +filterSharedByMe = "我共享的" +filterSharedWithMe = "与我共享的" +lastSynced = "ä¸Šæ¬¡åŒæ­¥" +localOnly = "仅本地" +makeCopy = "制作副本" +owner = "所有者" +ownerUnknown = "未知" +share = "共享" +shareSelected = "共享所选" +sharedByYou = "您共享的" +sharedEditNoticeBody = "您对该文件的æœåŠ¡å™¨ç‰ˆæœ¬æ²¡æœ‰ç¼–è¾‘æƒé™ã€‚您所åšçš„任何编辑都将ä¿å­˜ä¸ºæœ¬åœ°å‰¯æœ¬ã€‚" +sharedEditNoticeConfirm = "我知é“了" +sharedEditNoticeTitle = "æœåС噍åªè¯»å‰¯æœ¬" +sharedWithYou = "与您共享" +sharing = "共享" +storageState = "存储" +synced = "å·²åŒæ­¥" +updateOnServer = "在æœåŠ¡å™¨ä¸Šæ›´æ–°" +uploadSelected = "上传所选" +uploadToServer = "上传到æœåС噍" [files] addFiles = "添加文件" @@ -3367,6 +3696,77 @@ title = "关于 PDF æ‰å¹³åŒ–" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "关于群组签署" + +[groupSigning.tooltip.finalization] +bullet1 = "所有签å将按您指定的å‚与者顺åºåº”用" +bullet2 = "如有需è¦ï¼Œæ‚¨å¯ä»¥åœ¨éƒ¨åˆ†ç­¾å的情况下最终定稿" +bullet3 = "一旦定稿,会è¯å°†æ— æ³•修改" +description = "当所有å‚与者都已签署(或您选择æå‰å®šç¨¿ï¼‰åŽï¼Œæ‚¨å¯ä»¥ç”Ÿæˆæœ€ç»ˆçš„已签署 PDF。" +title = "定稿æµç¨‹" + +[groupSigning.tooltip.roles] +bullet1 = "所有者(您):创建会è¯ã€é…置签å默认值ã€å®Œæˆå®šç¨¿" +bullet2 = "å‚与者:创建其签åã€é€‰æ‹©è¯ä¹¦ã€æ”¾ç½®åˆ° PDF" +bullet3 = "å‚与者无法修改签åå¯è§æ€§ã€ç†ç”±æˆ–ä½ç½®è®¾ç½®" +description = "您为所有å‚与者控制签å外观设置。" +title = "å‚与者角色" + +[groupSigning.tooltip.sequential] +bullet1 = "第一ä½å‚与者签署åŽï¼Œç¬¬äºŒä½æ‰èƒ½è®¿é—®æ–‡æ¡£" +bullet2 = "ç¡®ä¿ç¬¦åˆè¦æ±‚的签署顺åºä»¥æ»¡è¶³åˆè§„" +bullet3 = "您å¯ä»¥åœ¨åˆ—è¡¨ä¸­æ‹–åŠ¨ä»¥é‡æ–°æŽ’åºå‚与者" +description = "å‚与者将按您指定的顺åºç­¾ç½²æ–‡æ¡£ã€‚到其签署时,æ¯ä½ç­¾ç½²è€…都会收到通知。" +title = "顺åºç­¾ç½²" + +[groupSigning.steps] +back = "返回" +completed = "已完æˆ" +current = "当å‰" +stepLabel = "第 {{number}} æ­¥" + +[groupSigning.steps.configureDefaults] +continue = "继续到审阅" +invisible = "ç­¾åå°†ä¸å¯è§ï¼ˆä»…元数æ®ï¼‰" +locationLabel = "ä½ç½®ï¼š" +preview = "预览" +reasonLabel = "ç†ç”±ï¼š" +title = "é…置签å设置" +visible = "ç­¾å将显示在第 {{page}} 页" + +[groupSigning.steps.review] +document = "文档" +dueDate = "截止日期(å¯é€‰ï¼‰" +dueDatePlaceholder = "选择截止日期…" +invisible = "ä¸å¯è§ï¼ˆä»…元数æ®ï¼‰" +location = "ä½ç½®ï¼š" +logo = "徽标:" +logoHidden = "无徽标" +logoShown = "已显示 Stirling PDF 徽标" +participants = "å‚与者" +reason = "ç†ç”±ï¼š" +send = "å‘é€ç­¾ç½²è¯·æ±‚" +signatureSettings = "ç­¾å设置" +title = "审阅会è¯è¯¦æƒ…" +titleShort = "审阅并å‘é€" +visibility = "å¯è§æ€§ï¼š" +visible = "显示在第 {{page}} 页" +participantCount = "{{count}} åå‚与者将按顺åºç­¾ç½²" + +[groupSigning.steps.selectDocument] +continue = "继续到å‚与者选择" +noFile = "请从您的活动文件中选择一个 PDF 以创建签署会è¯ã€‚" +selectedFile = "已选文档" +title = "选择文档" + +[groupSigning.steps.selectParticipants] +continue = "继续到签å设置" +count = "已选择 {{count}} åå‚与者" +label = "选择å‚与者" +placeholder = "选择需è¦ç­¾ç½²çš„å‚与者…" +title = "选择å‚与者" + [getPdfInfo] downloadJson = "JSON ཕབ་ལེནà¼" downloads = "下载" @@ -4460,7 +4860,10 @@ zoomOut = "缩å°" [viewer] cannotPreviewFile = "无法预览文件" +disableColorFilter = "ç¦ç”¨é¢œè‰²æ»¤é•œ" dualPageView = "åŒé¡µè§†å›¾" +enableDarkFilter = "å¯ç”¨æ·±è‰²æ»¤é•œ" +enableSepiaFilter = "å¯ç”¨æ£•è¤è‰²æ»¤é•œ" firstPage = "第一页" lastPage = "最åŽä¸€é¡µ" nextPage = "下一页" @@ -4470,6 +4873,22 @@ singlePageView = "å•页视图" unknownFile = "未知文件" zoomIn = "放大" zoomOut = "缩å°" +resetZoom = "é‡ç½®ç¼©æ”¾" + +[viewer.nonPdf] +fileTypeBadge = "{{type}} 文件" +convertToPdf = "转æ¢ä¸º PDF" +loading = "正在加载…" +emptyFile = "空文件" +csvStats = "{{rows}} 行 · {{columns}} 列 · {{size}}" +sortedBy = "排åºä¾æ®ï¼š{{column}}" +columnDefault = "列 {{index}}" +htmlPreviewWarning = "HTML 预览 — 外部资æºå¯èƒ½æ— æ³•加载 · {{size}}" +htmlPreview = "HTML 预览" +invalidJson = "无效的 JSON — 正在显示原始内容" +textStats = "{{lines}} 行 · {{size}}" +lineNumbers = "行å·" +renderMarkdown = "渲染 Markdown" [viewer.attachments] title = "附件" @@ -4531,6 +4950,7 @@ toggleAttachments = "切æ¢é™„ä»¶" toggleTheme = "切æ¢ä¸»é¢˜" language = "语言" toggleAnnotations = "åˆ‡æ¢æ³¨é‡Šå¯è§æ€§" +toggleLayers = "切æ¢å›¾å±‚" search = "æœç´¢ PDF" panMode = "平移模å¼" applyRedactionsFirst = "请先应用涂黑" @@ -5407,20 +5827,72 @@ title = "ཡིག་ཆ་པར་འདེབསà¼" 2 = "པར་འདེབས་འཕྲུལ་འà½à½¼à½¢à¼‹à½‚ྱི་མིང་འཇུག་པà¼" [quickAccess] +access = "访问" +accessAddPerson = "添加其他人员" +accessBack = "返回" +accessCopyLink = "å¤åˆ¶é“¾æŽ¥" +accessEmail = "电å­é‚®ç®±" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "文件" +accessGeneral = "通用访问" +accessInviteTitle = "邀请人员" +accessOwner = "所有者" +accessPanel = "文档访问" +accessPeople = "具有访问æƒé™çš„人员" +accessRemove = "移除" +accessRestricted = "å—é™" +accessRestrictedHint = "åªæœ‰æ‹¥æœ‰è®¿é—®æƒé™çš„人æ‰èƒ½æ‰“å¼€" +accessRole = "角色" +accessRoleCommenter = "评论者" +accessRoleEditor = "编辑者" +accessRoleViewer = "查看者" +accessSelectedFile = "已选文件" +accessSendInvite = "å‘é€é‚€è¯·" +accessTitle = "文档访问" +accessYou = "您" account = "账户" +activeSessions = "活动会è¯" +activeTab = "活动" activity = "活动" adminSettings = "管ç†å‘˜è®¾ç½®" +allSessions = "所有会è¯" allTools = "All Tools" automate = "自动化" +back = "返回" +certSign = "è¯ä¹¦ç­¾ç½²" +completedSessions = "已完æˆä¼šè¯" +completedTab = "已完æˆ" config = "é…ç½®" +createNew = "创建新请求" +createSession = "创建签署请求" +dueDate = "截止日期(å¯é€‰ï¼‰" files = "文件" help = "帮助" +noActiveSessions = "没有待处ç†çš„签署请求或活动会è¯" +noCompletedSessions = "没有已完æˆçš„会è¯" +noFile = "未选择文件" read = "阅读" reader = "阅读器" +refresh = "刷新" +requestSignatures = "请求签å" +selectSingleFileToRequest = "选择一个 PDF 文件以请求签å" +selectedFile = "已选文件" +selectUsers = "选择需è¦ç­¾ç½²çš„用户" +selectUsersPlaceholder = "选择å‚与者…" +sendingRequest = "正在å‘é€â€¦" settings = "设置" showMeAround = "带我看看" sign = "ç­¾å" +signatureRequests = "签署请求" +signYourself = "自己签署" +newRequest = "新请求" tours = "导览" +wetSign = "添加签å" +filterMine = "我的" +filterOverdue = "逾期" +filterSigned = "已签署" +filterDeclined = "已拒ç»" +searchDocuments = "æœç´¢æ–‡æ¡£â€¦" [quickAccess.helpMenu] adminTour = "管ç†å¯¼è§ˆ" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "您的 Stirling-PDF æœåŠ¡å™¨å·²ç¦»çº¿ï¼Œä¸”æœ¬åœ°åŽ expired = "à½à¾±à½ºà½‘་ཀྱི་གླེང་མོལ་དུས་ཡོལ་ཟིན༠ཤོག་ངོས་གསར་སྒྱུར་བྱས་ནས་ཡང་བསà¾à¾±à½¢à¼‹à½šà½¼à½‘་ལྟ་བྱེད་རོགསà¼" refreshPage = "ཤོག་ངོས་གསར་སྒྱུརà¼" +[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 = "签署顺åº" + +[signatureSettings.tooltip] +header = "ç­¾å外观设置" + +[signatureSettings.tooltip.location] +bullet1 = "示例:“New York, USAâ€ã€â€œLondon Officeâ€ã€â€œRemoteâ€" +bullet2 = "与页é¢ä½ç½®ä¸åŒ" +bullet3 = "在æŸäº›æ³•律辖区å¯èƒ½æ˜¯å¿…需的" +description = "å¯é€‰çš„签署地ç†ä½ç½®ã€‚存储在è¯ä¹¦å…ƒæ•°æ®ä¸­ã€‚" +title = "ç­¾åä½ç½®" + +[signatureSettings.tooltip.logo] +bullet1 = "与签åä¸Žæ–‡å­—ä¸€åŒæ˜¾ç¤º" +bullet2 = "æ”¯æŒ PNGã€JPG æ ¼å¼" +bullet3 = "æå‡ä¸“业外观" +description = "为å¯è§ç­¾å添加公å¸å¾½æ ‡ï¼Œä»¥å¢žå¼ºå“牌和真实性。" +title = "å…¬å¸å¾½æ ‡" + +[signatureSettings.tooltip.reason] +bullet1 = "示例:“Approvalâ€â€œContract Agreementâ€â€œReview Completeâ€" +bullet2 = "在 PDF ç­¾å属性中å¯è§" +bullet3 = "有助于审计追踪与åˆè§„" +description = "解释为何签署文档的å¯é€‰æ–‡æœ¬ã€‚存储在è¯ä¹¦å…ƒæ•°æ®ä¸­ã€‚" +title = "ç­¾åç†ç”±" + +[signatureSettings.tooltip.visibility] +bullet1 = "å¯è§ï¼šç­¾å将以自定义外观显示在 PDF 上" +bullet2 = "ä¸å¯è§ï¼šåµŒå…¥è¯ä¹¦ä½†æ— å¯è§†æ ‡è®°" +bullet3 = "ä¸å¯è§ç­¾ååŒæ ·æä¾›åŠ å¯†å­¦éªŒè¯" +description = "æŽ§åˆ¶ç­¾åæ˜¯åœ¨æ–‡æ¡£ä¸­å¯è§è¿˜æ˜¯ä»¥ä¸å¯è§æ–¹å¼åµŒå…¥ã€‚" +title = "ç­¾åå¯è§æ€§" + [settings.configuration] advanced = "高级" database = "æ•°æ®åº“" endpoints = "端点" features = "功能" +storageSharing = "文件存储与共享" systemSettings = "系统设置" title = "é…ç½®" @@ -6332,10 +6868,13 @@ title = "登录 Stirling" [setup.selfhosted] link = "或连接到自托管账户" subtitle = "输入您的æœåС噍凭æ®" +changeServerLocked = "您的组织已将此应用é™åˆ¶ä¸ºè¿žæŽ¥ç‰¹å®šæœåС噍" switchToLocal = "改用本地工具" title = "登录æœåС噍" [setup.selfhosted.unreachable] +changeServer = "连接到其他æœåС噍" +changeServerLocked = "您的组织已将此应用é™åˆ¶ä¸ºè¿žæŽ¥ç‰¹å®šæœåС噍" continueOffline = "改用本地工具" message = "无法访问 {{url}}。请检查æœåŠ¡å™¨æ˜¯å¦æ­£åœ¨è¿è¡Œä¸”å¯è®¿é—®ã€‚" retry = "é‡è¯•" @@ -6529,6 +7068,15 @@ saved = "å·²ä¿å­˜" text = "文本" title = "ç­¾å类型" +[signRequest] +declined = "签署请求已被拒ç»" +fetchFailed = "加载签署请求失败" +signed = "文档签署æˆåŠŸ" + +[signSession] +createFailed = "创建签署请求失败" +created = "å·²å‘é€ç­¾ç½²è¯·æ±‚" + [signup] accountCreatedSuccessfully = "è´¦å·åˆ›å»ºæˆåŠŸï¼æ‚¨çŽ°åœ¨å¯ä»¥ç™»å½•。" alreadyHaveAccount = "已有账å·ï¼Ÿç™»å½•" @@ -6807,6 +7355,106 @@ title = "ལེའ�་ལྟར་ PDF à½à¼‹à½‚ྱེསà¼" [splitPdfByChapters] tags = "à½à¼‹à½‚ྱེསà¼,ལེའུà¼,དཔེ་རྟགསà¼,གོ་སྒྲིག" +[storageShare] +accessed = "已访问" +accessDenied = "您无æƒè®¿é—®æ­¤å…±äº«æ–‡ä»¶ã€‚请请求所有者与您共享。" +accessFailed = "无法加载活动。" +accessDeniedBody = "您无æƒè®¿é—®æ­¤æ–‡ä»¶ã€‚请请求所有者与您共享。" +accessDeniedTitle = "无访问æƒé™" +accessLimitedCommenter = "å³å°†æ”¯æŒè¯„论æƒé™ã€‚è‹¥éœ€ä¸‹è½½ï¼Œè¯·å‘æ‰€æœ‰è€…请求编辑æƒé™ã€‚" +accessLimitedTitle = "å—é™è®¿é—®" +accessLimitedViewer = "æ­¤é“¾æŽ¥ä»…å¯æŸ¥çœ‹ã€‚è‹¥éœ€ä¸‹è½½ï¼Œè¯·å‘æ‰€æœ‰è€…请求编辑æƒé™ã€‚" +createdAt = "创建时间" +download = "下载" +downloadFailed = "无法下载此文件。" +expiredBody = "此共享链接无效或已过期。" +expiredTitle = "链接已过期" +goToLogin = "å‰å¾€ç™»å½•" +loadFailed = "无法打开共享文件。" +loading = "正在加载共享链接…" +loginPrompt = "登录以访问此共享文件。" +loginRequired = "需è¦ç™»å½•" +openInApp = "在 Stirling PDF 中打开" +ownerLabel = "所有者" +ownerUnknown = "未知" +requiresLogin = "此共享文件需è¦ç™»å½•。" +roleCommenter = "评论者" +roleEditor = "编辑者" +roleViewer = "查看者" +shareHeading = "已共享文件" +titleDefault = "已共享文件" +tryAgain = "请ç¨åŽå†è¯•。" +addUser = "添加" +commenterHint = "å³å°†æ”¯æŒè¯„论。" +copied = "链接已å¤åˆ¶åˆ°å‰ªè´´æ¿" +copy = "å¤åˆ¶" +copyFailed = "å¤åˆ¶å¤±è´¥" +description = "ä¸ºæ­¤æ–‡ä»¶åˆ›å»ºå…±äº«é“¾æŽ¥ã€‚æŒæœ‰è¯¥é“¾æŽ¥å¹¶å·²ç™»å½•的用户å¯è®¿é—®ã€‚" +downloadsCount = "下载次数:{{count}}" +emailWarningBody = "这看起æ¥åƒä¸€ä¸ªç”µå­é‚®ç®±åœ°å€ã€‚若此人尚未æˆä¸º Stirling PDF 用户,将无法访问该文件。" +emailWarningConfirm = "ä»è¦å…±äº«" +emailWarningTitle = "电å­é‚®ç®±åœ°å€" +errorTitle = "共享失败" +failure = "无法生æˆå…±äº«é“¾æŽ¥ã€‚请é‡è¯•。" +fileLabel = "文件" +generate = "生æˆé“¾æŽ¥" +generated = "已生æˆå…±äº«é“¾æŽ¥" +hideActivity = "éšè—活动" +invalidUsername = "è¯·è¾“å…¥æœ‰æ•ˆçš„ç”¨æˆ·åæˆ–电å­é‚®ç®±åœ°å€ã€‚" +lastAccessed = "上次访问" +linkAccessTitle = "共享链接访问" +linkLabel = "共享链接" +linksDisabled = "å·²ç¦ç”¨å…±äº«é“¾æŽ¥ã€‚" +linksDisabledBody = "æœåŠ¡å™¨è®¾ç½®å·²ç¦ç”¨å…±äº«é“¾æŽ¥ã€‚" +manage = "管ç†å…±äº«" +manageDescription = "创建并管ç†ç”¨äºŽå…±äº«æ­¤æ–‡ä»¶çš„链接。" +manageLoadFailed = "无法加载共享链接。" +manageTitle = "管ç†å…±äº«" +noActivity = "暂无活动。" +noLinks = "暂无有效的共享链接。" +noSharedUsers = "暂无用户拥有访问æƒé™ã€‚" +removeLink = "移除链接" +removeUser = "移除" +revokeFailed = "无法移除共享链接。" +revoked = "共享链接已移除" +roleLabel = "角色" +sharingDisabled = "共享已被ç¦ç”¨ã€‚" +sharingDisabledBody = "æœåŠ¡å™¨è®¾ç½®å·²ç¦ç”¨å…±äº«ã€‚" +sharedUsersTitle = "共享用户" +title = "共享文件" +unknownUser = "未知用户" +userAddFailed = "无法与该用户共享。" +userAdded = "已将用户添加到共享列表。" +usernameLabel = "ç”¨æˆ·åæˆ–邮箱" +usernamePlaceholder = "è¾“å…¥ç”¨æˆ·åæˆ–邮箱" +userRemoveFailed = "无法移除该用户。" +userRemoved = "已从共享列表中移除该用户。" +viewActivity = "查看活动" +viewed = "已查看" +viewsCount = "查看次数:{{count}}" +downloaded = "已下载" +bulkDescription = "创建一个链接,将所有所选文件共享给已登录用户。" +bulkTitle = "共享所选文件" +copyLink = "å¤åˆ¶å…±äº«é“¾æŽ¥" +fileCount = "已选择 {{count}} 个文件" +ownerOnly = "åªæœ‰æ‰€æœ‰è€…å¯ä»¥ç®¡ç†å…±äº«ã€‚" +selectSingleFile = "选择å•个文件以管ç†å…±äº«ã€‚" + +[storageUpload] +description = "è¿™ä¼šå°†å½“å‰æ–‡ä»¶ä¸Šä¼ åˆ°æœåŠ¡å™¨å­˜å‚¨ï¼Œä¾›æ‚¨è‡ªè¡Œè®¿é—®ã€‚" +errorTitle = "上传失败" +failure = "上传失败。请检查您的登录和存储设置。" +fileLabel = "文件" +hint = "公共链接和访问模å¼ç”±æ‚¨çš„æœåŠ¡å™¨è®¾ç½®æŽ§åˆ¶ã€‚" +success = "已上传到æœåС噍" +title = "上传到æœåС噍" +updateButton = "在æœåŠ¡å™¨ä¸Šæ›´æ–°" +uploadButton = "上传到æœåС噍" +bulkDescription = "这会将所选文件上传到您的æœåŠ¡å™¨å­˜å‚¨ã€‚" +bulkTitle = "上传所选文件" +fileCount = "已选择 {{count}} 个文件" +more = " +{{count}} 更多" + [storage] approximateSize = "大致大å°" fileTooLarge = "文件过大。å•个文件的最大大å°ä¸º" @@ -7153,6 +7801,30 @@ title = "查看/编辑 PDF" [warning] tooltipTitle = "警告" +[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 = "上传签å图åƒ" + [watermark] completed = "已添加水å°" desc = "å‘ PDF æ–‡ä»¶æ·»åŠ æ–‡æœ¬æˆ–å›¾åƒæ°´å°" @@ -7333,6 +8005,7 @@ activeSession = "活动会è¯" addMembers = "添加æˆå‘˜" admin = "管ç†å‘˜" confirmDelete = "确定è¦åˆ é™¤æ­¤ç”¨æˆ·å—?此æ“作无法撤销。" +confirmUnlock = "确定è¦è§£é”此用户账户å—?" deleteUser = "删除用户" deleteUserError = "删除用户失败" deleteUserSuccess = "用户删除æˆåŠŸ" @@ -7341,6 +8014,8 @@ disable = "ç¦ç”¨" disabled = "å·²ç¦ç”¨" editRole = "编辑角色" enable = "å¯ç”¨" +locked = "å·²é”定" +lockedBadge = "å·²é”定" loading = "正在加载人员..." loginRequired = "请先å¯ç”¨ç™»å½•模å¼" member = "æˆå‘˜" @@ -7350,6 +8025,9 @@ searchMembers = "æœç´¢æˆå‘˜..." status = "状æ€" team = "团队" title = "人员" +unlockAccount = "è§£é”账户" +unlockUserError = "è§£é”用户账户失败" +unlockUserSuccess = "å·²æˆåŠŸè§£é”用户账户" user = "用户" [workspace.people.actions] diff --git a/frontend/public/locales/zh-CN/translation.toml b/frontend/public/locales/zh-CN/translation.toml index bdf1a1286a..60f83ea9fd 100644 --- a/frontend/public/locales/zh-CN/translation.toml +++ b/frontend/public/locales/zh-CN/translation.toml @@ -8,6 +8,7 @@ black = "黑色" blue = "è“色" bored = "等待时觉得无èŠï¼Ÿ" cancel = "å–æ¶ˆ" +confirm = "确认" changedCredsMessage = "凭è¯å·²æ›´æ”¹ï¼" chooseFile = "选择文件" close = "关闭" @@ -146,6 +147,7 @@ insufficientCredits = "积分ä¸è¶³ã€‚所需:{{requiredCredits}},å¯ç”¨ï¼š{{ loadingCredits = "正在检查积分..." loadingProStatus = "正在检查订阅状æ€..." noticeTopUpOrPlan = "积分ä¸è¶³ï¼Œè¯·å……值或å‡çº§è‡³æŸä¸ªå¥—é¤" +accessInvite = "邀请" [account] accountSettings = "è´¦å·è®¾å®š" @@ -1427,6 +1429,34 @@ title = "处ç†" description = "等待处ç†ä½œä¸šçš„æœ€é•¿æ—¶é—´ï¼Œè¶…æ—¶åŽæŠ¥å‘Šé”™è¯¯ã€‚" label = "处ç†è¶…时(秒)" +[admin.settings.storage] +description = "控制æœåŠ¡å™¨å­˜å‚¨å’Œå…±äº«é€‰é¡¹ã€‚" +title = "文件存储与共享" + +[admin.settings.storage.enabled] +description = "å…许用户在æœåŠ¡å™¨ä¸Šå­˜å‚¨æ–‡ä»¶ã€‚" +label = "å¯ç”¨æœåŠ¡å™¨æ–‡ä»¶å­˜å‚¨" + +[admin.settings.storage.sharing.email] +description = "å…许通过电å­é‚®ä»¶åœ°å€è¿›è¡Œå…±äº«ã€‚" +label = "å¯ç”¨ç”µå­é‚®ä»¶å…±äº«" +mailLink = "é…置邮件设置" +mailNote = "需è¦é‚®ä»¶é…置。 " + +[admin.settings.storage.sharing.enabled] +description = "å…许用户共享已存储的文件。" +label = "å¯ç”¨å…±äº«" + +[admin.settings.storage.sharing.links] +description = "å…许通过需登录的链接进行共享。" +frontendUrlLink = "在系统设置中é…ç½®" +frontendUrlNote = "需è¦å‰ç«¯ URL。 " +label = "å¯ç”¨å…±äº«é“¾æŽ¥" + +[admin.settings.storage.signing.enabled] +description = "å…许用户创建多人å‚与的文档签署会è¯ã€‚需è¦å¯ç”¨æœåŠ¡å™¨æ–‡ä»¶å­˜å‚¨ã€‚" +label = "å¯ç”¨ç¾¤ç»„签署(Alpha)" + [admin.settings.unsavedChanges] cancel = "继续编辑" discard = "丢弃更改" @@ -2059,7 +2089,19 @@ numbers = "æ•°å­—/范围:5,10-20" progressions = "等差:3n,4n+1" [certSign] +allSigned = "所有å‚与者å‡å·²ç­¾ç½²ã€‚å¯è¿›è¡Œæœ€ç»ˆç¡®å®šã€‚" +awaitingSignatures = "等待签署" +signatureProgress = "{{signedCount}}/{{totalCount}} 个签å" chooseCertificate = "选择è¯ä¹¦æ–‡ä»¶" +declined = "已拒ç»" +fetchFailed = "加载签署数æ®å¤±è´¥" +finalized = "已最终确定" +notified = "待处ç†" +partialNote = "您å¯ä»¥å…ˆè¡Œä»¥å½“å‰ç­¾å进行最终确定。未签署的å‚与者将被排除。" +pending = "待处ç†" +readyToFinalize = "坿œ€ç»ˆç¡®å®š" +signed = "已签署" +viewed = "已查看" chooseJksFile = "选择 JKS 文件" chooseP12File = "选择 PKCS12 文件" choosePfxFile = "选择 PFX 文件" @@ -2082,6 +2124,7 @@ title = "è¯ä¹¦ç­¾å" invisible = "ä¸å¯è§" stepTitle = "ç­¾å外观" visible = "å¯è§" +visibility = "å¯è§æ€§" [certSign.appearance.options] title = "ç­¾å详情" @@ -2188,6 +2231,252 @@ bullet4 = "å¯ä½¿ç”¨è‡ªå®šä¹‰è¯ä¹¦è¿›è¡ŒéªŒè¯" text = "æ£€æŸ¥ç­¾åæ—¶ï¼Œå·¥å…·ä¼šå‘Šè¯‰æ‚¨ç­¾åæ˜¯å¦æœ‰æ•ˆã€è°ç­¾äº†åã€ä½•时签的,以åŠè‡ªç­¾ç½²åŽæ–‡æ¡£æ˜¯å¦è¢«æ›´æ”¹ã€‚" title = "检查签å" +[certSign.collab.finalize] +button = "最终确定并加载已签署的 PDF" +early = "以当å‰ç­¾å最终确定" + +[certSign.collab.sessionDetail] +addButton = "添加å‚与者" +addParticipants = "添加å‚与者" +addParticipantsError = "添加å‚与者失败" +backToList = "返回会è¯" +deleteConfirm = "您确定å—?此æ“作无法撤销。" +deleteError = "删除会è¯å¤±è´¥" +deleted = "会è¯å·²åˆ é™¤" +deleteSession = "删除会è¯" +dueDate = "截止日期" +finalizeError = "最终确定会è¯å¤±è´¥" +loadPdfError = "加载已签署的 PDF 失败" +loadSignedPdf = "将已签署的 PDF 加载到活动文件" +messageLabel = "消æ¯" +noAdditionalInfo = "æ— å…¶ä»–ä¿¡æ¯" +owner = "所有者" +participantRemoved = "已移除å‚与者" +participants = "å‚与者" +participantsAdded = "å·²æˆåŠŸæ·»åŠ å‚与者" +removeParticipant = "移除" +removeParticipantError = "移除å‚与者失败" +selectUsers = "选择用户..." +sessionInfo = "会è¯ä¿¡æ¯" +workbenchTitle = "会è¯ç®¡ç†" + +[certSign.collab.signRequest] +addedToFiles = "文档已添加到活动文件" +addSignature = "添加您的签å" +addToFiles = "添加到活动文件" +advancedSettings = "高级设置" +backToList = "返回签署请求" +certificateChoice = "选择用于签署的è¯ä¹¦" +changeSignature = "更改签å" +clearSignature = "清除签å" +completeAndSign = "完æˆå¹¶ç­¾ç½²" +createNewSignature = "创建新签å" +declineButton = "æ‹’ç»" +decline = "æ‹’ç»è¯·æ±‚" +deleteSelected = "删除所选签å" +drawSignature = "在下方绘制您的签å" +dueDate = "截止日期" +fileTooLarge = "文件大å°å¿…é¡»å°äºŽ 5MB" +fontFamily = "字体" +fontSize = "字体大å°ï¼š{{size}}px" +fontSizePlaceholder = "大å°" +from = "æ¥è‡ª" +invalidCertFile = "请选择 P12 或 PFX è¯ä¹¦æ–‡ä»¶" +invalidFileType = "è¯·é€‰æ‹©å›¾åƒæ–‡ä»¶" +location = "ä½ç½®ï¼ˆå¯é€‰ï¼‰" +locationPlaceholder = "您从哪里进行签署?" +message = "消æ¯" +noCertificate = "请选择è¯ä¹¦æ–‡ä»¶" +noSignatures = "请至少在 PDF 上放置一个签å" +p12File = "P12/PFX è¯ä¹¦æ–‡ä»¶" +password = "è¯ä¹¦å¯†ç " +passwordPlaceholder = "请输入密ç ..." +penColor = "画笔颜色" +penSize = "画笔大å°ï¼š{{size}}px" +placementActive = "点击 PDF 进行放置" +placeSignatureButton = "在 PDF 上放置签å" +reason = "原因(å¯é€‰ï¼‰" +reasonPlaceholder = "为什么进行签署?" +removeImage = "移除图åƒ" +removeCertFile = "移除文件" +savedSignatures = "å·²ä¿å­˜çš„ç­¾å" +selectFile = "é€‰æ‹©å›¾åƒæ–‡ä»¶" +selectSignatureTitle = "选择或创建签å" +signButton = "签署文档" +signatureInfo = "这些设置由文档所有者é…ç½®" +signaturePlaced = "已在页é¢ä¸Šæ”¾ç½®ç­¾å" +signatureSettings = "ç­¾å设置" +signatureText = "ç­¾åæ–‡æœ¬" +signatureTextPlaceholder = "输入您的姓å..." +signatureTypeLabel = "ç­¾å类型" +signingTitle = "签署" +textColor = "文本颜色" +typeSignature = "输入您的姓å以创建签å" +uploadCert = "自定义è¯ä¹¦" +uploadCertDesc = "使用您自己的 P12/PFX è¯ä¹¦" +uploadSignature = "上传您的签å图åƒ" +usePersonalCert = "个人è¯ä¹¦" +usePersonalCertDesc = "ä¸ºæ‚¨çš„å¸æˆ·è‡ªåŠ¨ç”Ÿæˆ" +useServerCert = "组织è¯ä¹¦" +useServerCertDesc = "共享的组织è¯ä¹¦" +workbenchTitle = "签署请求" + +[certSign.collab.signRequest.canvas] +colorPickerTitle = "选择笔画颜色" +continue = "ç»§ç»­" + +[certSign.collab.signRequest.certModal] +description = "您已放置 {{count}} 个签å。选择您的è¯ä¹¦ä»¥å®Œæˆç­¾ç½²ã€‚" +sign = "签署文档" +certValidating = "正在验è¯è¯ä¹¦..." +certValidUntil = "è¯ä¹¦æœ‰æ•ˆæœŸè‡³ {{date}}" +certInvalid = "è¯ä¹¦æ— æ•ˆï¼š{{error}}" +certInvalidFallback = "è¯ä¹¦æ— æ•ˆ" +certNetworkError = "无法验è¯è¯ä¹¦" +title = "é…ç½®è¯ä¹¦" + +[certSign.collab.signRequest.image] +hint = "上传 PNG 或 JPG æ ¼å¼çš„ç­¾å图åƒ" + +[certSign.collab.signRequest.mode] +move = "移动签å" +place = "放置签å" +title = "签署或移动模å¼" + +[certSign.collab.signRequest.modeTabs] +draw = "绘制" +image = "上传" +text = "输入" + +[certSign.collab.signRequest.placeSignature] +message = "点击 PDF 以放置您的签å" +title = "放置签å" + +[certSign.collab.signRequest.preview] +imageAlt = "已选签å" +missing = "无预览" +textFallback = "ç­¾å" + +[certSign.collab.signRequest.saved] +defaultCanvasLabel = "手绘签å" +defaultImageLabel = "已上传的签å" +defaultLabel = "ç­¾å" +defaultTextLabel = "输入的签å" +delete = "删除签å" +none = "æ— å·²ä¿å­˜çš„ç­¾å" + +[certSign.collab.signRequest.signatureType] +draw = "绘制" +type = "输入" +upload = "上传" + +[certSign.collab.signRequest.steps] +back = "返回" +cancelPlacement = "å–æ¶ˆæ”¾ç½®" +certificate = "è¯ä¹¦" +clickMultipleTimes = "在 PDF 上点击多次以放置多个签å。拖动任æ„ç­¾åå¯ç§»åŠ¨æˆ–è°ƒæ•´å¤§å°ã€‚" +clickToPlace = "点击 PDF 上希望显示签åçš„ä½ç½®ã€‚" +continue = "继续到è¯ä¹¦é€‰æ‹©" +continueToPlacement = "继续进行放置" +continueToReview = "继续到审阅" +createSignature = "创建签å" +invisible = "ä¸å¯è§" +location = "ä½ç½®ï¼š" +multipleSignatures = "将对 PDF 应用 {{count}} 个签å" +oneSignature = "将对 PDF 应用 1 个签å" +placeOnPdf = "放置到 PDF" +reason = "原因:" +reviewTitle = "签署å‰å®¡é˜…" +signaturePlaced = "ç­¾å已放置在第 {{page}} 页。您å¯ä»¥å†æ¬¡ç‚¹å‡»ä»¥è°ƒæ•´ä½ç½®ï¼Œæˆ–继续审阅。" +visible = "å¯è§" +visibility = "å¯è§æ€§ï¼š" +yourSignatures = "您的签å({{count}})" + +[certSign.collab.signRequest.text] +colorLabel = "颜色" +fontLabel = "字体" +fontSizeLabel = "大å°" +fontSizePlaceholder = "16" +label = "ç­¾åæ–‡æœ¬" +modalHint = "输入您的姓å,然åŽç‚¹å‡»â€œç»§ç»­â€å°†å…¶æ”¾ç½®åˆ° PDF。" +placeholder = "输入您的姓å..." + +[certSign.collab.participant] +certValidating = "正在验è¯è¯ä¹¦..." +certValid = "✓ è¯ä¹¦æœ‰æ•ˆ" +certValidUntil = " 截至 {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "è¯ä¹¦æ— æ•ˆ" +certNetworkError = "无法验è¯è¯ä¹¦" + +[certSign.collab.addParticipants] +add = "添加 {{count}} åå‚与者" +back = "返回" +configureSignatures = "é…置签å设置" +continue = "继续到签å设置" +reasonHelp = "为这些å‚与者预设签署原因(å¯é€‰ï¼Œç­¾ç½²æ—¶å¯è¦†ç›–)" +reasonPlaceholder = "例如:审批ã€å®¡é˜…..." +selectUsers = "选择用户" + +[certSign.collab.sessionCreation] +includeSummaryPage = "包å«ç­¾å摘è¦é¡µ" +includeSummaryPageHelp = "å°†åœ¨æœ«å°¾æ·»åŠ åŒ…å«æ‰€æœ‰ç­¾å元数æ®çš„æ‘˜è¦é¡µã€‚å„页é¢ä¸Šçš„æ•°å­—è¯ä¹¦ç­¾å框将被éšè—(手写签åä¸å—å½±å“)。" + +[certSign.collab.sessionList] +active = "进行中" +finalized = "已最终确定" + +[certSign.collab.signatureSettings] +description = "为所有å‚与者é…置签å的显示方å¼" +title = "ç­¾å外观" + +[certSign.collab.userSelector] +inviteUsers = "添加用户" +loadError = "加载用户失败" +noTeam = "无团队" +noUsers = "未找到其他用户。" +placeholder = "选择用户..." + +[certSign.mobile] +panelActions = "æ“作" +panelDocument = "文档" +panelPeople = "人员" + +[certSign.sessions] +deleted = "会è¯å·²åˆ é™¤" +fetchFailed = "加载会è¯è¯¦æƒ…失败" +finalized = "会è¯å·²æœ€ç»ˆç¡®å®š" +loaded = "已加载已签署的 PDF" +pdfNotReady = "PDF 未就绪" +pdfNotReadyDesc = "正在生æˆå·²ç­¾ç½²çš„ PDF。请ç¨åŽé‡è¯•。" + +[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" + [changeCreds] changePassword = "您正在使用默认登录凭è¯ï¼Œè¯·è¾“入新密ç " changeUsername = "更新您的用户åã€‚æ›´æ–°åŽæ‚¨å°†è¢«ç™»å‡ºã€‚" @@ -3242,6 +3531,46 @@ totalSelected = "åˆè®¡å·²é€‰" unsupported = "䏿”¯æŒ" unzip = "解压" uploadError = "部分文件上传失败。" +copyCreated = "副本已ä¿å­˜åˆ°æ­¤è®¾å¤‡ã€‚" +copyFailed = "无法创建副本。" +leaveShare = "从我的列表中移除" +leaveShareFailed = "无法移除该共享文件。" +leaveShareSuccess = "已从您的共享列表中移除。" +removeBoth = "åŒæ—¶ä»Žä¸¤å¤„移除" +removeFilePrompt = "此文件已ä¿å­˜åœ¨æ­¤è®¾å¤‡å’Œæ‚¨çš„æœåŠ¡å™¨ä¸Šã€‚æ‚¨å¸Œæœ›ä»Žå“ªé‡Œç§»é™¤ï¼Ÿ" +removeFileTitle = "移除文件" +removeLocalOnly = "仅此设备" +removeServerFailed = "无法从æœåŠ¡å™¨ç§»é™¤è¯¥æ–‡ä»¶ã€‚" +removeServerOnly = "ä»…æœåС噍" +removeServerOnlyPrompt = "此文件仅存储在您的æœåŠ¡å™¨ä¸Šã€‚æ˜¯å¦ä»ŽæœåŠ¡å™¨ç§»é™¤ï¼Ÿ" +removeServerSuccess = "已从æœåŠ¡å™¨ç§»é™¤ã€‚" +removeSharedPrompt = "此文件与您共享。您å¯ä»¥å°†å…¶ä»Žæ­¤è®¾å¤‡æˆ–您的共享列表中移除。" +removeSharedServerOnlyBlockedPrompt = "此文件与您共享且仅存储在æœåŠ¡å™¨ä¸Šã€‚" +removeSharedServerOnlyPrompt = "此文件与您共享且仅存储在æœåŠ¡å™¨ä¸Šã€‚æ˜¯å¦å°†å…¶ä»Žæ‚¨çš„列表中移除?" +changesNotUploaded = "更改未上传" +cloudFile = "云端文件" +filterAll = "全部" +filterLocal = "本地" +filterSharedByMe = "我共享的" +filterSharedWithMe = "与我共享的" +lastSynced = "ä¸Šæ¬¡åŒæ­¥" +localOnly = "仅本地" +makeCopy = "创建副本" +owner = "所有者" +ownerUnknown = "未知" +share = "共享" +shareSelected = "共享所选" +sharedByYou = "您共享的" +sharedEditNoticeBody = "您对该文件的æœåŠ¡å™¨ç‰ˆæœ¬æ²¡æœ‰ç¼–è¾‘æƒé™ã€‚您进行的任何编辑将ä¿å­˜ä¸ºæœ¬åœ°å‰¯æœ¬ã€‚" +sharedEditNoticeConfirm = "知é“了" +sharedEditNoticeTitle = "æœåŠ¡å™¨å‰¯æœ¬ä¸ºåªè¯»" +sharedWithYou = "与您共享的" +sharing = "共享" +storageState = "存储" +synced = "å·²åŒæ­¥" +updateOnServer = "在æœåŠ¡å™¨ä¸Šæ›´æ–°" +uploadSelected = "上传所选" +uploadToServer = "上传到æœåС噍" [files] addFiles = "添加文件" @@ -3367,6 +3696,77 @@ title = "关于 PDF æ‰å¹³åŒ–" discord = "Discord" issues = "GitHub" +[groupSigning.tooltip] +header = "关于群组签署" + +[groupSigning.tooltip.finalization] +bullet1 = "所有签å将按照您指定的å‚与者顺åºåº”用" +bullet2 = "如有需è¦ï¼Œæ‚¨å¯ä»¥åœ¨éƒ¨åˆ†ç­¾å的情况下进行最终确定" +bullet3 = "一旦最终确定,会è¯å°†æ— æ³•修改" +description = "当所有å‚与者都已签署(或您选择æå‰æœ€ç»ˆç¡®å®šï¼‰åŽï¼Œæ‚¨å¯ä»¥ç”Ÿæˆæœ€ç»ˆçš„已签署 PDF。" +title = "最终确定æµç¨‹" + +[groupSigning.tooltip.roles] +bullet1 = "所有者(您):创建会è¯ã€é…置签åé»˜è®¤å€¼ã€æœ€ç»ˆç¡®å®šæ–‡æ¡£" +bullet2 = "å‚与者:创建其签åã€é€‰æ‹©è¯ä¹¦ã€åœ¨ PDF 上放置" +bullet3 = "å‚与者无法修改签åçš„å¯è§æ€§ã€åŽŸå› æˆ–ä½ç½®ä¿¡æ¯è®¾ç½®" +description = "您å¯ä¸ºæ‰€æœ‰å‚与者控制签å外观设置。" +title = "å‚与者角色" + +[groupSigning.tooltip.sequential] +bullet1 = "第一ä½å‚与者签署åŽï¼Œç¬¬äºŒä½æ‰èƒ½è®¿é—®æ–‡æ¡£" +bullet2 = "ç¡®ä¿ç¬¦åˆæ³•律åˆè§„所需的签署顺åº" +bullet3 = "您å¯ä»¥é€šè¿‡åœ¨åˆ—表中拖动æ¥é‡æ–°æŽ’åºå‚与者" +description = "å‚与者按您指定的顺åºç­¾ç½²æ–‡æ¡£ã€‚轮到æ¯ä½ç­¾ç½²è€…时,他们都会收到通知。" +title = "顺åºç­¾ç½²" + +[groupSigning.steps] +back = "返回" +completed = "已完æˆ" +current = "当å‰" +stepLabel = "步骤 {{number}}" + +[groupSigning.steps.configureDefaults] +continue = "继续到审阅" +invisible = "ç­¾åå°†ä¸å¯è§ï¼ˆä»…元数æ®ï¼‰" +locationLabel = "ä½ç½®ï¼š" +preview = "预览" +reasonLabel = "原因:" +title = "é…置签å设置" +visible = "ç­¾å将在第 {{page}} 页å¯è§" + +[groupSigning.steps.review] +document = "文档" +dueDate = "截止日期(å¯é€‰ï¼‰" +dueDatePlaceholder = "选择截止日期..." +invisible = "ä¸å¯è§ï¼ˆä»…元数æ®ï¼‰" +location = "ä½ç½®ï¼š" +logo = "Logo:" +logoHidden = "æ—  Logo" +logoShown = "显示 Stirling PDF Logo" +participants = "å‚与者" +reason = "原因:" +send = "å‘é€ç­¾ç½²è¯·æ±‚" +signatureSettings = "ç­¾å设置" +title = "审阅会è¯è¯¦æƒ…" +titleShort = "审阅并å‘é€" +visibility = "å¯è§æ€§ï¼š" +visible = "在第 {{page}} 页å¯è§" +participantCount = "将有 {{count}} åå‚与者按顺åºç­¾ç½²" + +[groupSigning.steps.selectDocument] +continue = "继续选择å‚与者" +noFile = "请从您的活动文件中选择一个 PDF 文件以创建签署会è¯ã€‚" +selectedFile = "已选择的文档" +title = "选择文档" + +[groupSigning.steps.selectParticipants] +continue = "继续到签å设置" +count = "已选择 {{count}} åå‚与者" +label = "选择å‚与者" +placeholder = "选择签署的å‚与者..." +title = "选择å‚与者" + [getPdfInfo] downloadJson = "下载 JSON" downloads = "下载" @@ -4460,7 +4860,10 @@ zoomOut = "缩å°" [viewer] cannotPreviewFile = "无法预览文件" +disableColorFilter = "ç¦ç”¨é¢œè‰²æ»¤é•œ" dualPageView = "åŒé¡µè§†å›¾" +enableDarkFilter = "å¯ç”¨æ·±è‰²æ»¤é•œ" +enableSepiaFilter = "å¯ç”¨è¤è‰²æ»¤é•œ" firstPage = "第一页" lastPage = "最åŽä¸€é¡µ" nextPage = "下一页" @@ -4470,6 +4873,22 @@ singlePageView = "å•页视图" unknownFile = "未知文件" zoomIn = "放大" zoomOut = "缩å°" +resetZoom = "é‡ç½®ç¼©æ”¾" + +[viewer.nonPdf] +fileTypeBadge = "{{type}} 文件" +convertToPdf = "转æ¢ä¸º PDF" +loading = "正在加载..." +emptyFile = "空文件" +csvStats = "{{rows}} 行 · {{columns}} 列 · {{size}}" +sortedBy = "排åºä¾æ®ï¼š{{column}}" +columnDefault = "列 {{index}}" +htmlPreviewWarning = "HTML 预览 — 外部资æºå¯èƒ½æ— æ³•加载 · {{size}}" +htmlPreview = "HTML 预览" +invalidJson = "无效的 JSON — 显示原始内容" +textStats = "{{lines}} 行 · {{size}}" +lineNumbers = "行å·" +renderMarkdown = "渲染 markdown" [viewer.attachments] title = "附件" @@ -4531,6 +4950,7 @@ toggleAttachments = "切æ¢é™„ä»¶" toggleTheme = "切æ¢ä¸»é¢˜" language = "语言" toggleAnnotations = "åˆ‡æ¢æ³¨é‡Šå¯è§æ€§" +toggleLayers = "切æ¢å›¾å±‚" search = "æœç´¢ PDF" panMode = "平移模å¼" applyRedactionsFirst = "请先应用涂黑" @@ -5407,20 +5827,72 @@ title = "æ‰“å°æ–‡ä»¶" 2 = "è¾“å…¥æ‰“å°æœºåç§°" [quickAccess] +access = "访问" +accessAddPerson = "添加其他人" +accessBack = "返回" +accessCopyLink = "å¤åˆ¶é“¾æŽ¥" +accessEmail = "电å­é‚®ä»¶åœ°å€" +accessEmailPlaceholder = "name@company.com" +accessFileLabel = "文件" +accessGeneral = "常规访问" +accessInviteTitle = "邀请人员" +accessOwner = "所有者" +accessPanel = "文档访问" +accessPeople = "有æƒè®¿é—®çš„人员" +accessRemove = "移除" +accessRestricted = "å—é™" +accessRestrictedHint = "仅有æƒé™çš„äººå¯æ‰“å¼€" +accessRole = "角色" +accessRoleCommenter = "评论者" +accessRoleEditor = "编辑者" +accessRoleViewer = "查看者" +accessSelectedFile = "已选择的文件" +accessSendInvite = "å‘é€é‚€è¯·" +accessTitle = "文档访问" +accessYou = "您" account = "账户" +activeSessions = "进行中的会è¯" +activeTab = "进行中" activity = "活动" adminSettings = "管ç†è®¾ç½®" +allSessions = "所有会è¯" allTools = "全部工具" automate = "自动化" +back = "返回" +certSign = "è¯ä¹¦ç­¾ç½²" +completedSessions = "已完æˆçš„会è¯" +completedTab = "已完æˆ" config = "é…ç½®" +createNew = "创建新请求" +createSession = "创建签署请求" +dueDate = "截止日期(å¯é€‰ï¼‰" files = "文件" help = "帮助" +noActiveSessions = "没有待处ç†çš„签署请求或进行中的会è¯" +noCompletedSessions = "没有已完æˆçš„会è¯" +noFile = "未选择文件" read = "阅读" reader = "阅读器" +refresh = "刷新" +requestSignatures = "请求签署" +selectSingleFileToRequest = "选择一个 PDF 文件以请求签署" +selectedFile = "已选择的文件" +selectUsers = "选择è¦ç­¾ç½²çš„用户" +selectUsersPlaceholder = "选择å‚与者..." +sendingRequest = "正在å‘é€..." settings = "设置" showMeAround = "带我看看" sign = "ç­¾å" +signatureRequests = "签署请求" +signYourself = "自行签署" +newRequest = "新请求" tours = "导览" +wetSign = "添加签å" +filterMine = "我的" +filterOverdue = "已逾期" +filterSigned = "已签署" +filterDeclined = "已拒ç»" +searchDocuments = "æœç´¢æ–‡æ¡£â€¦" [quickAccess.helpMenu] adminTour = "管ç†å¯¼è§ˆ" @@ -6050,11 +6522,75 @@ toolNotAvailableLocally = "您的 Stirling-PDF æœåŠ¡å™¨å¤„äºŽç¦»çº¿çŠ¶æ€ï¼Œä¸” expired = "您的会è¯å·²è¿‡æœŸã€‚请刷新页é¢å¹¶é‡è¯•。" refreshPage = "刷新页é¢" +[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 = "签署顺åº" + +[signatureSettings.tooltip] +header = "ç­¾å外观设置" + +[signatureSettings.tooltip.location] +bullet1 = "示例:“New York, USAâ€ã€â€œLondon Officeâ€ã€â€œRemoteâ€" +bullet2 = "与页é¢ä½ç½®ä¸åŒ" +bullet3 = "æŸäº›æ³•域å¯èƒ½è¦æ±‚填写" +description = "å¯é€‰çš„ç­¾å应用地ç†ä½ç½®ã€‚存储于è¯ä¹¦å…ƒæ•°æ®ä¸­ã€‚" +title = "ç­¾åä½ç½®" + +[signatureSettings.tooltip.logo] +bullet1 = "与签ååŠæ–‡å­—ä¸€åŒæ˜¾ç¤º" +bullet2 = "æ”¯æŒ PNGã€JPG æ ¼å¼" +bullet3 = "æå‡ä¸“业外观" +description = "为å¯è§ç­¾åæ·»åŠ å…¬å¸ Logo,以增强å“牌和真实性。" +title = "å…¬å¸ Logo" + +[signatureSettings.tooltip.reason] +bullet1 = "示例:“Approvalâ€ã€â€œContract Agreementâ€ã€â€œReview Completeâ€" +bullet2 = "在 PDF ç­¾å属性中å¯è§" +bullet3 = "有助于审计跟踪和åˆè§„" +description = "说明为何签署文档的å¯é€‰æ–‡æœ¬ã€‚存储于è¯ä¹¦å…ƒæ•°æ®ä¸­ã€‚" +title = "ç­¾å原因" + +[signatureSettings.tooltip.visibility] +bullet1 = "å¯è§ï¼šç­¾å以自定义外观显示在 PDF 上" +bullet2 = "ä¸å¯è§ï¼šåµŒå…¥è¯ä¹¦ä½†ä¸æ˜¾ç¤ºè§†è§‰æ ‡è®°" +bullet3 = "ä¸å¯è§ç­¾åä»æä¾›åŠ å¯†éªŒè¯" +description = "æŽ§åˆ¶ç­¾åæ˜¯åœ¨æ–‡æ¡£ä¸­å¯è§ï¼Œè¿˜æ˜¯ä»¥ä¸å¯è§æ–¹å¼åµŒå…¥ã€‚" +title = "ç­¾åå¯è§æ€§" + [settings.configuration] advanced = "高级" database = "æ•°æ®åº“" endpoints = "端点" features = "功能" +storageSharing = "文件存储与共享" systemSettings = "系统设置" title = "é…ç½®" @@ -6332,10 +6868,13 @@ title = "登录 Stirling" [setup.selfhosted] link = "或连接到自托管账户" subtitle = "输入您的æœåС噍凭æ®" +changeServerLocked = "您的组织已将此应用é™åˆ¶ä¸ºè¿žæŽ¥åˆ°ç‰¹å®šæœåС噍" switchToLocal = "改用本地工具" title = "登录到æœåС噍" [setup.selfhosted.unreachable] +changeServer = "连接到其他æœåС噍" +changeServerLocked = "您的组织已将此应用é™åˆ¶ä¸ºè¿žæŽ¥åˆ°ç‰¹å®šæœåС噍" continueOffline = "改用本地工具" message = "无法访问 {{url}}。请检查æœåŠ¡å™¨æ˜¯å¦æ­£åœ¨è¿è¡Œä¸”å¯è®¿é—®ã€‚" retry = "é‡è¯•" @@ -6529,6 +7068,15 @@ saved = "å·²ä¿å­˜" text = "文本" title = "ç­¾å类型" +[signRequest] +declined = "签署请求已被拒ç»" +fetchFailed = "加载签署请求失败" +signed = "文档签署æˆåŠŸ" + +[signSession] +createFailed = "创建签署请求失败" +created = "签署请求已å‘é€" + [signup] accountCreatedSuccessfully = "账户创建æˆåŠŸï¼æ‚¨çŽ°åœ¨å¯ä»¥ç™»å½•。" alreadyHaveAccount = "å·²ç»æœ‰è´¦æˆ·ï¼ŸåŽ»ç™»å½•" @@ -6807,6 +7355,106 @@ title = "按章节拆分 PDF" [splitPdfByChapters] tags = "分割,章节,书签,组织" +[storageShare] +accessed = "已访问" +accessDenied = "您无æƒè®¿é—®æ­¤å…±äº«æ–‡ä»¶ã€‚请请求所有者与您共享。" +accessFailed = "无法加载活动。" +accessDeniedBody = "您无æƒè®¿é—®æ­¤æ–‡ä»¶ã€‚请请求所有者与您共享。" +accessDeniedTitle = "æ— æƒé™" +accessLimitedCommenter = "评论æƒé™å³å°†ä¸Šçº¿ã€‚å¦‚éœ€ä¸‹è½½ï¼Œè¯·å‘æ‰€æœ‰è€…请求编辑者æƒé™ã€‚" +accessLimitedTitle = "å—é™è®¿é—®" +accessLimitedViewer = "æ­¤é“¾æŽ¥ä»…å¯æŸ¥çœ‹ã€‚å¦‚éœ€ä¸‹è½½ï¼Œè¯·å‘æ‰€æœ‰è€…请求编辑者æƒé™ã€‚" +createdAt = "创建时间" +download = "下载" +downloadFailed = "无法下载此文件。" +expiredBody = "此共享链接无效或已过期。" +expiredTitle = "链接已过期" +goToLogin = "å‰å¾€ç™»å½•" +loadFailed = "无法打开共享文件。" +loading = "正在加载共享链接..." +loginPrompt = "登录以访问此共享文件。" +loginRequired = "需è¦ç™»å½•" +openInApp = "在 Stirling PDF 中打开" +ownerLabel = "所有者" +ownerUnknown = "未知" +requiresLogin = "此共享文件需è¦ç™»å½•。" +roleCommenter = "评论者" +roleEditor = "编辑者" +roleViewer = "查看者" +shareHeading = "共享文件" +titleDefault = "共享文件" +tryAgain = "请ç¨åŽå†è¯•。" +addUser = "添加" +commenterHint = "评论功能å³å°†ä¸Šçº¿ã€‚" +copied = "链接已å¤åˆ¶åˆ°å‰ªè´´æ¿" +copy = "å¤åˆ¶" +copyFailed = "å¤åˆ¶å¤±è´¥" +description = "ä¸ºæ­¤æ–‡ä»¶åˆ›å»ºå…±äº«é“¾æŽ¥ã€‚æŒæœ‰é“¾æŽ¥å¹¶å·²ç™»å½•的用户å¯ä»¥è®¿é—®ã€‚" +downloadsCount = "下载次数:{{count}}" +emailWarningBody = "这看起æ¥åƒæ˜¯ä¸€ä¸ªç”µå­é‚®ä»¶åœ°å€ã€‚若此人尚未æˆä¸º Stirling PDF 用户,则无法访问该文件。" +emailWarningConfirm = "ä»è¦å…±äº«" +emailWarningTitle = "电å­é‚®ä»¶åœ°å€" +errorTitle = "共享失败" +failure = "无法生æˆå…±äº«é“¾æŽ¥ã€‚请é‡è¯•。" +fileLabel = "文件" +generate = "生æˆé“¾æŽ¥" +generated = "已生æˆå…±äº«é“¾æŽ¥" +hideActivity = "éšè—活动" +invalidUsername = "è¯·è¾“å…¥æœ‰æ•ˆçš„ç”¨æˆ·åæˆ–电å­é‚®ä»¶åœ°å€ã€‚" +lastAccessed = "上次访问时间" +linkAccessTitle = "共享链接访问" +linkLabel = "共享链接" +linksDisabled = "共享链接已被ç¦ç”¨ã€‚" +linksDisabledBody = "共享链接已在æœåŠ¡å™¨è®¾ç½®ä¸­è¢«ç¦ç”¨ã€‚" +manage = "管ç†å…±äº«" +manageDescription = "创建并管ç†ç”¨äºŽå…±äº«æ­¤æ–‡ä»¶çš„链接。" +manageLoadFailed = "无法加载共享链接。" +manageTitle = "管ç†å…±äº«" +noActivity = "尚无活动。" +noLinks = "尚无活动的共享链接。" +noSharedUsers = "尚无用户具有访问æƒé™ã€‚" +removeLink = "移除链接" +removeUser = "移除" +revokeFailed = "无法移除共享链接。" +revoked = "已移除共享链接" +roleLabel = "角色" +sharingDisabled = "å·²ç¦ç”¨å…±äº«ã€‚" +sharingDisabledBody = "æœåŠ¡å™¨è®¾ç½®å·²ç¦ç”¨å…±äº«ã€‚" +sharedUsersTitle = "共享用户" +title = "共享文件" +unknownUser = "未知用户" +userAddFailed = "无法与该用户共享。" +userAdded = "已将用户添加到共享列表。" +usernameLabel = "ç”¨æˆ·åæˆ–邮箱" +usernamePlaceholder = "è¾“å…¥ç”¨æˆ·åæˆ–邮箱" +userRemoveFailed = "无法移除该用户。" +userRemoved = "已从共享列表中移除用户。" +viewActivity = "查看活动" +viewed = "已查看" +viewsCount = "查看次数: {{count}}" +downloaded = "已下载" +bulkDescription = "为所有选中的文件创建一个链接,与已登录用户共享。" +bulkTitle = "共享所选文件" +copyLink = "å¤åˆ¶å…±äº«é“¾æŽ¥" +fileCount = "已选择 {{count}} 个文件" +ownerOnly = "åªæœ‰æ‰€æœ‰è€…å¯ä»¥ç®¡ç†å…±äº«ã€‚" +selectSingleFile = "选择å•个文件以管ç†å…±äº«ã€‚" + +[storageUpload] +description = "è¿™ä¼šå°†å½“å‰æ–‡ä»¶ä¸Šä¼ åˆ°æœåŠ¡å™¨å­˜å‚¨ï¼Œä¾›æ‚¨è‡ªå·±è®¿é—®ã€‚" +errorTitle = "上传失败" +failure = "上传失败。请检查您的登录和存储设置。" +fileLabel = "文件" +hint = "公共链接和访问模å¼ç”±æ‚¨çš„æœåŠ¡å™¨è®¾ç½®æŽ§åˆ¶ã€‚" +success = "已上传到æœåС噍" +title = "上传到æœåС噍" +updateButton = "在æœåŠ¡å™¨ä¸Šæ›´æ–°" +uploadButton = "上传到æœåС噍" +bulkDescription = "这会将所选文件上传到您的æœåŠ¡å™¨å­˜å‚¨ã€‚" +bulkTitle = "上传所选文件" +fileCount = "已选择 {{count}} 个文件" +more = " +{{count}} 更多" + [storage] approximateSize = "大约大å°" fileTooLarge = "文件过大。å•个文件的最大大å°ä¸º" @@ -7153,6 +7801,30 @@ title = "æµè§ˆ/编辑 PDF" [warning] tooltipTitle = "警告" +[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 = "上传签å图åƒ" + [watermark] completed = "已添加水å°" desc = "å‘ PDF æ·»åŠ æ–‡æœ¬æˆ–å›¾åƒæ°´å°" @@ -7333,6 +8005,7 @@ activeSession = "活动会è¯" addMembers = "添加æˆå‘˜" admin = "管ç†å‘˜" confirmDelete = "确定è¦åˆ é™¤æ­¤ç”¨æˆ·å—?此æ“作无法撤销。" +confirmUnlock = "确定è¦è§£é”此用户账户å—?" deleteUser = "删除用户" deleteUserError = "删除用户失败" deleteUserSuccess = "用户删除æˆåŠŸ" @@ -7341,6 +8014,8 @@ disable = "ç¦ç”¨" disabled = "å·²ç¦ç”¨" editRole = "编辑角色" enable = "å¯ç”¨" +locked = "å·²é”定" +lockedBadge = "å·²é”定" loading = "正在加载æˆå‘˜..." loginRequired = "请先å¯ç”¨ç™»å½•模å¼" member = "æˆå‘˜" @@ -7350,6 +8025,9 @@ searchMembers = "æœç´¢æˆå‘˜..." status = "状æ€" team = "团队" title = "æˆå‘˜" +unlockAccount = "è§£é”账户" +unlockUserError = "è§£é”用户账户失败" +unlockUserSuccess = "用户账户已æˆåŠŸè§£é”" user = "用户" [workspace.people.actions] diff --git a/frontend/public/locales/zh-TW/translation.toml b/frontend/public/locales/zh-TW/translation.toml index 5ff949526c..862b3d5e8a 100644 --- a/frontend/public/locales/zh-TW/translation.toml +++ b/frontend/public/locales/zh-TW/translation.toml @@ -147,7 +147,6 @@ insufficientCredits = "點數ä¸è¶³ã€‚需è¦ï¼š{{requiredCredits}},å¯ç”¨ï¼š{{ loadingCredits = "正在檢查點數..." loadingProStatus = "正在檢查訂閱狀態..." noticeTopUpOrPlan = "點數ä¸è¶³ï¼Œè«‹å„²å€¼æˆ–å‡ç´šæ–¹æ¡ˆ" - accessInvite = "邀請" [account] @@ -1382,12 +1381,18 @@ title = "回饋訊æ¯" [admin.settings.telegram.feedback.channel] title = "é »é“回饋è¦å‰‡" -errorMessage.description = "å‘é »é“顯示詳細錯誤訊æ¯ã€‚" -errorMessage.label = "顯示錯誤訊æ¯ï¼ˆé »é“)" -errorProcessing.description = "將處ç†éŒ¯èª¤è¨Šæ¯å‚³é€è‡³é »é“。" -errorProcessing.label = "顯示處ç†éŒ¯èª¤ï¼ˆé »é“)" -noValidDocument.description = "å°é »é“上傳抑制「沒有有效文件ã€çš„回應。" -noValidDocument.label = "顯示「沒有有效文件ã€ï¼ˆé »é“)" + +[admin.settings.telegram.feedback.channel.errorMessage] +description = "å‘é »é“顯示詳細錯誤訊æ¯ã€‚" +label = "顯示錯誤訊æ¯ï¼ˆé »é“)" + +[admin.settings.telegram.feedback.channel.errorProcessing] +description = "將處ç†éŒ¯èª¤è¨Šæ¯å‚³é€è‡³é »é“。" +label = "顯示處ç†éŒ¯èª¤ï¼ˆé »é“)" + +[admin.settings.telegram.feedback.channel.noValidDocument] +description = "å°é »é“上傳抑制「沒有有效文件ã€çš„回應。" +label = "顯示「沒有有效文件ã€ï¼ˆé »é“)" [admin.settings.telegram.feedback.general.enabled] description = "控制機器人是å¦è¦ç™¼é€å›žé¥‹è¨Šæ¯ã€‚" @@ -1395,12 +1400,18 @@ label = "啟用回饋" [admin.settings.telegram.feedback.user] title = "使用者回饋è¦å‰‡" -errorMessage.description = "å‘使用者顯示詳細錯誤訊æ¯ã€‚" -errorMessage.label = "顯示錯誤訊æ¯ï¼ˆä½¿ç”¨è€…)" -errorProcessing.description = "將處ç†éŒ¯èª¤è¨Šæ¯å‚³é€çµ¦ä½¿ç”¨è€…。" -errorProcessing.label = "顯示處ç†éŒ¯èª¤ï¼ˆä½¿ç”¨è€…)" -noValidDocument.description = "å°ä½¿ç”¨è€…上傳抑制「沒有有效文件ã€çš„回應。" -noValidDocument.label = "顯示「沒有有效文件ã€ï¼ˆä½¿ç”¨è€…)" + +[admin.settings.telegram.feedback.user.errorMessage] +description = "å‘使用者顯示詳細錯誤訊æ¯ã€‚" +label = "顯示錯誤訊æ¯ï¼ˆä½¿ç”¨è€…)" + +[admin.settings.telegram.feedback.user.errorProcessing] +description = "將處ç†éŒ¯èª¤è¨Šæ¯å‚³é€çµ¦ä½¿ç”¨è€…。" +label = "顯示處ç†éŒ¯èª¤ï¼ˆä½¿ç”¨è€…)" + +[admin.settings.telegram.feedback.user.noValidDocument] +description = "å°ä½¿ç”¨è€…上傳抑制「沒有有效文件ã€çš„回應。" +label = "顯示「沒有有效文件ã€ï¼ˆä½¿ç”¨è€…)" [admin.settings.telegram.pipelineInboxFolder] description = "pipeline 目錄下儲存來自 Telegram 之傳入檔案的資料夾。" @@ -2324,14 +2335,6 @@ certInvalidFallback = "憑證無效" certNetworkError = "無法驗證憑證" title = "設定憑證" -[certSign.collab.participant] -certValidating = "正在驗證憑證..." -certValid = "✓ 憑證有效" -certValidUntil = " 有效至 {{date}}" -certInvalid = "✗ {{error}}" -certInvalidFallback = "憑證無效" -certNetworkError = "無法驗證憑證" - [certSign.collab.signRequest.image] hint = "上傳 PNG 或 JPG ç°½å圖片" @@ -2398,6 +2401,14 @@ label = "ç°½åæ–‡å­—" modalHint = "請輸入姓åï¼Œç„¶å¾Œé»žé¸ [繼續] 以放置於 PDF 上。" placeholder = "請輸入姓å..." +[certSign.collab.participant] +certValidating = "正在驗證憑證..." +certValid = "✓ 憑證有效" +certValidUntil = " 有效至 {{date}}" +certInvalid = "✗ {{error}}" +certInvalidFallback = "憑證無效" +certNetworkError = "無法驗證憑證" + [certSign.collab.addParticipants] add = "新增 {{count}} ä½åƒèˆ‡è€…" back = "上一步" @@ -2863,6 +2874,9 @@ title = "å“質調整" [compressPdfs] tags = "壓縮,å°,å¾®å°" +[config] +plan = "方案" + [config.account.overview] confirmDelete = "刪除我的帳號" deleteAccount = "刪除帳號" @@ -2916,9 +2930,6 @@ socialLogin = "使用社群帳號å‡ç´š" title = "å‡ç´šè¨ªå®¢å¸³è™Ÿ" upgradeButton = "å‡ç´šå¸³è™Ÿ" -[config] -plan = "方案" - [config.apiKeys] chartAriaLabel = "é¡åº¦ä½¿ç”¨ï¼šåŒ…å« {{includedUsed}} / {{includedTotal}},已購買 {{purchasedUsed}} / {{purchasedTotal}}" copyKeyAriaLabel = "複製 API 金鑰" @@ -3882,47 +3893,47 @@ sortBy = "æŽ’åºæ–¹å¼ï¼š" [home.addAttachments] desc = "在 PDF 中新增或移除內嵌檔案(附件)" -tags = "內嵌,附加,包å«" +tags = "內嵌,附加,包å«,embed,attach,include,attachments,attach files,embed files,include files,add files,file attachment,associated files,supplementary files" title = "新增附件" [home.addImage] desc = "在 PDF 的指定ä½ç½®æ–°å¢žåœ–片" -tags = "æ’å…¥,內嵌,放置" +tags = "æ’å…¥,內嵌,放置,insert,embed,place,add image,insert image,place image,embed image,add photo,add picture,add logo,graphics,insert picture,place photo,PNG,JPG,JPEG" title = "新增圖片" [home.addPageNumbers] desc = "在文件的設定ä½ç½®æ–°å¢žé ç¢¼" -tags = "編號,é ç¢¼,計數" +tags = "編號,é ç¢¼,計數,number,pagination,count,add page numbers,page numbering,page numbers,footer,header,number pages,sequential,pagination tool" title = "新增é ç¢¼" [home.addPassword] desc = "用密碼加密您的 PDF 檔案。" -tags = "加密,密碼,鎖定,安全,ä¿è­·,安全性,加密,ä¿éšœ,機密,ç§äºº,é™åˆ¶å­˜å–" +tags = "加密,密碼,鎖定,安全,ä¿è­·,安全性,加密,ä¿éšœ,機密,ç§äºº,é™åˆ¶å­˜å–,encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access" title = "新增密碼" [home.addStamp] desc = "在指定ä½ç½®åŠ å…¥æ–‡å­—æˆ–å½±åƒåœ–ç« " -tags = "圖章,標記,å°ç« " +tags = "圖章,標記,å°ç« ,stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original" title = "新增圖章至 PDF" [home.addText] desc = "在 PDF 的任æ„ä½ç½®æ–°å¢žè‡ªè¨‚文字" -tags = "文字,註解,標籤" +tags = "文字,註解,標籤,text,annotation,label,add text,insert text,place text,text box,add label,add caption,type on PDF,write on PDF,add words,add note,text overlay,typewriter" title = "新增文字" [home.adjustContrast] desc = "調整 PDF çš„å°æ¯”度ã€é£½å’Œåº¦å’Œäº®åº¦" -tags = "å°æ¯”,亮度,飽和度" +tags = "å°æ¯”,亮度,飽和度,contrast,brightness,saturation,adjust colors,color correction,enhance,lighten,darken,improve quality,color balance,hue,vibrance" title = "調整é¡è‰²/å°æ¯”度" [home.annotate] desc = "在檢視器中çªé¡¯ã€æ‰‹ç¹ªã€åŠ å…¥è¨»é‡‹èˆ‡å½¢ç‹€" -tags = "註解,螢光標記,繪圖" +tags = "註解,螢光標記,繪圖,annotate,highlight,draw,markup,comment,notes,review,redline,feedback,markup tools,sticky notes,shapes,arrows,text box,freehand" title = "註解" [home.automate] desc = "將多個 PDF 動作串接,建立多步驟工作æµç¨‹ã€‚é©åˆé‡è¤‡æ€§å·¥ä½œã€‚" -tags = "工作æµç¨‹,åºåˆ—,自動化" +tags = "工作æµç¨‹,åºåˆ—,自動化,workflow,sequence,automation,automate,batch,batch processing,pipeline,chain,multi-step,recurring,scheduled,automatic,process multiple,bulk operations" title = "自動化" [home.formFill] @@ -3931,112 +3942,112 @@ title = "填寫表單" [home.autoRename] desc = "ä¾åµæ¸¬åˆ°çš„æ¨™é ­è‡ªå‹•釿–°å‘½å PDF 檔案" -tags = "è‡ªå‹•åµæ¸¬,便¨™é ­,æ•´ç†,釿–°å‘½å" +tags = "è‡ªå‹•åµæ¸¬,便¨™é ­,æ•´ç†,釿–°å‘½å,auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title" title = "è‡ªå‹•é‡æ–°å‘½å PDF 檔案" [home.autoSizeSplitPDF] desc = "根據大å°ã€é æ•¸æˆ–檔案數將單一 PDF 分割為多個檔案" -tags = "自動,分割,大å°" +tags = "自動,分割,大å°,auto,split,size" title = "根據大å°/數é‡è‡ªå‹•分割" [home.autoSplitPDF] desc = "自動分割掃æçš„ PDF,使用實體掃æé é¢åˆ†å‰²å™¨ QR Code" -tags = "自動,分割,QR" +tags = "自動,分割,QR,auto,split,auto split,QR code,QR split,barcode,automatic split,divider page,separator page,scan divider,batch scanning" title = "自動分割é é¢" [home.bookletImposition] desc = "建立é©åˆåˆ—å°èˆ‡è£è¨‚çš„å°å†Šå­é åºèˆ‡å¤šé ç‰ˆé¢" -tags = "å°å†Šå­,列å°,è£è¨‚" +tags = "å°å†Šå­,列å°,è£è¨‚,booklet,print,binding,imposition,booklet printing,saddle stitch,fold,pamphlet,brochure,print booklet,duplex,two-sided,signature,book layout,page imposition,print layout" title = "å°å†Šå­æ‹¼ç‰ˆ" [home.certSign] desc = "使用憑證/金鑰(PEM/P12)簽章 PDF" -tags = "é©—è­‰,PEM,P12,官方,加密,簽署,憑證,PKCS12,JKS,伺æœå™¨,手動,自動" +tags = "é©—è­‰,PEM,P12,官方,加密,簽署,憑證,PKCS12,JKS,伺æœå™¨,手動,自動,authenticate,official,encrypt,sign,certificate,server,manual,auto,digital certificate,certificate signature,PKI,cryptographic signature,trusted signature" title = "使用憑證簽章" [home.changeMetadata] desc = "從 PDF 檔案中變更/移除/新增中繼資料" -tags = "編輯,修改,æ›´æ–°" +tags = "編輯,修改,æ›´æ–°,edit,modify,update,metadata,properties,document properties,author,title,subject,keywords,creator,producer,info,document info,file properties" title = "變更中繼資料" [home.changePermissions] desc = "變更文件é™åˆ¶èˆ‡æ¬Šé™" -tags = "權é™,é™åˆ¶,權利,å­˜å–æŽ§åˆ¶,å…許,拒絕,列å°,複製,編輯,修改權é™,安全設定,使用者權利" +tags = "權é™,é™åˆ¶,權利,å­˜å–æŽ§åˆ¶,å…許,拒絕,列å°,複製,編輯,修改權é™,安全設定,使用者權利,permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights" title = "變更權é™" [home.compare] desc = "比較並顯示 2 個 PDF 檔案的差異" -tags = "差異" +tags = "差異,difference,compare,diff,compare PDFs,compare documents,find differences,show differences,changes,what changed,track changes,revisions,version compare,side by side,contrast,delta" title = "比較" [home.compress] desc = "壓縮 PDF 以減少其檔案大å°ã€‚" -tags = "壓縮,減少,最佳化" +tags = "壓縮,減少,最佳化,shrink,reduce,optimize,compress,smaller,downsize,file size,reduce size,minimize,make smaller,decrease size,optimize size" title = "壓縮" [home.convert] desc = "在ä¸åŒæ ¼å¼ä¹‹é–“è½‰æ›æª”案" -tags = "轉æ›,變更" +tags = "轉æ›,變更,transform,change,convert,PDF to Word,PDF to Excel,PDF to image,Word to PDF,Excel to PDF,PowerPoint to PDF,HTML to PDF,export,import,file conversion,format change,save as" title = "轉æ›" [home.crop] desc = "è£å‰ª PDF 以減少其大å°ï¼ˆä¿æŒæ–‡å­—ï¼ï¼‰" -tags = "è£åˆ‡,剪è£,調整大å°" +tags = "è£åˆ‡,剪è£,調整大å°,trim,cut,resize,crop,crop PDF,trim PDF,trim margins,remove margins,cut edges,trim borders,remove white space,crop pages,trim pages,reduce margins,set margins" title = "è£å‰ª PDF" [home.devAirgapped] desc = "連çµè‡³éš”離網路設定指å—" -tags = "離線,隔離,無網路,æ–·ç·š,安全,網路隔離,ç¨ç«‹é‹ä½œ" +tags = "離線,隔離,無網路,æ–·ç·š,安全,網路隔離,ç¨ç«‹é‹ä½œ,air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone" title = "隔離網路設定" [home.devApi] desc = "連çµè‡³ API 文件" -tags = "API,開發,文件" +tags = "API,開發,文件,development,documentation,developer,REST,integration,endpoints,programmatic,automation,scripting" title = "API" [home.devFolderScanning] desc = "連çµè‡³è‡ªå‹•åŒ–è³‡æ–™å¤¾æŽƒææŒ‡å—" -tags = "自動化,資料夾,掃æ" +tags = "自動化,資料夾,掃æ,automation,folder,scanning,watch folder,hot folder,automatic processing,batch,monitor folder,auto process,folder monitoring" title = "自動化資料夾掃æ" [home.devSsoGuide] desc = "連çµè‡³ SSO 指å—" -tags = "SSO,單一登入,é©—è­‰,SAML,OAuth,OIDC,登入,伿¥­,身分æä¾›è€…,IdP" +tags = "SSO,單一登入,é©—è­‰,SAML,OAuth,OIDC,登入,伿¥­,身分æä¾›è€…,IdP,single sign-on,authentication,login,enterprise,identity provider" title = "SSO 指å—" [home.editTableOfContents] desc = "在 PDF 檔案中新增或編輯書籤和目錄" -tags = "書籤,目錄,編輯" +tags = "書籤,目錄,編輯,bookmarks,contents,edit,table of contents,TOC,outline,navigation,chapters,sections,add bookmarks,edit bookmarks,PDF outline" title = "編輯目錄" [home.extractImages] desc = "從 PDF 中æå–所有圖片並將它們儲存到壓縮檔中" -tags = "æ“·å–,儲存,匯出" +tags = "æ“·å–,儲存,匯出,pull,save,export,extract images,get images,save images,export images,extract photos,extract pictures,pull images,download images,rip images,extract graphics,save photos" title = "æå–圖片" [home.extractPages] desc = "從 PDF 檔案中擷å–特定é é¢" -tags = "æ“·å–,é¸å–,複製" +tags = "æ“·å–,é¸å–,複製,pull,select,copy,extract,extract pages,get pages,pull out,save pages,export pages,copy pages,select pages,specific pages" title = "æå–é é¢" [home.flatten] desc = "從 PDF 中移除所有互動元素和表單" -tags = "簡化,移除,互動" +tags = "簡化,移除,互動,simplify,remove,interactive,flatten,flatten form,remove form fields,make static,finalize form,lock form,disable editing,convert to image,non-editable" title = "å¹³å¦åŒ–" [home.getPdfInfo] desc = "å–å¾— PDF 的所有å¯èƒ½è³‡è¨Š" -tags = "資訊,中繼資料,詳細" +tags = "資訊,中繼資料,詳細,info,metadata,details,PDF info,document info,properties,file info,get info,show info,view properties,document properties,statistics,page count,file details,inspect" title = "å–å¾— PDF 的所有資訊" [home.manageCertificates] desc = "匯入ã€åŒ¯å‡ºæˆ–刪除用於簽署 PDF çš„æ•¸ä½æ†‘證檔。" -tags = "憑證,匯入,匯出" +tags = "憑證,匯入,匯出,certificates,import,export,manage certificates,digital certificates,certificate management,PFX,P12,keystore,import certificate,export certificate,certificate store,PKI" title = "ç®¡ç†æ†‘è­‰" [home.merge] desc = "輕鬆將多個 PDF åˆä½µç‚ºä¸€å€‹ã€‚" -tags = "åˆä½µ,連接,æ•´åˆ" +tags = "åˆä½µ,連接,æ•´åˆ,combine,join,unite,merge,merge PDFs,combine PDFs,join PDFs,concatenate,append,stitch,combine files,join files,merge documents" title = "åˆä½µ" [home.mobile] @@ -4051,162 +4062,162 @@ workspace = "工作å€" [home.multiTool] desc = "åˆä½µã€æ—‹è½‰ã€é‡æ–°æŽ’列和移除é é¢" -tags = "多個,工具" +tags = "多個,工具,multiple,tools,multi-tool,all-in-one,swiss army,page organizer,page editor,edit pages,manage pages,organize,reorganize" title = "PDF 複åˆå·¥å…·" [home.ocr] desc = "æ¸…ç†æŽƒæä¸¦å¾ž PDF 中的影åƒä¸­åµæ¸¬æ–‡å­—䏦釿–°æ–°å¢žç‚ºæ–‡å­—。" -tags = "æ“·å–,掃æ" +tags = "æ“·å–,掃æ,extract,scan,OCR,optical character recognition,text recognition,scan to text,image to text,scanned document,searchable PDF,make searchable,extract text,recognize text,read scanned" title = "OCR / æ¸…ç†æŽƒæ" [home.overlay-pdfs] desc = "å°‡ PDF 覆蓋在å¦ä¸€å€‹ PDF 上" -tags = "疊加,åˆä½µ,圖層,é‡ç–Š,疊加 PDF,圖層 PDF,åˆä½µ PDF,堆疊,疊加é é¢,背景,剿™¯,åˆæˆ" +tags = "疊加,åˆä½µ,圖層,é‡ç–Š,疊加 PDF,圖層 PDF,åˆä½µ PDF,堆疊,疊加é é¢,背景,剿™¯,åˆæˆ,overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite" title = "覆蓋 PDF" [home.pageLayout] desc = "å°‡ PDF 檔案的多個é é¢åˆä½µåˆ°å–®ä¸€é é¢" -tags = "版é¢,排列,組åˆ" +tags = "版é¢,排列,組åˆ,layout,arrange,combine,N-up,2-up,4-up,multiple per page,pages per sheet,layout pages,tile,grid layout,multi-page layout,combine on page,handout" title = "多é ç‰ˆé¢é…ç½®" [home.pdfOrganiser] desc = "以任何順åºç§»é™¤/釿–°æŽ’列é é¢" -tags = "æ•´ç†,釿–°æŽ’列,釿–°æŽ’åº" +tags = "æ•´ç†,釿–°æŽ’列,釿–°æŽ’åº,organize,rearrange,reorder,organise,arrange pages,sort,move pages,delete pages,remove pages,page management,page organizer,page organiser,resequence" title = "æ•´ç†" [home.pdfTextEditor] desc = "檢視與編輯 Stirling PDF çš„ JSON 匯出,支æ´ç¾¤çµ„æ–‡å­—ç·¨è¼¯èˆ‡é‡æ–°ç”¢ç”Ÿ PDF" -tags = "編輯文字,修改文字,變更文字,編輯內容,更新文字,改寫,æ ¡æ­£,修訂,文字編輯器,內容編輯器" +tags = "編輯文字,修改文字,變更文字,編輯內容,更新文字,改寫,æ ¡æ­£,修訂,文字編輯器,內容編輯器,edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor" title = "PDF 文字編輯器" [home.pdfToSinglePage] desc = "將所有 PDF é é¢åˆä½µç‚ºä¸€å€‹å¤§çš„單一é é¢" -tags = "åˆä½µ,æ•´åˆ,å–®é " +tags = "åˆä½µ,æ•´åˆ,å–®é ,combine,merge,single,single page,one page,merge to single,combine all,stitch pages,concatenate vertical,long page,poster" title = "PDF 轉單一大é é¢" [home.read] desc = "檢視並註解 PDFs。å¯å白文字ã€ç¹ªåœ–或æ’入評論以供審閱與å”作。" -tags = "檢視,開啟,顯示" +tags = "檢視,開啟,顯示,view,open,display,read,viewer,PDF viewer,PDF reader,open PDF,view PDF,display PDF,preview,browse" title = "閱讀" [home.redact] desc = "便“šé¸å–的文字ã€ç¹ªè£½çš„形狀和é¸å–çš„é é¢å¡—黑 PDF" -tags = "é®è”½,塗黑,éš±è—" +tags = "é®è”½,塗黑,éš±è—,censor,blackout,hide,redact,redaction,black out,block out,remove sensitive,hide text,privacy,confidential,GDPR,PII,sensitive data,permanently remove,cover up,legal redaction" title = "手動塗黑" [home.removeAnnotations] desc = "從 PDF 中移除所有註釋/註解" -tags = "刪除,清ç†,去除" +tags = "刪除,清ç†,去除,delete,clean,strip,remove annotations,remove comments,delete comments,remove markup,remove highlights,clean annotations,strip comments,remove notes,delete markup,clear comments" title = "移除註釋" [home.removeBlanks] desc = "嵿¸¬ä¸¦å¾žæ–‡ä»¶ä¸­ç§»é™¤ç©ºç™½é é¢" -tags = "刪除,清ç†,空白" +tags = "刪除,清ç†,空白,delete,clean,empty,remove blank,delete blank pages,empty pages,white pages,remove empty,clean up,cleanup blank" title = "移除空白é é¢" [home.removeCertSign] desc = "從 PDF 移除簽章" -tags = "移除,刪除,解鎖" +tags = "移除,刪除,解鎖,remove,delete,unlock,remove certificate,remove signature,delete signature,unsigned,remove digital signature,strip signature,remove cert,unsign" title = "移除簽章" [home.removeImage] desc = "從 PDF 中移除圖片以減少檔案大å°" -tags = "移除,刪除,清ç†" +tags = "移除,刪除,清ç†,remove,delete,clean,remove image,delete image,strip images,remove pictures,delete photos,clean images,reduce size,remove graphics" title = "移除圖片" [home.removePages] desc = "從您的 PDF 檔案中刪除ä¸éœ€è¦çš„é é¢ã€‚" -tags = "刪除,æ“·å–,排除" +tags = "刪除,æ“·å–,排除,delete,extract,exclude,remove pages,delete pages,remove page,delete page,exclude pages,take out pages,discard pages,drop pages" title = "移除" [home.removePassword] desc = "從您的 PDF 檔案中移除密碼ä¿è­·ã€‚" -tags = "解鎖" +tags = "解鎖,unlock,remove password,unlock PDF,decrypt,remove encryption,unprotect,open protected PDF,password removal,unlock protected,disable password,remove security,remove owner password" title = "移除密碼" [home.reorganizePages] desc = "é€éŽè¦–è¦ºåŒ–æ‹–æ”¾æŽ§åˆ¶ï¼Œé‡æ–°æŽ’列ã€è¤‡è£½æˆ–刪除 PDF é é¢ã€‚" -tags = "釿–°æŽ’列,釿–°æŽ’åº,æ•´ç†" +tags = "釿–°æŽ’列,釿–°æŽ’åº,æ•´ç†,rearrange,reorder,organize,reorganize,move pages,page order,sort pages,arrange pages,shuffle,resequence" title = "é‡çµ„é é¢" [home.repair] desc = "嘗試修復æå£ž/ç ´æçš„ PDF" -tags = "修復,還原" +tags = "修復,還原,fix,restore,repair,fix PDF,fix broken,fix corrupt,repair PDF,repair corrupt,broken PDF,corrupt PDF,damaged PDF,recover,fix errors,PDF won't open,can't open PDF,PDF errors,troubleshoot,restore PDF,rebuild,corrupted" title = "修復" [home.replaceColor] desc = "在 PDF 檔案中å–代或å轉é¡è‰²" -tags = "å–代é¡è‰²,å轉é¡è‰²,é¡è‰²å–代,交æ›é¡è‰²,變更é¡è‰²,å轉,負片,é¡è‰²äº¤æ›,尋找並å–代é¡è‰²,轉æ›é¡è‰²,é¡è‰²è®Šæ›´" +tags = "å–代é¡è‰²,å轉é¡è‰²,é¡è‰²å–代,交æ›é¡è‰²,變更é¡è‰²,å轉,負片,é¡è‰²äº¤æ›,尋找並å–代é¡è‰²,轉æ›é¡è‰²,é¡è‰²è®Šæ›´,replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change" title = "å–代與å轉é¡è‰²" [home.rotate] desc = "輕鬆旋轉您的 PDF。" -tags = "旋轉,翻轉,調整方å‘" +tags = "旋轉,翻轉,調整方å‘,turn,flip,orient,rotate,orientation,landscape,portrait,90 degrees,180 degrees,clockwise,anticlockwise,counter-clockwise,fix orientation" title = "旋轉" [home.sanitize] desc = "移除 PDF 中å¯èƒ½æœ‰å®³çš„元素" -tags = "清ç†,清除,移除" +tags = "清ç†,清除,移除,clean,purge,remove,sanitize,sanitise,remove scripts,remove javascript,remove metadata,strip metadata,security,clean document,remove hidden data,privacy" title = "淨化" [home.scalePages] desc = "修改é é¢åŠå…¶å…§å®¹çš„大å°/比例。" -tags = "調整大å°,調整,縮放" +tags = "調整大å°,調整,縮放,resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size" title = "調整é é¢å¤§å°/比例" [home.scannerEffect] desc = "å»ºç«‹çœ‹èµ·ä¾†åƒæ˜¯æŽƒæéŽçš„ PDF" -tags = "掃æ,模擬,建立" +tags = "掃æ,模擬,建立,scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan" title = "掃æå™¨æ•ˆæžœ" [home.scannerImageSplit] desc = "嵿¸¬ä¸¦å°‡æŽƒæçš„照片分割為ç¨ç«‹é é¢" -tags = "嵿¸¬,分割,照片" +tags = "嵿¸¬,分割,照片,detect,split,photos,auto detect,detect photos,split photos,separate photos,split scanned images,multiple photos,auto split,photo detection,image detection,scan separation" title = "嵿¸¬ä¸¦åˆ†å‰²æŽƒæç…§ç‰‡" [home.showJS] desc = "æœå°‹ä¸¦é¡¯ç¤ºåµŒå…¥ PDF 中的任何 JS(JavaScript)" -tags = "JavaScript,程å¼ç¢¼,指令碼" +tags = "JavaScript,程å¼ç¢¼,指令碼,javascript,code,script,show javascript,show JS,find javascript,detect javascript,view javascript,embedded scripts,malware,security,inspect,debug" title = "顯示 JavaScript" [home.sign] desc = "é€éŽç¹ªåœ–ã€æ–‡å­—æˆ–å½±åƒæ–°å¢žç°½ç« åˆ° PDF" -tags = "ç°½å,ç½²å" +tags = "ç°½å,ç½²å,signature,autograph,e-sign,electronic signature,digital signature,sign document,approval,signoff,authorize,endorse,ink signature,handwriting" title = "簽章" [home.timestampPdf] desc = "新增 RFC 3161 文件時間戳記,以證明您的 PDF 於何時存在" -tags = "時間戳記,RFC 3161,TSA,時間戳記授權機構,文件時間戳記,存在證明,時間戳記權æ–,å¯ä¿¡æ™‚間戳記,簽署時間戳記,公證" +tags = "時間戳記,RFC 3161,TSA,時間戳記授權機構,文件時間戳記,存在證明,時間戳記權æ–,å¯ä¿¡æ™‚間戳記,簽署時間戳記,公證,timestamp,time stamp authority,document timestamp,proof of existence,timestamp token,trusted timestamp,sign timestamp,notarise" title = "PDF 加上時間戳記" [home.split] desc = "å°‡ PDF 分割為多個檔案" -tags = "分割,分開,拆分" +tags = "分割,分開,拆分,divide,separate,break,split,extract pages,separate pages,divide document,break apart,separate files,unbind,split by page,divide by chapter" title = "分割" [home.splitByChapters] desc = "根據 PDF çš„ç« ç¯€çµæ§‹å°‡å…¶åˆ†å‰²æˆå¤šå€‹æª”案。" -tags = "分割,章節,çµæ§‹" +tags = "分割,章節,çµæ§‹,split,chapters,structure,split by chapters,split by bookmarks,bookmarks,outline,table of contents,TOC split,chapter split,divide by sections" title = "ä¾ç« ç¯€åˆ†å‰² PDF" [home.splitBySections] desc = "å°‡ PDF çš„æ¯é åˆ†æˆè¼ƒå°çš„æ°´å¹³èˆ‡åž‚ç›´å€å¡Š" -tags = "分割,å€å¡Š,切分" +tags = "分割,å€å¡Š,切分,split,sections,divide,split by sections,grid split,divide pages,split into sections,cut pages,divide grid,section split,horizontal split,vertical split" title = "ä¾å€å¡Šåˆ†å‰² PDF" [home.swagger] desc = "檢視 API 文件並測試端點" -tags = "API,文件,測試" +tags = "API,文件,測試,documentation,test,swagger,API docs,REST API,endpoints,developer,API reference,API testing,OpenAPI,integration,developer docs" title = "API 文件" [home.unlockPDFForms] desc = "移除 PDF 檔案中表單欄ä½çš„唯讀屬性" -tags = "解鎖,啟用,編輯" +tags = "解鎖,啟用,編輯,unlock,enable,edit,unlock forms,enable forms,editable forms,remove read only,make editable,unlock fields,enable editing,form fields,fillable,unprotect forms" title = "解鎖 PDF 表單" [home.validateSignature] desc = "é©—è­‰ PDF 檔案中的數ä½ç°½ç« èˆ‡æ†‘è­‰" -tags = "é©—è­‰,æ ¡é©—,憑證" +tags = "é©—è­‰,æ ¡é©—,憑證,validate,verify,certificate,validate signature,verify signature,check signature,digital signature,certificate verification,signature validation,authentic,trust,signed,verify certificate" title = "é©—è­‰ PDF 簽章" [home.viewPdf] @@ -4215,7 +4226,7 @@ title = "檢視/編輯 PDF" [home.watermark] desc = "在您的 PDF 檔案中新增自訂浮水å°ã€‚" -tags = "圖章,標記,覆蓋" +tags = "圖章,標記,覆蓋,stamp,mark,overlay,watermark,branding,logo,confidential,draft,copyright,trademark,text overlay,image overlay,background text" title = "新增浮水å°" [HTMLToPDF] @@ -4879,6 +4890,49 @@ textStats = "{{lines}} 行 · {{size}}" lineNumbers = "行號" renderMarkdown = "轉譯 Markdown" +[viewer.attachments] +title = "附件" +searchPlaceholder = "æœå°‹é™„ä»¶" +noSupport = "æ­¤æª¢è¦–å™¨ä¸æ”¯æ´é™„件。" +noDocument = "開啟 PDF 以檢視其附件。" +loading = "正在載入附件..." +empty = "此文件沒有附件" +noMatch = "æ²’æœ‰ç¬¦åˆæœå°‹çš„附件" + +[viewer.comments] +title = "è©•è«–" +hint = "使用「評論ã€ã€ã€Œæ’å…¥æ–‡å­—ã€æˆ–「å–代文字ã€å·¥å…·æ”¾ç½®è©•論。它們會ä¾é é¢é¡¯ç¤ºæ–¼æ­¤è™•。" +placeholder = "輸入您的評論..." +pageLabel = "第 {{page}} é " +oneComment = "1 則評論" +nComments = "{{count}} 則評論" +addCommentPlaceholder = "新增評論..." +addLink = "新增連çµ" +goToLink = "å‰å¾€é€£çµ" +addComment = "新增評論" +viewComment = "檢視評論" +addReplyPlaceholder = "新增回覆..." +saveReply = "儲存回覆" +send = "傳é€" +moreActions = "更多動作" +typeComment = "è©•è«–" +typeInsertText = "æ’入文字" +typeReplaceText = "å–代文字" +locateAnnotation = "在文件中定ä½" +deleteTitle = "è¦å¾žè©•論中移除註解嗎?" +deleteDescription = "此註解附有評論。您å¯ä»¥åƒ…從å´é‚Šæ¬„移除評論並ä¿ç•™è¨»è§£ï¼Œæˆ–刪除全部內容。" +removeCommentOnly = "僅移除評論" +deleteAnnotationAndComment = "刪除註解與評論" + +[viewer.formBar] +title = "表單欄ä½" +unsavedBadge = "未儲存" +unsavedDesc = "您有未儲存的變更" +hasFieldsDesc = "æ­¤ PDF 包å«å¯å¡«å¯«æ¬„ä½" +dismiss = "關閉" +apply = "套用變更" +download = "下載 PDF" + [rightRail] closeSelected = "é—œé–‰å·²é¸æª”案" selectAll = "å…¨é¸" @@ -5763,7 +5817,6 @@ messageWithAmount = "執行 {{tool}} éœ€è¦ {{required}} 點,但您僅有 {{cu teamMember = "啟用超é‡è¨ˆè²»ï¼Œæ°¸ä¸è€—盡點數。" title = "點數ä¸è¶³" - [printFile] header = "使用å°è¡¨æ©Ÿå°å‡ºæª”案" submit = "列å°" @@ -6457,8 +6510,6 @@ placeholder = "輸入æœå°‹è©ž..." searching = "æœå°‹ä¸­..." title = "æœå°‹ PDF" -[selfHosted] - [selfHosted.offline] hideTools = "éš±è—無法使用的工具 â–´" messageNoFallback = "在您的伺æœå™¨æ¢å¾©é€£ç·šå‰ï¼Œé€™äº›å·¥å…·ç„¡æ³•使用。" @@ -6817,10 +6868,13 @@ title = "登入 Stirling" [setup.selfhosted] link = "或連線到自行託管的帳號" subtitle = "輸入您的伺æœå™¨èªè­‰è³‡è¨Š" +changeServerLocked = "您的組織已將此應用程å¼é™åˆ¶æ–¼ç‰¹å®šä¼ºæœå™¨" switchToLocal = "改用本機工具" title = "登入伺æœå™¨" [setup.selfhosted.unreachable] +changeServer = "連線至其他伺æœå™¨" +changeServerLocked = "您的組織已將此應用程å¼é™åˆ¶æ–¼ç‰¹å®šä¼ºæœå™¨" continueOffline = "改用本機工具" message = "無法連線至 {{url}}。請確èªä¼ºæœå™¨æ­£åœ¨åŸ·è¡Œä¸”å¯å­˜å–。" retry = "é‡è©¦" @@ -7739,49 +7793,6 @@ fileManager = "檔案管ç†å™¨" pageEditor = "é é¢ç·¨è¼¯å™¨" viewer = "檢視器" -[viewer.attachments] -title = "附件" -searchPlaceholder = "æœå°‹é™„ä»¶" -noSupport = "æ­¤æª¢è¦–å™¨ä¸æ”¯æ´é™„件。" -noDocument = "開啟 PDF 以檢視其附件。" -loading = "正在載入附件..." -empty = "此文件沒有附件" -noMatch = "æ²’æœ‰ç¬¦åˆæœå°‹çš„附件" - -[viewer.comments] -title = "è©•è«–" -hint = "使用「評論ã€ã€ã€Œæ’å…¥æ–‡å­—ã€æˆ–「å–代文字ã€å·¥å…·æ”¾ç½®è©•論。它們會ä¾é é¢é¡¯ç¤ºæ–¼æ­¤è™•。" -placeholder = "輸入您的評論..." -pageLabel = "第 {{page}} é " -oneComment = "1 則評論" -nComments = "{{count}} 則評論" -addCommentPlaceholder = "新增評論..." -addLink = "新增連çµ" -goToLink = "å‰å¾€é€£çµ" -addComment = "新增評論" -viewComment = "檢視評論" -addReplyPlaceholder = "新增回覆..." -saveReply = "儲存回覆" -send = "傳é€" -moreActions = "更多動作" -typeComment = "è©•è«–" -typeInsertText = "æ’入文字" -typeReplaceText = "å–代文字" -locateAnnotation = "在文件中定ä½" -deleteTitle = "è¦å¾žè©•論中移除註解嗎?" -deleteDescription = "此註解附有評論。您å¯ä»¥åƒ…從å´é‚Šæ¬„移除評論並ä¿ç•™è¨»è§£ï¼Œæˆ–刪除全部內容。" -removeCommentOnly = "僅移除評論" -deleteAnnotationAndComment = "刪除註解與評論" - -[viewer.formBar] -title = "表單欄ä½" -unsavedBadge = "未儲存" -unsavedDesc = "您有未儲存的變更" -hasFieldsDesc = "æ­¤ PDF 包å«å¯å¡«å¯«æ¬„ä½" -dismiss = "關閉" -apply = "套用變更" -download = "下載 PDF" - [viewPdf] header = "檢視 PDF" tags = "檢視,閱讀,註釋,文字,圖片" @@ -8255,7 +8266,6 @@ title = "大型 ZIP 檔案" [cloudBadge] tooltip = "æ­¤æ“作會使用您的雲端點數" - [team] cancelInviteError = "å–æ¶ˆé‚€è«‹å¤±æ•—" confirmCancelInvite = "確定è¦å–消此邀請嗎?" diff --git a/frontend/public/manifest-classic.json b/frontend/public/manifest-classic.json index 9b47da7d05..d6e81e7ddf 100644 --- a/frontend/public/manifest-classic.json +++ b/frontend/public/manifest-classic.json @@ -23,4 +23,3 @@ "theme_color": "#000000", "background_color": "#ffffff" } - diff --git a/frontend/scripts/build-provisioner.mjs b/frontend/scripts/build-provisioner.mjs index 2f974a195f..8cd5c5cfc6 100644 --- a/frontend/scripts/build-provisioner.mjs +++ b/frontend/scripts/build-provisioner.mjs @@ -1,28 +1,56 @@ -import { execFileSync } from 'node:child_process'; -import { existsSync, mkdirSync, copyFileSync } from 'node:fs'; -import { join, resolve } from 'node:path'; +import { execFileSync } from "node:child_process"; +import { existsSync, mkdirSync, copyFileSync } from "node:fs"; +import { join, resolve } from "node:path"; -if (process.platform !== 'win32') { +if (process.platform !== "win32") { process.exit(0); } const frontendDir = process.cwd(); -const tauriDir = resolve(frontendDir, 'src-tauri'); -const provisionerManifest = join(tauriDir, 'provisioner', 'Cargo.toml'); +const tauriDir = resolve(frontendDir, "src-tauri"); +const provisionerManifest = join(tauriDir, "provisioner", "Cargo.toml"); execFileSync( - 'cargo', - ['build', '--release', '--manifest-path', provisionerManifest], - { stdio: 'inherit' } + "cargo", + ["build", "--release", "--manifest-path", provisionerManifest], + { stdio: "inherit" }, ); -const provisionerExe = join(tauriDir, 'provisioner', 'target', 'release', 'stirling-provisioner.exe'); +const provisionerExe = join( + tauriDir, + "provisioner", + "target", + "release", + "stirling-provisioner.exe", +); if (!existsSync(provisionerExe)) { throw new Error(`Provisioner binary not found at ${provisionerExe}`); } -const wixDir = join(tauriDir, 'windows', 'wix'); +const wixDir = join(tauriDir, "windows", "wix"); mkdirSync(wixDir, { recursive: true }); -const destExe = join(wixDir, 'stirling-provision.exe'); +const destExe = join(wixDir, "stirling-provision.exe"); copyFileSync(provisionerExe, destExe); + +// --- Thumbnail handler DLL --- +const thumbManifest = join(tauriDir, "thumbnail-handler", "Cargo.toml"); + +execFileSync( + "cargo", + ["build", "--release", "--manifest-path", thumbManifest], + { stdio: "inherit" }, +); + +const thumbDll = join( + tauriDir, + "thumbnail-handler", + "target", + "release", + "stirling_thumbnail_handler.dll", +); +if (!existsSync(thumbDll)) { + throw new Error(`Thumbnail handler DLL not found at ${thumbDll}`); +} + +copyFileSync(thumbDll, join(wixDir, "stirling_thumbnail_handler.dll")); diff --git a/frontend/scripts/generate-icons.js b/frontend/scripts/generate-icons.js index 96566341c6..fb9ecd71ec 100644 --- a/frontend/scripts/generate-icons.js +++ b/frontend/scripts/generate-icons.js @@ -1,11 +1,12 @@ #!/usr/bin/env node -const { icons } = require('@iconify-json/material-symbols'); -const fs = require('fs'); -const path = require('path'); +const { icons } = require("@iconify-json/material-symbols"); +const fs = require("fs"); +const path = require("path"); // Check for verbose flag -const isVerbose = process.argv.includes('--verbose') || process.argv.includes('-v'); +const isVerbose = + process.argv.includes("--verbose") || process.argv.includes("-v"); // Logging functions const info = (message) => console.log(message); @@ -18,12 +19,12 @@ const debug = (message) => { // Function to scan codebase for LocalIcon usage function scanForUsedIcons() { const usedIcons = new Set(); - const srcDir = path.join(__dirname, '..', 'src'); + const srcDir = path.join(__dirname, "..", "src"); - info('🔠Scanning codebase for LocalIcon usage...'); + info("🔠Scanning codebase for LocalIcon usage..."); if (!fs.existsSync(srcDir)) { - console.error('⌠Source directory not found:', srcDir); + console.error("⌠Source directory not found:", srcDir); process.exit(1); } @@ -31,72 +32,92 @@ function scanForUsedIcons() { function scanDirectory(dir) { const files = fs.readdirSync(dir); - files.forEach(file => { + files.forEach((file) => { const filePath = path.join(dir, file); const stat = fs.statSync(filePath); if (stat.isDirectory()) { scanDirectory(filePath); - } else if (file.endsWith('.tsx') || file.endsWith('.ts')) { - const content = fs.readFileSync(filePath, 'utf8'); + } else if (file.endsWith(".tsx") || file.endsWith(".ts")) { + const content = fs.readFileSync(filePath, "utf8"); // Match LocalIcon usage: - const localIconMatches = content.match(/]*icon="([^"]+)"/g); + const localIconMatches = content.match( + /]*icon="([^"]+)"/g, + ); if (localIconMatches) { - localIconMatches.forEach(match => { + localIconMatches.forEach((match) => { const iconMatch = match.match(/icon="([^"]+)"/); if (iconMatch) { usedIcons.add(iconMatch[1]); - debug(` Found: ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`); + debug( + ` Found: ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`, + ); } }); } // Match LocalIcon usage: - const localIconSingleQuoteMatches = content.match(/]*icon='([^']+)'/g); + const localIconSingleQuoteMatches = content.match( + /]*icon='([^']+)'/g, + ); if (localIconSingleQuoteMatches) { - localIconSingleQuoteMatches.forEach(match => { + localIconSingleQuoteMatches.forEach((match) => { const iconMatch = match.match(/icon='([^']+)'/); if (iconMatch) { usedIcons.add(iconMatch[1]); - debug(` Found: ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`); + debug( + ` Found: ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`, + ); } }); } // Match old material-symbols-rounded spans: icon-name - const spanMatches = content.match(/]*className="[^"]*material-symbols-rounded[^"]*"[^>]*>([^<]+)<\/span>/g); + const spanMatches = content.match( + /]*className="[^"]*material-symbols-rounded[^"]*"[^>]*>([^<]+)<\/span>/g, + ); if (spanMatches) { - spanMatches.forEach(match => { + spanMatches.forEach((match) => { const iconMatch = match.match(/>([^<]+)<\/span>/); if (iconMatch && iconMatch[1].trim()) { const iconName = iconMatch[1].trim(); usedIcons.add(iconName); - debug(` Found (legacy): ${iconName} in ${path.relative(srcDir, filePath)}`); + debug( + ` Found (legacy): ${iconName} in ${path.relative(srcDir, filePath)}`, + ); } }); } // Match Icon component usage: - const iconMatches = content.match(/]*icon="material-symbols:([^"]+)"/g); + const iconMatches = content.match( + /]*icon="material-symbols:([^"]+)"/g, + ); if (iconMatches) { - iconMatches.forEach(match => { + iconMatches.forEach((match) => { const iconMatch = match.match(/icon="material-symbols:([^"]+)"/); if (iconMatch) { usedIcons.add(iconMatch[1]); - debug(` Found (Icon): ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`); + debug( + ` Found (Icon): ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`, + ); } }); } // Match icon config usage: icon: 'icon-name' or icon: "icon-name" - const iconPropertyMatches = content.match(/icon:\s*(['"])([a-z0-9-]+)\1/g); + const iconPropertyMatches = content.match( + /icon:\s*(['"])([a-z0-9-]+)\1/g, + ); if (iconPropertyMatches) { - iconPropertyMatches.forEach(match => { + iconPropertyMatches.forEach((match) => { const iconMatch = match.match(/icon:\s*(['"])([a-z0-9-]+)\1/); if (iconMatch) { usedIcons.add(iconMatch[2]); - debug(` Found (config): ${iconMatch[2]} in ${path.relative(srcDir, filePath)}`); + debug( + ` Found (config): ${iconMatch[2]} in ${path.relative(srcDir, filePath)}`, + ); } }); } @@ -118,18 +139,26 @@ async function main() { const usedIcons = scanForUsedIcons(); // Check if we need to regenerate (compare with existing) - const outputPath = path.join(__dirname, '..', 'src', 'assets', 'material-symbols-icons.json'); + const outputPath = path.join( + __dirname, + "..", + "src", + "assets", + "material-symbols-icons.json", + ); let needsRegeneration = true; if (fs.existsSync(outputPath)) { try { - const existingSet = JSON.parse(fs.readFileSync(outputPath, 'utf8')); + const existingSet = JSON.parse(fs.readFileSync(outputPath, "utf8")); const existingIcons = Object.keys(existingSet.icons || {}).sort(); const currentIcons = [...usedIcons].sort(); if (JSON.stringify(existingIcons) === JSON.stringify(currentIcons)) { needsRegeneration = false; - info(`✅ Icon set already up-to-date (${usedIcons.length} icons, ${Math.round(fs.statSync(outputPath).size / 1024)}KB)`); + info( + `✅ Icon set already up-to-date (${usedIcons.length} icons, ${Math.round(fs.statSync(outputPath).size / 1024)}KB)`, + ); } } catch { // If we can't parse existing file, regenerate @@ -138,34 +167,40 @@ async function main() { } if (!needsRegeneration) { - info('🎉 No regeneration needed!'); + info("🎉 No regeneration needed!"); process.exit(0); } info(`🔠Extracting ${usedIcons.length} icons from Material Symbols...`); // Dynamic import of ES module - const { getIcons } = await import('@iconify/utils'); + const { getIcons } = await import("@iconify/utils"); // Extract only our used icons from the full set const extractedIcons = getIcons(icons, usedIcons); if (!extractedIcons) { - console.error('⌠Failed to extract icons'); + console.error("⌠Failed to extract icons"); process.exit(1); } // Check for missing icons const extractedIconNames = Object.keys(extractedIcons.icons || {}); - const missingIcons = usedIcons.filter(icon => !extractedIconNames.includes(icon)); + const missingIcons = usedIcons.filter( + (icon) => !extractedIconNames.includes(icon), + ); if (missingIcons.length > 0) { - info(`âš ï¸ Missing icons (${missingIcons.length}): ${missingIcons.join(', ')}`); - info('💡 These icons don\'t exist in Material Symbols. Please use available alternatives.'); + info( + `âš ï¸ Missing icons (${missingIcons.length}): ${missingIcons.join(", ")}`, + ); + info( + "💡 These icons don't exist in Material Symbols. Please use available alternatives.", + ); } // Create output directory - const outputDir = path.join(__dirname, '..', 'src', 'assets'); + const outputDir = path.join(__dirname, "..", "src", "assets"); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } @@ -173,8 +208,12 @@ async function main() { // Write the extracted icon set to a file (outputPath already defined above) fs.writeFileSync(outputPath, JSON.stringify(extractedIcons, null, 2)); - info(`✅ Successfully extracted ${Object.keys(extractedIcons.icons || {}).length} icons`); - info(`📦 Bundle size: ${Math.round(JSON.stringify(extractedIcons).length / 1024)}KB`); + info( + `✅ Successfully extracted ${Object.keys(extractedIcons.icons || {}).length} icons`, + ); + info( + `📦 Bundle size: ${Math.round(JSON.stringify(extractedIcons).length / 1024)}KB`, + ); info(`💾 Saved to: ${outputPath}`); // Generate TypeScript types @@ -182,7 +221,7 @@ async function main() { // This file is automatically generated by scripts/generate-icons.js // Do not edit manually - changes will be overwritten -export type MaterialSymbolIcon = ${usedIcons.map(icon => `'${icon}'`).join(' | ')}; +export type MaterialSymbolIcon = ${usedIcons.map((icon) => `'${icon}'`).join(" | ")}; export interface IconSet { prefix: string; @@ -196,7 +235,7 @@ declare const iconSet: IconSet; export default iconSet; `; - const typesPath = path.join(outputDir, 'material-symbols-icons.d.ts'); + const typesPath = path.join(outputDir, "material-symbols-icons.d.ts"); fs.writeFileSync(typesPath, typesContent); info(`📠Generated types: ${typesPath}`); @@ -204,7 +243,7 @@ export default iconSet; } // Run the main function -main().catch(error => { - console.error('⌠Script failed:', error); +main().catch((error) => { + console.error("⌠Script failed:", error); process.exit(1); }); diff --git a/frontend/scripts/generate-licenses.js b/frontend/scripts/generate-licenses.js index e4b40c0e42..bfeb848550 100644 --- a/frontend/scripts/generate-licenses.js +++ b/frontend/scripts/generate-licenses.js @@ -1,11 +1,16 @@ #!/usr/bin/env node -const { execSync } = require('node:child_process'); -const { existsSync, mkdirSync, writeFileSync, readFileSync } = require('node:fs'); -const path = require('node:path'); +const { execSync } = require("node:child_process"); +const { + existsSync, + mkdirSync, + writeFileSync, + readFileSync, +} = require("node:fs"); +const path = require("node:path"); -const { argv } = require('node:process'); -const inputIdx = argv.indexOf('--input'); +const { argv } = require("node:process"); +const inputIdx = argv.indexOf("--input"); const INPUT_FILE = inputIdx > -1 ? argv[inputIdx + 1] : null; const POSTPROCESS_ONLY = !!INPUT_FILE; @@ -16,408 +21,498 @@ const POSTPROCESS_ONLY = !!INPUT_FILE; * This script creates a JSON file similar to the Java backend's 3rdPartyLicenses.json */ -const OUTPUT_FILE = path.join(__dirname, '..', 'src', 'assets', '3rdPartyLicenses.json'); -const PACKAGE_JSON = path.join(__dirname, '..', 'package.json'); +const OUTPUT_FILE = path.join( + __dirname, + "..", + "src", + "assets", + "3rdPartyLicenses.json", +); +const PACKAGE_JSON = path.join(__dirname, "..", "package.json"); // Ensure the output directory exists const outputDir = path.dirname(OUTPUT_FILE); if (!existsSync(outputDir)) { - mkdirSync(outputDir, { recursive: true }); + mkdirSync(outputDir, { recursive: true }); } -console.log('🔠Generating frontend license report...'); +console.log("🔠Generating frontend license report..."); try { - // Safety guard: don't run this script on fork PRs (workflow setzt PR_IS_FORK) - if (process.env.PR_IS_FORK === 'true' && !POSTPROCESS_ONLY) { - console.error('Fork PR detected: only --input (postprocess-only) mode is allowed.'); - process.exit(2); - } - - let licenseData; - // Generate license report using pinned license-checker; disable lifecycle scripts - if (POSTPROCESS_ONLY) { - if (!INPUT_FILE || !existsSync(INPUT_FILE)) { - console.error('⌠--input file missing or not found'); - process.exit(1); - } - licenseData = JSON.parse(readFileSync(INPUT_FILE, 'utf8')); - } else { - const licenseReport = execSync( - // 'npx --yes license-checker@25.0.1 --production --json', - 'npx --yes license-report --only=prod --output=json', - { - encoding: 'utf8', - cwd: path.dirname(PACKAGE_JSON), - env: { ...process.env, NPM_CONFIG_IGNORE_SCRIPTS: 'true' } - } - ); - try { - licenseData = JSON.parse(licenseReport); - } catch (parseError) { - console.error('⌠Failed to parse license data:', parseError.message); - console.error('Raw output:', licenseReport.substring(0, 500) + '...'); - process.exit(1); - } - } - - if (!Array.isArray(licenseData)) { - console.error('⌠Invalid license data structure'); - process.exit(1); - } - - // Convert license-checker format to array - const licenseArray = licenseData.map(dep => { - let licenseType = dep.licenseType; - - // Handle missing or null licenses - if (!licenseType || licenseType === null || licenseType === undefined) { - licenseType = 'Unknown'; - } - - // Handle empty string licenses - if (licenseType === '') { - licenseType = 'Unknown'; - } - - // Handle array licenses (rare but possible) - if (Array.isArray(licenseType)) { - licenseType = licenseType.join(' AND '); - } - - // Handle object licenses (fallback) - if (typeof licenseType === 'object' && licenseType !== null) { - licenseType = 'Unknown'; - } - - if ( "posthog-js" === dep.name && licenseType.startsWith("SEE LICENSE IN LICENSE")) { - licenseType = "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE"; - } - - return { - name: dep.name, - version: dep.installedVersion || dep.definedVersion || dep.remoteVersion || 'unknown', - licenseType: licenseType, - repository: dep.link, - url: dep.link, - link: dep.link - }; - }); - - // Transform to match Java backend format - const transformedData = { - dependencies: licenseArray.map(dep => { - const licenseType = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : (dep.licenseType || 'Unknown'); - const licenseUrl = dep.link || getLicenseUrl(licenseType); - - return { - moduleName: dep.name, - moduleUrl: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`, - moduleVersion: dep.version, - moduleLicense: licenseType, - moduleLicenseUrl: licenseUrl - }; - }) - }; - - // Log summary of license types found - const licenseSummary = licenseArray.reduce((acc, dep) => { - const license = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : (dep.licenseType || 'Unknown'); - acc[license] = (acc[license] || 0) + 1; - return acc; - }, {}); - - console.log('📊 License types found:'); - Object.entries(licenseSummary).forEach(([license, count]) => { - console.log(` ${license}: ${count} packages`); - }); - - // Log any complex or unusual license formats for debugging - const complexLicenses = licenseArray.filter(dep => - dep.licenseType && ( - dep.licenseType.includes('AND') || - dep.licenseType.includes('OR') || - dep.licenseType === 'Unknown' || - dep.licenseType.includes('SEE LICENSE') - ) + // Safety guard: don't run this script on fork PRs (workflow setzt PR_IS_FORK) + if (process.env.PR_IS_FORK === "true" && !POSTPROCESS_ONLY) { + console.error( + "Fork PR detected: only --input (postprocess-only) mode is allowed.", ); + process.exit(2); + } - if (complexLicenses.length > 0) { - console.log('\n🔠Complex/Edge case licenses detected:'); - complexLicenses.forEach(dep => { - console.log(` ${dep.name}@${dep.version}: "${dep.licenseType}"`); - }); + let licenseData; + // Generate license report using pinned license-checker; disable lifecycle scripts + if (POSTPROCESS_ONLY) { + if (!INPUT_FILE || !existsSync(INPUT_FILE)) { + console.error("⌠--input file missing or not found"); + process.exit(1); } - - // Check for potentially problematic licenses - const problematicLicenses = checkLicenseCompatibility(licenseSummary, licenseArray); - if (problematicLicenses.length > 0) { - console.log('\nâš ï¸ License compatibility warnings:'); - problematicLicenses.forEach(warning => { - console.log(` ${warning.message}`); - }); - - // Write license warnings to a separate file for CI/CD - const warningsFile = path.join(__dirname, '..', 'src', 'assets', 'license-warnings.json'); - writeFileSync(warningsFile, JSON.stringify({ - warnings: problematicLicenses, - generated: new Date().toISOString() - }, null, 2)); - console.log(`âš ï¸ License warnings saved to: ${warningsFile}`); - } else { - console.log('\n✅ All licenses appear to be corporate-friendly'); + licenseData = JSON.parse(readFileSync(INPUT_FILE, "utf8")); + } else { + const licenseReport = execSync( + // 'npx --yes license-checker@25.0.1 --production --json', + "npx --yes license-report --only=prod --output=json", + { + encoding: "utf8", + cwd: path.dirname(PACKAGE_JSON), + env: { ...process.env, NPM_CONFIG_IGNORE_SCRIPTS: "true" }, + }, + ); + try { + licenseData = JSON.parse(licenseReport); + } catch (parseError) { + console.error("⌠Failed to parse license data:", parseError.message); + console.error("Raw output:", licenseReport.substring(0, 500) + "..."); + process.exit(1); } + } - // Write to file - writeFileSync(OUTPUT_FILE, JSON.stringify(transformedData, null, 4)); - - console.log(`✅ License report generated successfully!`); - console.log(`📄 Found ${transformedData.dependencies.length} dependencies`); - console.log(`💾 Saved to: ${OUTPUT_FILE}`); - -} catch (error) { - console.error('⌠Error generating license report:', error.message); + if (!Array.isArray(licenseData)) { + console.error("⌠Invalid license data structure"); process.exit(1); + } + + // Convert license-checker format to array + const licenseArray = licenseData.map((dep) => { + let licenseType = dep.licenseType; + + // Handle missing or null licenses + if (!licenseType || licenseType === null || licenseType === undefined) { + licenseType = "Unknown"; + } + + // Handle empty string licenses + if (licenseType === "") { + licenseType = "Unknown"; + } + + // Handle array licenses (rare but possible) + if (Array.isArray(licenseType)) { + licenseType = licenseType.join(" AND "); + } + + // Handle object licenses (fallback) + if (typeof licenseType === "object" && licenseType !== null) { + licenseType = "Unknown"; + } + + if ( + "posthog-js" === dep.name && + licenseType.startsWith("SEE LICENSE IN LICENSE") + ) { + licenseType = + "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE"; + } + + return { + name: dep.name, + version: + dep.installedVersion || + dep.definedVersion || + dep.remoteVersion || + "unknown", + licenseType: licenseType, + repository: dep.link, + url: dep.link, + link: dep.link, + }; + }); + + // Transform to match Java backend format + const transformedData = { + dependencies: licenseArray.map((dep) => { + const licenseType = Array.isArray(dep.licenseType) + ? dep.licenseType.join(", ") + : dep.licenseType || "Unknown"; + const licenseUrl = dep.link || getLicenseUrl(licenseType); + + return { + moduleName: dep.name, + moduleUrl: + dep.repository || + dep.url || + `https://www.npmjs.com/package/${dep.name}`, + moduleVersion: dep.version, + moduleLicense: licenseType, + moduleLicenseUrl: licenseUrl, + }; + }), + }; + + // Log summary of license types found + const licenseSummary = licenseArray.reduce((acc, dep) => { + const license = Array.isArray(dep.licenseType) + ? dep.licenseType.join(", ") + : dep.licenseType || "Unknown"; + acc[license] = (acc[license] || 0) + 1; + return acc; + }, {}); + + console.log("📊 License types found:"); + Object.entries(licenseSummary).forEach(([license, count]) => { + console.log(` ${license}: ${count} packages`); + }); + + // Log any complex or unusual license formats for debugging + const complexLicenses = licenseArray.filter( + (dep) => + dep.licenseType && + (dep.licenseType.includes("AND") || + dep.licenseType.includes("OR") || + dep.licenseType === "Unknown" || + dep.licenseType.includes("SEE LICENSE")), + ); + + if (complexLicenses.length > 0) { + console.log("\n🔠Complex/Edge case licenses detected:"); + complexLicenses.forEach((dep) => { + console.log(` ${dep.name}@${dep.version}: "${dep.licenseType}"`); + }); + } + + // Check for potentially problematic licenses + const problematicLicenses = checkLicenseCompatibility( + licenseSummary, + licenseArray, + ); + if (problematicLicenses.length > 0) { + console.log("\nâš ï¸ License compatibility warnings:"); + problematicLicenses.forEach((warning) => { + console.log(` ${warning.message}`); + }); + + // Write license warnings to a separate file for CI/CD + const warningsFile = path.join( + __dirname, + "..", + "src", + "assets", + "license-warnings.json", + ); + writeFileSync( + warningsFile, + JSON.stringify( + { + warnings: problematicLicenses, + generated: new Date().toISOString(), + }, + null, + 2, + ), + ); + console.log(`âš ï¸ License warnings saved to: ${warningsFile}`); + } else { + console.log("\n✅ All licenses appear to be corporate-friendly"); + } + + // Write to file + writeFileSync(OUTPUT_FILE, JSON.stringify(transformedData, null, 2) + "\n"); + + console.log(`✅ License report generated successfully!`); + console.log(`📄 Found ${transformedData.dependencies.length} dependencies`); + console.log(`💾 Saved to: ${OUTPUT_FILE}`); +} catch (error) { + console.error("⌠Error generating license report:", error.message); + process.exit(1); } /** * Get standard license URLs for common licenses */ function getLicenseUrl(licenseType) { - if (!licenseType || licenseType === 'Unknown') return ''; + if (!licenseType || licenseType === "Unknown") return ""; - const licenseUrls = { - 'MIT': 'https://opensource.org/licenses/MIT', - 'MIT*': 'https://opensource.org/licenses/MIT', - 'Apache-2.0': 'https://www.apache.org/licenses/LICENSE-2.0', - 'Apache License 2.0': 'https://www.apache.org/licenses/LICENSE-2.0', - 'BSD-3-Clause': 'https://opensource.org/licenses/BSD-3-Clause', - 'BSD-2-Clause': 'https://opensource.org/licenses/BSD-2-Clause', - 'BSD': 'https://opensource.org/licenses/BSD-3-Clause', - 'GPL-3.0': 'https://www.gnu.org/licenses/gpl-3.0.html', - 'GPL-2.0': 'https://www.gnu.org/licenses/gpl-2.0.html', - 'LGPL-2.1': 'https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html', - 'LGPL-3.0': 'https://www.gnu.org/licenses/lgpl-3.0.html', - 'ISC': 'https://opensource.org/licenses/ISC', - 'CC0-1.0': 'https://creativecommons.org/publicdomain/zero/1.0/', - 'Unlicense': 'https://unlicense.org/', - 'MPL-2.0': 'https://www.mozilla.org/en-US/MPL/2.0/', - 'WTFPL': 'http://www.wtfpl.net/', - 'Zlib': 'https://opensource.org/licenses/Zlib', - 'Artistic-2.0': 'https://opensource.org/licenses/Artistic-2.0', - 'EPL-1.0': 'https://www.eclipse.org/legal/epl-v10.html', - 'EPL-2.0': 'https://www.eclipse.org/legal/epl-2.0/', - 'CDDL-1.0': 'https://opensource.org/licenses/CDDL-1.0', - 'Ruby': 'https://www.ruby-lang.org/en/about/license.txt', - 'Python-2.0': 'https://www.python.org/download/releases/2.0/license/', - 'Public Domain': 'https://creativecommons.org/publicdomain/zero/1.0/', - 'UNLICENSED': '' - }; + const licenseUrls = { + MIT: "https://opensource.org/licenses/MIT", + "MIT*": "https://opensource.org/licenses/MIT", + "Apache-2.0": "https://www.apache.org/licenses/LICENSE-2.0", + "Apache License 2.0": "https://www.apache.org/licenses/LICENSE-2.0", + "BSD-3-Clause": "https://opensource.org/licenses/BSD-3-Clause", + "BSD-2-Clause": "https://opensource.org/licenses/BSD-2-Clause", + BSD: "https://opensource.org/licenses/BSD-3-Clause", + "GPL-3.0": "https://www.gnu.org/licenses/gpl-3.0.html", + "GPL-2.0": "https://www.gnu.org/licenses/gpl-2.0.html", + "LGPL-2.1": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html", + "LGPL-3.0": "https://www.gnu.org/licenses/lgpl-3.0.html", + ISC: "https://opensource.org/licenses/ISC", + "CC0-1.0": "https://creativecommons.org/publicdomain/zero/1.0/", + Unlicense: "https://unlicense.org/", + "MPL-2.0": "https://www.mozilla.org/en-US/MPL/2.0/", + WTFPL: "http://www.wtfpl.net/", + Zlib: "https://opensource.org/licenses/Zlib", + "Artistic-2.0": "https://opensource.org/licenses/Artistic-2.0", + "EPL-1.0": "https://www.eclipse.org/legal/epl-v10.html", + "EPL-2.0": "https://www.eclipse.org/legal/epl-2.0/", + "CDDL-1.0": "https://opensource.org/licenses/CDDL-1.0", + Ruby: "https://www.ruby-lang.org/en/about/license.txt", + "Python-2.0": "https://www.python.org/download/releases/2.0/license/", + "Public Domain": "https://creativecommons.org/publicdomain/zero/1.0/", + UNLICENSED: "", + }; - // Try exact match first - if (licenseUrls[licenseType]) { - return licenseUrls[licenseType]; + // Try exact match first + if (licenseUrls[licenseType]) { + return licenseUrls[licenseType]; + } + + // Try case-insensitive match + const lowerType = licenseType.toLowerCase(); + for (const [key, url] of Object.entries(licenseUrls)) { + if (key.toLowerCase() === lowerType) { + return url; } + } - // Try case-insensitive match - const lowerType = licenseType.toLowerCase(); - for (const [key, url] of Object.entries(licenseUrls)) { - if (key.toLowerCase() === lowerType) { - return url; - } + // Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)" + if (licenseType.includes("AND") || licenseType.includes("OR")) { + // Extract the first license from compound expressions for URL + const match = licenseType.match(/\(?\s*([A-Za-z0-9\-.]+)/); + if (match && licenseUrls[match[1]]) { + return licenseUrls[match[1]]; } + } - // Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)" - if (licenseType.includes('AND') || licenseType.includes('OR')) { - // Extract the first license from compound expressions for URL - const match = licenseType.match(/\(?\s*([A-Za-z0-9\-.]+)/); - if (match && licenseUrls[match[1]]) { - return licenseUrls[match[1]]; - } - } - - // For non-standard licenses, return empty string (will use package link if available) - return ''; + // For non-standard licenses, return empty string (will use package link if available) + return ""; } /** * Check for potentially problematic licenses that may not be MIT/corporate compatible */ function checkLicenseCompatibility(licenseSummary, licenseArray) { - const warnings = []; + const warnings = []; - // Define problematic license patterns - const problematicLicenses = { - // Copyleft licenses - 'GPL-2.0': 'Strong copyleft license - requires derivative works to be GPL', - 'GPL-3.0': 'Strong copyleft license - requires derivative works to be GPL', - 'LGPL-2.1': 'Weak copyleft license - may require source disclosure for modifications', - 'LGPL-3.0': 'Weak copyleft license - may require source disclosure for modifications', - 'AGPL-3.0': 'Network copyleft license - requires source disclosure for network use', - 'AGPL-1.0': 'Network copyleft license - requires source disclosure for network use', + // Define problematic license patterns + const problematicLicenses = { + // Copyleft licenses + "GPL-2.0": "Strong copyleft license - requires derivative works to be GPL", + "GPL-3.0": "Strong copyleft license - requires derivative works to be GPL", + "LGPL-2.1": + "Weak copyleft license - may require source disclosure for modifications", + "LGPL-3.0": + "Weak copyleft license - may require source disclosure for modifications", + "AGPL-3.0": + "Network copyleft license - requires source disclosure for network use", + "AGPL-1.0": + "Network copyleft license - requires source disclosure for network use", - // Other potentially problematic licenses - 'WTFPL': 'Potentially problematic license - legal uncertainty', - 'CC-BY-SA-4.0': 'ShareAlike license - requires derivative works to use same license', - 'CC-BY-SA-3.0': 'ShareAlike license - requires derivative works to use same license', - 'CC-BY-NC-4.0': 'Non-commercial license - prohibits commercial use', - 'CC-BY-NC-3.0': 'Non-commercial license - prohibits commercial use', - 'OSL-3.0': 'Copyleft license - requires derivative works to be OSL', - 'EPL-1.0': 'Weak copyleft license - may require source disclosure', - 'EPL-2.0': 'Weak copyleft license - may require source disclosure', - 'CDDL-1.0': 'Weak copyleft license - may require source disclosure', - 'CDDL-1.1': 'Weak copyleft license - may require source disclosure', - 'CPL-1.0': 'Weak copyleft license - may require source disclosure', - 'MPL-1.1': 'Weak copyleft license - may require source disclosure', - 'EUPL-1.1': 'Copyleft license - requires derivative works to be EUPL', - 'EUPL-1.2': 'Copyleft license - requires derivative works to be EUPL', - 'UNLICENSED': 'No license specified - usage rights unclear', - 'Unknown': 'License not detected - manual review required' - }; + // Other potentially problematic licenses + WTFPL: "Potentially problematic license - legal uncertainty", + "CC-BY-SA-4.0": + "ShareAlike license - requires derivative works to use same license", + "CC-BY-SA-3.0": + "ShareAlike license - requires derivative works to use same license", + "CC-BY-NC-4.0": "Non-commercial license - prohibits commercial use", + "CC-BY-NC-3.0": "Non-commercial license - prohibits commercial use", + "OSL-3.0": "Copyleft license - requires derivative works to be OSL", + "EPL-1.0": "Weak copyleft license - may require source disclosure", + "EPL-2.0": "Weak copyleft license - may require source disclosure", + "CDDL-1.0": "Weak copyleft license - may require source disclosure", + "CDDL-1.1": "Weak copyleft license - may require source disclosure", + "CPL-1.0": "Weak copyleft license - may require source disclosure", + "MPL-1.1": "Weak copyleft license - may require source disclosure", + "EUPL-1.1": "Copyleft license - requires derivative works to be EUPL", + "EUPL-1.2": "Copyleft license - requires derivative works to be EUPL", + UNLICENSED: "No license specified - usage rights unclear", + Unknown: "License not detected - manual review required", + }; - // Known good licenses (no warnings needed) - const goodLicenses = new Set([ - 'MIT', 'MIT*', 'Apache-2.0', 'Apache License 2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'BSD', - 'ISC', 'CC0-1.0', 'Public Domain', 'Unlicense', '0BSD', 'BlueOak-1.0.0', - 'Zlib', 'Artistic-2.0', 'Python-2.0', 'Ruby', 'MPL-2.0', 'CC-BY-4.0', - 'SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE', - 'SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE' - ]); + // Known good licenses (no warnings needed) + const goodLicenses = new Set([ + "MIT", + "MIT*", + "Apache-2.0", + "Apache License 2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "BSD", + "ISC", + "CC0-1.0", + "Public Domain", + "Unlicense", + "0BSD", + "BlueOak-1.0.0", + "Zlib", + "Artistic-2.0", + "Python-2.0", + "Ruby", + "MPL-2.0", + "CC-BY-4.0", + "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE", + "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE", + ]); - // Helper function to normalize license names for comparison - function normalizeLicense(license) { - return license - .replace(/-or-later$/, '') // Remove -or-later suffix - .replace(/\+$/, '') // Remove + suffix - .trim(); + // Helper function to normalize license names for comparison + function normalizeLicense(license) { + return license + .replace(/-or-later$/, "") // Remove -or-later suffix + .replace(/\+$/, "") // Remove + suffix + .trim(); + } + + // Check each license type + Object.entries(licenseSummary).forEach(([license, count]) => { + // Skip known good licenses + if (goodLicenses.has(license)) { + return; } - // Check each license type - Object.entries(licenseSummary).forEach(([license, count]) => { - // Skip known good licenses - if (goodLicenses.has(license)) { - return; - } - - // Check if this license only affects our own packages - const affectedPackages = licenseArray.filter(dep => { - const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : dep.licenseType; - return depLicense === license; - }); - - const isOnlyOurPackages = affectedPackages.every(dep => - dep.name === 'frontend' || - dep.name.toLowerCase().includes('stirling-pdf') || - dep.name.toLowerCase().includes('stirling_pdf') || - dep.name.toLowerCase().includes('stirlingpdf') - ); - - if (isOnlyOurPackages && (license === 'UNLICENSED' || license.startsWith('SEE LICENSE IN'))) { - return; // Skip warnings for our own Stirling-PDF packages - } - - // Check for compound licenses like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)" - if (license.includes('AND') || license.includes('OR')) { - // For OR licenses, check if there's at least one acceptable license option - if (license.includes('OR')) { - // Extract license components from OR expression - const orComponents = license - .replace(/[()]/g, '') // Remove parentheses - .split(' OR ') - .map(component => component.trim()); - - // Check if any component is in the goodLicenses set (with normalization) - const hasGoodLicense = orComponents.some(component => { - const normalized = normalizeLicense(component); - return goodLicenses.has(component) || goodLicenses.has(normalized); - }); - - if (hasGoodLicense) { - return; // Skip warning - can use the good license option - } - } - - // For AND licenses or OR licenses with no good options, check for problematic components - const hasProblematicComponent = Object.keys(problematicLicenses).some(problematic => - license.includes(problematic) - ); - - if (hasProblematicComponent) { - const affectedPackages = licenseArray - .filter(dep => { - const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : dep.licenseType; - return depLicense === license; - }) - .map(dep => ({ - name: dep.name, - version: dep.version, - url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}` - })); - - const licenseType = license.includes('AND') ? 'AND' : 'OR'; - const reason = licenseType === 'AND' - ? 'Compound license with AND requirement - all components must be compatible' - : 'Compound license with potentially problematic components and no good fallback options'; - - warnings.push({ - message: `📋 This PR contains ${count} package${count > 1 ? 's' : ''} with compound license "${license}" - manual review recommended`, - licenseType: license, - licenseUrl: '', - reason: reason, - packageCount: count, - affectedDependencies: affectedPackages - }); - } - return; - } - - // Check for exact matches with problematic licenses - if (problematicLicenses[license]) { - const affectedPackages = licenseArray - .filter(dep => { - const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : dep.licenseType; - return depLicense === license; - }) - .map(dep => ({ - name: dep.name, - version: dep.version, - url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}` - })); - - const packageList = affectedPackages.map(pkg => pkg.name).slice(0, 5).join(', ') + (affectedPackages.length > 5 ? `, and ${affectedPackages.length - 5} more` : ''); - const licenseUrl = getLicenseUrl(license) || 'https://opensource.org/licenses'; - - warnings.push({ - message: `âš ï¸ This PR contains ${count} package${count > 1 ? 's' : ''} with license type [${license}](${licenseUrl}) - ${problematicLicenses[license]}. Affected packages: ${packageList}`, - licenseType: license, - licenseUrl: licenseUrl, - reason: problematicLicenses[license], - packageCount: count, - affectedDependencies: affectedPackages - }); - } else { - // Unknown license type - flag for manual review - const affectedPackages = licenseArray - .filter(dep => { - const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : dep.licenseType; - return depLicense === license; - }) - .map(dep => ({ - name: dep.name, - version: dep.version, - url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}` - })); - - warnings.push({ - message: `â“ This PR contains ${count} package${count > 1 ? 's' : ''} with unknown license type "${license}" - manual review required`, - licenseType: license, - licenseUrl: '', - reason: 'Unknown license type', - packageCount: count, - affectedDependencies: affectedPackages - }); - } + // Check if this license only affects our own packages + const affectedPackages = licenseArray.filter((dep) => { + const depLicense = Array.isArray(dep.licenseType) + ? dep.licenseType.join(", ") + : dep.licenseType; + return depLicense === license; }); - return warnings; + const isOnlyOurPackages = affectedPackages.every( + (dep) => + dep.name === "frontend" || + dep.name.toLowerCase().includes("stirling-pdf") || + dep.name.toLowerCase().includes("stirling_pdf") || + dep.name.toLowerCase().includes("stirlingpdf"), + ); + + if ( + isOnlyOurPackages && + (license === "UNLICENSED" || license.startsWith("SEE LICENSE IN")) + ) { + return; // Skip warnings for our own Stirling-PDF packages + } + + // Check for compound licenses like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)" + if (license.includes("AND") || license.includes("OR")) { + // For OR licenses, check if there's at least one acceptable license option + if (license.includes("OR")) { + // Extract license components from OR expression + const orComponents = license + .replace(/[()]/g, "") // Remove parentheses + .split(" OR ") + .map((component) => component.trim()); + + // Check if any component is in the goodLicenses set (with normalization) + const hasGoodLicense = orComponents.some((component) => { + const normalized = normalizeLicense(component); + return goodLicenses.has(component) || goodLicenses.has(normalized); + }); + + if (hasGoodLicense) { + return; // Skip warning - can use the good license option + } + } + + // For AND licenses or OR licenses with no good options, check for problematic components + const hasProblematicComponent = Object.keys(problematicLicenses).some( + (problematic) => license.includes(problematic), + ); + + if (hasProblematicComponent) { + const affectedPackages = licenseArray + .filter((dep) => { + const depLicense = Array.isArray(dep.licenseType) + ? dep.licenseType.join(", ") + : dep.licenseType; + return depLicense === license; + }) + .map((dep) => ({ + name: dep.name, + version: dep.version, + url: + dep.repository || + dep.url || + `https://www.npmjs.com/package/${dep.name}`, + })); + + const licenseType = license.includes("AND") ? "AND" : "OR"; + const reason = + licenseType === "AND" + ? "Compound license with AND requirement - all components must be compatible" + : "Compound license with potentially problematic components and no good fallback options"; + + warnings.push({ + message: `📋 This PR contains ${count} package${count > 1 ? "s" : ""} with compound license "${license}" - manual review recommended`, + licenseType: license, + licenseUrl: "", + reason: reason, + packageCount: count, + affectedDependencies: affectedPackages, + }); + } + return; + } + + // Check for exact matches with problematic licenses + if (problematicLicenses[license]) { + const affectedPackages = licenseArray + .filter((dep) => { + const depLicense = Array.isArray(dep.licenseType) + ? dep.licenseType.join(", ") + : dep.licenseType; + return depLicense === license; + }) + .map((dep) => ({ + name: dep.name, + version: dep.version, + url: + dep.repository || + dep.url || + `https://www.npmjs.com/package/${dep.name}`, + })); + + const packageList = + affectedPackages + .map((pkg) => pkg.name) + .slice(0, 5) + .join(", ") + + (affectedPackages.length > 5 + ? `, and ${affectedPackages.length - 5} more` + : ""); + const licenseUrl = + getLicenseUrl(license) || "https://opensource.org/licenses"; + + warnings.push({ + message: `âš ï¸ This PR contains ${count} package${count > 1 ? "s" : ""} with license type [${license}](${licenseUrl}) - ${problematicLicenses[license]}. Affected packages: ${packageList}`, + licenseType: license, + licenseUrl: licenseUrl, + reason: problematicLicenses[license], + packageCount: count, + affectedDependencies: affectedPackages, + }); + } else { + // Unknown license type - flag for manual review + const affectedPackages = licenseArray + .filter((dep) => { + const depLicense = Array.isArray(dep.licenseType) + ? dep.licenseType.join(", ") + : dep.licenseType; + return depLicense === license; + }) + .map((dep) => ({ + name: dep.name, + version: dep.version, + url: + dep.repository || + dep.url || + `https://www.npmjs.com/package/${dep.name}`, + })); + + warnings.push({ + message: `â“ This PR contains ${count} package${count > 1 ? "s" : ""} with unknown license type "${license}" - manual review required`, + licenseType: license, + licenseUrl: "", + reason: "Unknown license type", + packageCount: count, + affectedDependencies: affectedPackages, + }); + } + }); + + return warnings; } diff --git a/frontend/scripts/sample-pdf/generate.mjs b/frontend/scripts/sample-pdf/generate.mjs index 93e5cf7ee3..2ad477cc98 100755 --- a/frontend/scripts/sample-pdf/generate.mjs +++ b/frontend/scripts/sample-pdf/generate.mjs @@ -8,20 +8,20 @@ * for users to experiment with Stirling PDF's features. */ -import puppeteer from 'puppeteer'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; -import { existsSync, mkdirSync, statSync } from 'fs'; +import puppeteer from "puppeteer"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import { existsSync, mkdirSync, statSync } from "fs"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const TEMPLATE_PATH = join(__dirname, 'template.html'); -const OUTPUT_DIR = join(__dirname, '../../public/samples'); -const OUTPUT_PATH = join(OUTPUT_DIR, 'Sample.pdf'); +const TEMPLATE_PATH = join(__dirname, "template.html"); +const OUTPUT_DIR = join(__dirname, "../../public/samples"); +const OUTPUT_PATH = join(OUTPUT_DIR, "Sample.pdf"); async function generatePDF() { - console.log('🚀 Starting Stirling PDF sample document generation...\n'); + console.log("🚀 Starting Stirling PDF sample document generation...\n"); // Ensure output directory exists if (!existsSync(OUTPUT_DIR)) { @@ -40,66 +40,65 @@ async function generatePDF() { let browser; try { // Launch Puppeteer - console.log('🌠Launching browser...'); + console.log("🌠Launching browser..."); browser = await puppeteer.launch({ - headless: 'new', - args: ['--no-sandbox', '--disable-setuid-sandbox'] + headless: "new", + args: ["--no-sandbox", "--disable-setuid-sandbox"], }); const page = await browser.newPage(); // Set viewport to match A4 proportions await page.setViewport({ - width: 794, // A4 width in pixels at 96 DPI + width: 794, // A4 width in pixels at 96 DPI height: 1123, // A4 height in pixels at 96 DPI - deviceScaleFactor: 2 // Higher quality rendering + deviceScaleFactor: 2, // Higher quality rendering }); // Navigate to the template file const fileUrl = `file://${TEMPLATE_PATH}`; - console.log('📖 Loading HTML template...'); + console.log("📖 Loading HTML template..."); await page.goto(fileUrl, { - waitUntil: 'networkidle0' // Wait for all resources to load + waitUntil: "networkidle0", // Wait for all resources to load }); // Generate PDF with A4 dimensions - console.log('📠Generating PDF...'); + console.log("📠Generating PDF..."); await page.pdf({ path: OUTPUT_PATH, - format: 'A4', + format: "A4", printBackground: true, margin: { top: 0, right: 0, bottom: 0, - left: 0 + left: 0, }, - preferCSSPageSize: true + preferCSSPageSize: true, }); - console.log('\n✅ PDF generated successfully!'); + console.log("\n✅ PDF generated successfully!"); console.log(`📦 Output: ${OUTPUT_PATH}`); // Get file size const stats = statSync(OUTPUT_PATH); const fileSizeInKB = (stats.size / 1024).toFixed(2); console.log(`📊 File size: ${fileSizeInKB} KB`); - } catch (error) { - console.error('\n⌠Error generating PDF:', error.message); + console.error("\n⌠Error generating PDF:", error.message); process.exit(1); } finally { if (browser) { await browser.close(); - console.log('🔒 Browser closed.'); + console.log("🔒 Browser closed."); } } - console.log('\n🎉 Done! Sample PDF is ready for use in Stirling PDF.\n'); + console.log("\n🎉 Done! Sample PDF is ready for use in Stirling PDF.\n"); } // Run the generator -generatePDF().catch(error => { - console.error('Fatal error:', error); +generatePDF().catch((error) => { + console.error("Fatal error:", error); process.exit(1); }); diff --git a/frontend/scripts/sample-pdf/styles.css b/frontend/scripts/sample-pdf/styles.css index 067452833c..f4cb87ac6e 100644 --- a/frontend/scripts/sample-pdf/styles.css +++ b/frontend/scripts/sample-pdf/styles.css @@ -20,8 +20,9 @@ --color-white: #ffffff; /* Font Stack */ - --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + --font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", + "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; } * { diff --git a/frontend/scripts/sample-pdf/template.html b/frontend/scripts/sample-pdf/template.html index edd7f2c9f4..20733766fa 100644 --- a/frontend/scripts/sample-pdf/template.html +++ b/frontend/scripts/sample-pdf/template.html @@ -1,234 +1,325 @@ - + - - - - Stirling PDF - Sample Document - - - - -

-
- - - - - -
-
-
- + + + + Stirling PDF - Sample Document + + + + +
+
+ + + + +
-

The Free Adobe Acrobat Alternative

-
-
- 10M+ - Downloads +
+
+
-
-
-
Open Source
-
Privacy First
-
Self-Hosted
-
-
-
- - -
-
-

What is Stirling PDF?

-

- Stirling PDF is a robust, web-based PDF manipulation tool. - It enables you to carry out various operations on PDF files, including splitting, - merging, converting, rearranging, adding images, rotating, compressing, and more. -

- -
-
-
- - - +

The Free Adobe Acrobat Alternative

+
+
+ 10M+ + Downloads
-

50+ PDF Operations

-

Comprehensive toolkit covering all your PDF needs. From basic operations to advanced processing.

- -
-
- - - -
-

Workflow Automation

-

Chain multiple operations together and save them as reusable workflows. Perfect for recurring tasks.

-
- -
-
- - - - - -
-

Multi-Language Support

-

Available in over 30 languages with community-contributed translations. Accessible to users worldwide.

-
- -
-
- - - - - -
-

Privacy First

-

Self-hosted solution means your data stays on your infrastructure. You have full control over your documents.

-
- -
-
- - - - -
-

Open Source

-

Transparent, community-driven development. Inspect the code, contribute features, and adapt as needed.

-
- -
-
- - - - -
-

API Access

-

RESTful API for integration with external tools and scripts. Automate PDF operations programmatically.

+
+
Open Source
+
Privacy First
+
Self-Hosted
-
- -
-
-

Key Features

+ +
+
+

What is Stirling PDF?

+

+ Stirling PDF is a robust, web-based PDF manipulation tool. It enables + you to carry out various operations on PDF files, including splitting, + merging, converting, rearranging, adding images, rotating, + compressing, and more. +

-
-
-
-
- - - - - - +
+
+
+ +
-

Page Operations

+

50+ PDF Operations

+

+ Comprehensive toolkit covering all your PDF needs. From basic + operations to advanced processing. +

-
    -
  • Merge & split PDFs
  • -
  • Rearrange pages
  • -
  • Rotate & crop
  • -
  • Extract pages
  • -
  • Multi-page layout
  • -
-
-
-
-
- - - - -
-

Security & Signing

-
-
    -
  • Password protection
  • -
  • Digital signatures
  • -
  • Watermarks
  • -
  • Permission controls
  • -
  • Redaction tools
  • -
-
- -
-
-
+
+
- +
-

File Conversions

+

Workflow Automation

+

+ Chain multiple operations together and save them as reusable + workflows. Perfect for recurring tasks. +

-
    -
  • PDF to/from images
  • -
  • Office documents
  • -
  • HTML to PDF
  • -
  • Markdown to PDF
  • -
  • PDF to Word/Excel
  • -
-
-
-
-
- - +
+
+ + + +
-

Automation

+

Multi-Language Support

+

+ Available in over 30 languages with community-contributed + translations. Accessible to users worldwide. +

-
    -
  • Multi-step workflows
  • -
  • Chain PDF operations
  • -
  • Save recurring tasks
  • -
  • Batch file processing
  • -
  • API integration
  • -
-
-
-
-
-
- - - +
+
+ + + + + +
+

Privacy First

+

+ Self-hosted solution means your data stays on your infrastructure. + You have full control over your documents. +

+
+ +
+
+ + + + +
+

Open Source

+

+ Transparent, community-driven development. Inspect the code, + contribute features, and adapt as needed. +

+
+ +
+
+ + + + +
+

API Access

+

+ RESTful API for integration with external tools and scripts. + Automate PDF operations programmatically. +

-

Plus Many More

-
-
-
    -
  • OCR text recognition
  • -
  • Compress PDFs
  • -
  • Add images & stamps
  • -
  • Detect blank pages
  • -
  • Extract images
  • -
  • Edit metadata
  • -
-
    -
  • Flatten forms
  • -
  • PDF/A conversion
  • -
  • Add page numbers
  • -
  • Remove pages
  • -
  • Repair PDFs
  • -
  • And 40+ more tools
  • -
-
- + +
+
+

Key Features

+ +
+
+
+
+ + + + + + + +
+

Page Operations

+
+
    +
  • Merge & split PDFs
  • +
  • Rearrange pages
  • +
  • Rotate & crop
  • +
  • Extract pages
  • +
  • Multi-page layout
  • +
+
+ +
+
+
+ + + + +
+

Security & Signing

+
+
    +
  • Password protection
  • +
  • Digital signatures
  • +
  • Watermarks
  • +
  • Permission controls
  • +
  • Redaction tools
  • +
+
+ +
+
+
+ + + +
+

File Conversions

+
+
    +
  • PDF to/from images
  • +
  • Office documents
  • +
  • HTML to PDF
  • +
  • Markdown to PDF
  • +
  • PDF to Word/Excel
  • +
+
+ +
+
+
+ + + +
+

Automation

+
+
    +
  • Multi-step workflows
  • +
  • Chain PDF operations
  • +
  • Save recurring tasks
  • +
  • Batch file processing
  • +
  • API integration
  • +
+
+
+ +
+
+
+ + + +
+

Plus Many More

+
+
+
    +
  • OCR text recognition
  • +
  • Compress PDFs
  • +
  • Add images & stamps
  • +
  • Detect blank pages
  • +
  • Extract images
  • +
  • Edit metadata
  • +
+
    +
  • Flatten forms
  • +
  • PDF/A conversion
  • +
  • Add page numbers
  • +
  • Remove pages
  • +
  • Repair PDFs
  • +
  • And 40+ more tools
  • +
+
+
+
+
+ diff --git a/frontend/scripts/setup-env.ts b/frontend/scripts/setup-env.ts index 00ec03df00..14c6aef82d 100644 --- a/frontend/scripts/setup-env.ts +++ b/frontend/scripts/setup-env.ts @@ -10,22 +10,24 @@ * tsx scripts/setup-env.ts --saas # also checks .env.saas */ -import { existsSync, copyFileSync, readFileSync } from 'fs'; -import { join } from 'path'; -import { config, parse } from 'dotenv'; +import { existsSync, copyFileSync, readFileSync } from "fs"; +import { join } from "path"; +import { config, parse } from "dotenv"; // npm scripts run from the directory containing package.json (frontend/) const root = process.cwd(); const args = process.argv.slice(2); -const isDesktop = args.includes('--desktop'); -const isSaas = args.includes('--saas'); +const isDesktop = args.includes("--desktop"); +const isSaas = args.includes("--saas"); -console.log('setup-env: see frontend/README.md#environment-variables for documentation'); +console.log( + "setup-env: see frontend/README.md#environment-variables for documentation", +); function getExampleKeys(exampleFile: string): string[] { const examplePath = join(root, exampleFile); if (!existsSync(examplePath)) return []; - return Object.keys(parse(readFileSync(examplePath, 'utf-8'))); + return Object.keys(parse(readFileSync(examplePath, "utf-8"))); } function ensureEnvFile(envFile: string, exampleFile: string): boolean { @@ -44,13 +46,15 @@ function ensureEnvFile(envFile: string, exampleFile: string): boolean { config({ path: envPath }); - const missing = getExampleKeys(exampleFile).filter(k => !(k in process.env)); + const missing = getExampleKeys(exampleFile).filter( + (k) => !(k in process.env), + ); if (missing.length > 0) { console.error( `setup-env: ${envFile} is missing keys from ${exampleFile}:\n` + - missing.map(k => ` ${k}`).join('\n') + - '\n Add them manually or delete your local file to re-copy from the example.' + missing.map((k) => ` ${k}`).join("\n") + + "\n Add them manually or delete your local file to re-copy from the example.", ); return true; } @@ -59,29 +63,31 @@ function ensureEnvFile(envFile: string, exampleFile: string): boolean { } let failed = false; -failed = ensureEnvFile('.env', 'config/.env.example') || failed; +failed = ensureEnvFile(".env", "config/.env.example") || failed; if (isDesktop) { - failed = ensureEnvFile('.env.desktop', 'config/.env.desktop.example') || failed; + failed = + ensureEnvFile(".env.desktop", "config/.env.desktop.example") || failed; } if (isSaas) { - failed = ensureEnvFile('.env.saas', 'config/.env.saas.example') || failed; + failed = ensureEnvFile(".env.saas", "config/.env.saas.example") || failed; } // Warn about any VITE_ vars set in the environment that aren't listed in any example file. const allExampleKeys = new Set([ - ...getExampleKeys('config/.env.example'), - ...getExampleKeys('config/.env.desktop.example'), - ...getExampleKeys('config/.env.saas.example'), + ...getExampleKeys("config/.env.example"), + ...getExampleKeys("config/.env.desktop.example"), + ...getExampleKeys("config/.env.saas.example"), ]); -const unknownViteVars = Object.keys(process.env) - .filter(k => k.startsWith('VITE_') && !allExampleKeys.has(k)); +const unknownViteVars = Object.keys(process.env).filter( + (k) => k.startsWith("VITE_") && !allExampleKeys.has(k), +); if (unknownViteVars.length > 0) { console.warn( - 'setup-env: the following VITE_ vars are set but not listed in any example file:\n' + - unknownViteVars.map(k => ` ${k}`).join('\n') + - '\n Add them to the appropriate config/.env.*.example file if they are required.' + "setup-env: the following VITE_ vars are set but not listed in any example file:\n" + + unknownViteVars.map((k) => ` ${k}`).join("\n") + + "\n Add them to the appropriate config/.env.*.example file if they are required.", ); } diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json index 6acaac5145..9259e4543f 100644 --- a/frontend/src-tauri/capabilities/default.json +++ b/frontend/src-tauri/capabilities/default.json @@ -2,9 +2,7 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "enables the default permissions", - "windows": [ - "main" - ], + "windows": ["main"], "permissions": [ "core:default", "core:window:allow-destroy", diff --git a/frontend/src-tauri/src/commands/connection.rs b/frontend/src-tauri/src/commands/connection.rs index 8b8c68a814..157f25ce5b 100644 --- a/frontend/src-tauri/src/commands/connection.rs +++ b/frontend/src-tauri/src/commands/connection.rs @@ -72,6 +72,28 @@ pub async fn set_connection_mode( ) -> Result<(), String> { log::info!("Setting connection mode: {:?}", mode); + let store = app_handle + .store(STORE_FILE) + .map_err(|e| format!("Failed to access store: {}", e))?; + + // If the store is already locked, protect connection_mode, server_config, and the lock + // flag from being overwritten by any JS-side call. + // Only allow marking setup_completed and updating auth-related fields. + let already_locked = store + .get(LOCK_CONNECTION_KEY) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + if already_locked { + log::warn!("set_connection_mode called while lock_connection_mode=true — preserving connection settings, but marking setup as completed"); + // Still allow setup_completed to be written so the onboarding doesn't repeat. + store.set(FIRST_LAUNCH_KEY, serde_json::json!(true)); + store + .save() + .map_err(|e| format!("Failed to save store: {}", e))?; + return Ok(()); + } + // Update in-memory state if let Ok(mut conn_state) = state.0.lock() { conn_state.mode = mode.clone(); @@ -81,11 +103,6 @@ pub async fn set_connection_mode( } } - // Save to store - let store = app_handle - .store(STORE_FILE) - .map_err(|e| format!("Failed to access store: {}", e))?; - store.set( CONNECTION_MODE_KEY, serde_json::to_value(&mode).map_err(|e| format!("Failed to serialize mode: {}", e))?, diff --git a/frontend/src-tauri/stirling-pdf.desktop b/frontend/src-tauri/stirling-pdf.desktop index 45db59c737..e9e26a39c1 100644 --- a/frontend/src-tauri/stirling-pdf.desktop +++ b/frontend/src-tauri/stirling-pdf.desktop @@ -3,7 +3,8 @@ Version=1.0 Type=Application Name=Stirling-PDF Comment=Locally hosted web application that allows you to perform various operations on PDF files -Exec=/usr/bin/stirling-pdf +TryExec={{exec}} +Exec={{exec}} %F Icon={{icon}} Terminal=false MimeType=application/pdf; @@ -12,4 +13,4 @@ Actions=open-file; [Desktop Action open-file] Name=Open PDF File -Exec=/usr/bin/stirling-pdf %F \ No newline at end of file +Exec={{exec}} %F \ No newline at end of file diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 44ef827c3f..117aea592e 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -1,98 +1,88 @@ { - "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", - "productName": "Stirling-PDF", - "version": "2.8.0", - "identifier": "stirling.pdf.dev", - "build": { - "frontendDist": "../dist", - "devUrl": "http://localhost:5173", - "beforeDevCommand": "npm run dev -- --mode desktop", - "beforeBuildCommand": "node scripts/build-provisioner.mjs && npm run build -- --mode desktop" + "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", + "productName": "Stirling-PDF", + "version": "2.9.2", + "identifier": "stirling.pdf.dev", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:5173", + "beforeDevCommand": "npx vite --mode desktop", + "beforeBuildCommand": "npx vite build --mode desktop" + }, + "app": { + "windows": [ + { + "title": "Stirling-PDF", + "width": 1280, + "height": 800, + "resizable": true, + "fullscreen": false, + "additionalBrowserArgs": "--enable-features=CertVerifierBuiltinFeature" + } + ] + }, + "bundle": { + "active": true, + "publisher": "Stirling PDF Inc.", + "targets": ["deb", "rpm", "appimage", "dmg", "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" + ], + "resources": ["libs/*.jar", "runtime/jre/**/*"], + "fileAssociations": [ + { + "ext": ["pdf"], + "name": "PDF Document", + "role": "Editor", + "mimeType": "application/pdf" + } + ], + "linux": { + "deb": { + "desktopTemplate": "stirling-pdf.desktop" + }, + "rpm": { + "desktopTemplate": "stirling-pdf.desktop" + }, + "appimage": { + "bundleMediaFramework": false + } }, - "app": { - "windows": [ - { - "title": "Stirling-PDF", - "width": 1280, - "height": 800, - "resizable": true, - "fullscreen": false, - "additionalBrowserArgs": "--enable-features=CertVerifierBuiltinFeature" - } - ] + "windows": { + "certificateThumbprint": null, + "digestAlgorithm": "sha256", + "timestampUrl": "http://timestamp.digicert.com", + "wix": { + "fragmentPaths": ["windows/wix/provisioning.wxs"], + "componentGroupRefs": ["ProvisioningComponentGroup"] + } }, - "bundle": { - "active": true, - "publisher": "Stirling PDF Inc.", - "targets": [ - "deb", - "rpm", - "dmg", - "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" - ], - "resources": [ - "libs/*.jar", - "runtime/jre/**/*" - ], - "fileAssociations": [ - { - "ext": [ - "pdf" - ], - "name": "PDF Document", - "role": "Editor", - "mimeType": "application/pdf" - } - ], - "linux": { - "deb": { - "desktopTemplate": "stirling-pdf.desktop" - } - }, - "windows": { - "certificateThumbprint": null, - "digestAlgorithm": "sha256", - "timestampUrl": "http://timestamp.digicert.com", - "wix": { - "fragmentPaths": [ - "windows/wix/provisioning.wxs" - ], - "componentGroupRefs": [ - "ProvisioningComponentGroup" - ] - } - }, - "macOS": { - "minimumSystemVersion": "10.15", - "signingIdentity": null, - "entitlements": null, - "providerShortName": null, - "infoPlist": "Info.plist" - } - }, - "plugins": { - "shell": { - "open": true - }, - "fs": { - "requireLiteralLeadingDot": false - }, - "deep-link": { - "desktop": { - "schemes": [ - "stirlingpdf" - ] - } - } + "macOS": { + "minimumSystemVersion": "10.15", + "signingIdentity": null, + "entitlements": null, + "providerShortName": null, + "infoPlist": "Info.plist" } + }, + "plugins": { + "shell": { + "open": true + }, + "fs": { + "requireLiteralLeadingDot": false + }, + "deep-link": { + "desktop": { + "schemes": ["stirlingpdf"] + } + } + } } diff --git a/frontend/src-tauri/thumbnail-handler/.gitignore b/frontend/src-tauri/thumbnail-handler/.gitignore new file mode 100644 index 0000000000..ea8c4bf7f3 --- /dev/null +++ b/frontend/src-tauri/thumbnail-handler/.gitignore @@ -0,0 +1 @@ +/target diff --git a/frontend/src-tauri/thumbnail-handler/Cargo.lock b/frontend/src-tauri/thumbnail-handler/Cargo.lock new file mode 100644 index 0000000000..713a4fb9e8 --- /dev/null +++ b/frontend/src-tauri/thumbnail-handler/Cargo.lock @@ -0,0 +1,174 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "stirling-thumbnail-handler" +version = "0.1.0" +dependencies = [ + "windows", + "windows-core", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/frontend/src-tauri/thumbnail-handler/Cargo.toml b/frontend/src-tauri/thumbnail-handler/Cargo.toml new file mode 100644 index 0000000000..87b40cad19 --- /dev/null +++ b/frontend/src-tauri/thumbnail-handler/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "stirling-thumbnail-handler" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies.windows] +version = "0.58" +features = [ + "implement", + "Win32_Foundation", + "Win32_System_Com", + "Win32_System_Com_StructuredStorage", + "Win32_Graphics_Gdi", + "Win32_Graphics_Imaging", + "Win32_UI_Shell", + "Win32_UI_Shell_PropertiesSystem", + "Storage_Streams", + "Data_Pdf", + "Foundation", +] + +[dependencies] +windows-core = "0.58" diff --git a/frontend/src-tauri/thumbnail-handler/README.md b/frontend/src-tauri/thumbnail-handler/README.md new file mode 100644 index 0000000000..3deffaab96 --- /dev/null +++ b/frontend/src-tauri/thumbnail-handler/README.md @@ -0,0 +1,61 @@ +# Windows PDF Thumbnail Handler + +A lightweight COM DLL that provides PDF page-preview thumbnails in Windows Explorer when Stirling-PDF is the default PDF application. + +## Why this exists + +When Stirling-PDF registers as the default PDF handler, Windows associates `.pdf` files with Stirling's ProgID. Without a thumbnail handler on that ProgID, Explorer falls back to showing the application icon (the big S logo) instead of a page preview. This DLL restores thumbnail previews by implementing the Windows Shell `IThumbnailProvider` COM interface. + +## How it works + +1. **Explorer requests a thumbnail** — when a folder with PDFs is opened in Medium/Large icon view, Explorer loads the DLL via the registered COM CLSID. +2. **Shell calls `IInitializeWithStream`** — passes the PDF file content as an `IStream`. +3. **Shell calls `IThumbnailProvider::GetThumbnail(cx)`** — requests a bitmap of size `cx × cx`. +4. **The DLL renders page 1** using the built-in `Windows.Data.Pdf` WinRT API (the same engine Edge uses), preserving aspect ratio. +5. **WIC decodes the rendered PNG** into BGRA pixels, which are copied into an `HBITMAP` via `CreateDIBSection`. +6. **Explorer displays the bitmap** as the file's thumbnail. + +All COM methods are wrapped in `catch_unwind` so a malformed PDF cannot crash Explorer. + +## Technical details + +| | | +|---|---| +| **Language** | Rust (cdylib) | +| **DLL size** | ~156 KB | +| **External deps** | None — uses only Windows built-in APIs | +| **PDF renderer** | `Windows.Data.Pdf` (WinRT, Windows 10+) | +| **Image decode** | WIC (`IWICImagingFactory`) with BGRA32 format conversion | +| **COM CLSID** | `{2D2FBE3A-9A88-4308-A52E-7EF63CA7CF48}` | +| **Threading model** | Apartment (STA — standard for shell extensions) | +| **Min Windows** | Windows 10 | + +## Registry entries (managed by MSI) + +The WiX installer (`provisioning.wxs`) registers: + +- **CLSID** at `HKLM\SOFTWARE\Classes\CLSID\{2D2FBE3A-...}\InprocServer32` pointing to the DLL +- **Shellex** at `HKLM\SOFTWARE\Classes\.pdf\shellex\{E357FCCD-...}` linking `.pdf` thumbnails to our CLSID + +Both are automatically removed on uninstall. + +## Building + +The DLL is built automatically as part of the Tauri build pipeline via `build-provisioner.mjs`: + +```bash +cd frontend +npm run tauri-build +``` + +To build the DLL standalone: + +```bash +cd frontend/src-tauri/thumbnail-handler +cargo build --release +# Output: target/release/stirling_thumbnail_handler.dll +``` + +## Linux / macOS + +This DLL is Windows-only. Linux and macOS don't need it — their thumbnail systems (thumbnailers on Linux, Quick Look on macOS) are decoupled from the default app association and continue working regardless of which app is set as default. diff --git a/frontend/src-tauri/thumbnail-handler/src/lib.rs b/frontend/src-tauri/thumbnail-handler/src/lib.rs new file mode 100644 index 0000000000..f75b10d030 --- /dev/null +++ b/frontend/src-tauri/thumbnail-handler/src/lib.rs @@ -0,0 +1,383 @@ +//! Stirling-PDF Windows Thumbnail Handler +//! +//! A lightweight COM DLL that implements IThumbnailProvider for PDF files. +//! Uses the built-in Windows.Data.Pdf WinRT API to render page 1 as a thumbnail. + +use std::cell::RefCell; +use std::ffi::c_void; +use std::panic::catch_unwind; +use std::sync::atomic::{AtomicU32, Ordering}; + +use windows::core::{implement, IUnknown, Interface, GUID, HRESULT}; +use windows::Win32::Foundation::{ + BOOL, CLASS_E_CLASSNOTAVAILABLE, CLASS_E_NOAGGREGATION, E_FAIL, E_UNEXPECTED, S_FALSE, S_OK, +}; +use windows::Win32::Graphics::Gdi::{ + CreateDIBSection, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, HBITMAP, +}; +use windows::Win32::Graphics::Imaging::{ + CLSID_WICImagingFactory, GUID_WICPixelFormat32bppBGRA, IWICImagingFactory, + WICBitmapDitherTypeNone, WICBitmapPaletteTypeCustom, WICDecodeMetadataCacheOnDemand, +}; +use windows::Win32::System::Com::{ + CoCreateInstance, IClassFactory, IClassFactory_Impl, IStream, CLSCTX_INPROC_SERVER, + STATFLAG_DEFAULT, STREAM_SEEK_SET, +}; +use windows::Win32::UI::Shell::{ + IThumbnailProvider, IThumbnailProvider_Impl, SHCreateMemStream, WTS_ALPHATYPE, +}; +use windows::Win32::UI::Shell::PropertiesSystem::{ + IInitializeWithStream, IInitializeWithStream_Impl, +}; + +// WinRT imports for PDF rendering +use windows::Data::Pdf::PdfDocument; +use windows::Storage::Streams::{DataWriter, InMemoryRandomAccessStream, IRandomAccessStream}; + +// CLSID for this thumbnail handler -- must match WiX registry entries +const CLSID_STIRLING_THUMBNAIL: GUID = GUID::from_u128(0x2d2fbe3a_9a88_4308_a52e_7ef63ca7cf48); + +static DLL_REF_COUNT: AtomicU32 = AtomicU32::new(0); + +// Maximum PDF size we'll attempt to thumbnail (256 MB) +const MAX_PDF_SIZE: usize = 256 * 1024 * 1024; + +// --------------------------------------------------------------------------- +// ThumbnailProvider -- the COM object +// --------------------------------------------------------------------------- + +#[implement(IThumbnailProvider, IInitializeWithStream)] +struct ThumbnailProvider { + stream: RefCell>, +} + +impl ThumbnailProvider { + fn new() -> Self { + DLL_REF_COUNT.fetch_add(1, Ordering::SeqCst); + Self { + stream: RefCell::new(None), + } + } +} + +impl Drop for ThumbnailProvider { + fn drop(&mut self) { + DLL_REF_COUNT.fetch_sub(1, Ordering::SeqCst); + } +} + +impl IInitializeWithStream_Impl for ThumbnailProvider_Impl { + fn Initialize( + &self, + pstream: Option<&IStream>, + _grfmode: u32, + ) -> windows::core::Result<()> { + let result = catch_unwind(std::panic::AssertUnwindSafe(|| { + *self.stream.borrow_mut() = pstream.cloned(); + })); + match result { + Ok(()) => Ok(()), + Err(_) => Err(E_UNEXPECTED.into()), + } + } +} + +impl IThumbnailProvider_Impl for ThumbnailProvider_Impl { + fn GetThumbnail( + &self, + cx: u32, + phbmp: *mut HBITMAP, + pdwalpha: *mut WTS_ALPHATYPE, + ) -> windows::core::Result<()> { + let result = catch_unwind(std::panic::AssertUnwindSafe(|| { + self.get_thumbnail_inner(cx, phbmp, pdwalpha) + })); + + match result { + Ok(inner) => inner, + Err(_) => Err(E_UNEXPECTED.into()), + } + } +} + +impl ThumbnailProvider_Impl { + fn get_thumbnail_inner( + &self, + cx: u32, + phbmp: *mut HBITMAP, + pdwalpha: *mut WTS_ALPHATYPE, + ) -> windows::core::Result<()> { + let stream = self.stream.borrow(); + let stream = stream.as_ref().ok_or(E_FAIL)?; + + // Step 1: Read the IStream into a byte buffer + let bytes = read_istream_to_vec(stream)?; + if bytes.is_empty() { + return Err(E_FAIL.into()); + } + + // Step 2: Load the PDF via WinRT + let winrt_stream = bytes_to_random_access_stream(&bytes)?; + let pdf_doc = PdfDocument::LoadFromStreamAsync(&winrt_stream)?.get()?; + + if pdf_doc.PageCount()? == 0 { + return Err(E_FAIL.into()); + } + + let page = pdf_doc.GetPage(0)?; + + // Step 3: Render page 1 to a PNG stream + let output_stream = InMemoryRandomAccessStream::new()?; + let render_options = windows::Data::Pdf::PdfPageRenderOptions::new()?; + + // Calculate dimensions preserving aspect ratio + let page_size = page.Size()?; + let scale = cx as f64 / f64::max(page_size.Width as f64, page_size.Height as f64); + let render_w = (page_size.Width as f64 * scale).max(1.0) as u32; + let render_h = (page_size.Height as f64 * scale).max(1.0) as u32; + + render_options.SetDestinationWidth(render_w)?; + render_options.SetDestinationHeight(render_h)?; + + page.RenderWithOptionsToStreamAsync(&output_stream, &render_options)? + .get()?; + + // Step 4: Decode the PNG using WIC -> raw BGRA pixels -> HBITMAP + let hbitmap = png_stream_to_hbitmap(&output_stream, render_w, render_h)?; + + // Step 5: Return the HBITMAP + unsafe { + *phbmp = hbitmap; + // WTSAT_ARGB = 2 + *pdwalpha = WTS_ALPHATYPE(2); + } + + Ok(()) + } +} + +// --------------------------------------------------------------------------- +// Helper: read IStream to Vec (with loop for short reads) +// --------------------------------------------------------------------------- + +fn read_istream_to_vec(stream: &IStream) -> windows::core::Result> { + unsafe { + // Get stream size + let mut stat = std::mem::zeroed(); + stream.Stat(&mut stat, STATFLAG_DEFAULT)?; + let size = stat.cbSize as usize; + + if size == 0 { + return Ok(Vec::new()); + } + if size > MAX_PDF_SIZE { + return Err(E_FAIL.into()); + } + + // Seek to beginning + stream.Seek(0, STREAM_SEEK_SET, None)?; + + // Read all bytes, looping for short reads + let mut buffer = vec![0u8; size]; + let mut total_read = 0usize; + while total_read < size { + let mut bytes_read = 0u32; + stream + .Read( + buffer[total_read..].as_mut_ptr() as *mut c_void, + (size - total_read) as u32, + Some(&mut bytes_read), + ) + .ok()?; + if bytes_read == 0 { + break; + } + total_read += bytes_read as usize; + } + buffer.truncate(total_read); + + Ok(buffer) + } +} + +// --------------------------------------------------------------------------- +// Helper: bytes -> WinRT IRandomAccessStream +// --------------------------------------------------------------------------- + +fn bytes_to_random_access_stream(bytes: &[u8]) -> windows::core::Result { + let mem_stream = InMemoryRandomAccessStream::new()?; + let writer = DataWriter::CreateDataWriter(&mem_stream)?; + writer.WriteBytes(bytes)?; + writer.StoreAsync()?.get()?; + // Detach the writer so it doesn't close the stream + writer.DetachStream()?; + + // Seek back to beginning + mem_stream.Seek(0)?; + + Ok(mem_stream.cast()?) +} + +// --------------------------------------------------------------------------- +// Helper: PNG stream -> HBITMAP via WIC (with format conversion to BGRA32) +// --------------------------------------------------------------------------- + +fn png_stream_to_hbitmap( + winrt_stream: &InMemoryRandomAccessStream, + width: u32, + height: u32, +) -> windows::core::Result { + unsafe { + // Seek to beginning and read PNG data + winrt_stream.Seek(0)?; + + let size = winrt_stream.Size()? as usize; + if size == 0 { + return Err(E_FAIL.into()); + } + + let reader = windows::Storage::Streams::DataReader::CreateDataReader( + &winrt_stream.GetInputStreamAt(0)?, + )?; + reader.LoadAsync(size as u32)?.get()?; + let mut png_bytes = vec![0u8; size]; + reader.ReadBytes(&mut png_bytes)?; + + // Create a COM IStream from the PNG bytes + let png_stream = SHCreateMemStream(Some(&png_bytes)).ok_or(E_FAIL)?; + + // Create WIC factory and decode the PNG + let wic_factory: IWICImagingFactory = + CoCreateInstance(&CLSID_WICImagingFactory, None, CLSCTX_INPROC_SERVER)?; + + let decoder = wic_factory.CreateDecoderFromStream( + &png_stream, + std::ptr::null(), + WICDecodeMetadataCacheOnDemand, + )?; + + let frame = decoder.GetFrame(0)?; + + // Convert to BGRA32 to ensure consistent pixel format + let converter = wic_factory.CreateFormatConverter()?; + converter.Initialize( + &frame, + &GUID_WICPixelFormat32bppBGRA, + WICBitmapDitherTypeNone, + None, + 0.0, + WICBitmapPaletteTypeCustom, + )?; + + // Read pixels as BGRA + let stride = width * 4; + let buf_size = (stride * height) as usize; + let mut pixels = vec![0u8; buf_size]; + converter.CopyPixels(std::ptr::null(), stride, &mut pixels)?; + + // Create a DIB section HBITMAP + let bmi = BITMAPINFO { + bmiHeader: BITMAPINFOHEADER { + biSize: std::mem::size_of::() as u32, + biWidth: width as i32, + biHeight: -(height as i32), // top-down + biPlanes: 1, + biBitCount: 32, + biCompression: BI_RGB.0, + biSizeImage: 0, + biXPelsPerMeter: 0, + biYPelsPerMeter: 0, + biClrUsed: 0, + biClrImportant: 0, + }, + bmiColors: [std::mem::zeroed()], + }; + + let mut bits: *mut c_void = std::ptr::null_mut(); + let hbitmap = CreateDIBSection(None, &bmi, DIB_RGB_COLORS, &mut bits, None, 0)?; + + if bits.is_null() { + return Err(E_FAIL.into()); + } + + // Copy pixel data into the DIB section + std::ptr::copy_nonoverlapping(pixels.as_ptr(), bits as *mut u8, buf_size); + + Ok(hbitmap) + } +} + +// --------------------------------------------------------------------------- +// ClassFactory +// --------------------------------------------------------------------------- + +#[implement(IClassFactory)] +struct ThumbnailProviderFactory; + +impl IClassFactory_Impl for ThumbnailProviderFactory_Impl { + fn CreateInstance( + &self, + punkouter: Option<&IUnknown>, + riid: *const GUID, + ppvobject: *mut *mut c_void, + ) -> windows::core::Result<()> { + unsafe { + *ppvobject = std::ptr::null_mut(); + } + + if punkouter.is_some() { + return Err(CLASS_E_NOAGGREGATION.into()); + } + + let provider = ThumbnailProvider::new(); + let unknown: IUnknown = provider.into(); + + unsafe { unknown.query(&*riid, ppvobject).ok() } + } + + fn LockServer(&self, flock: BOOL) -> windows::core::Result<()> { + if flock.as_bool() { + DLL_REF_COUNT.fetch_add(1, Ordering::SeqCst); + } else { + DLL_REF_COUNT.fetch_sub(1, Ordering::SeqCst); + } + Ok(()) + } +} + +// --------------------------------------------------------------------------- +// DLL exports +// --------------------------------------------------------------------------- + +#[no_mangle] +unsafe extern "system" fn DllGetClassObject( + rclsid: *const GUID, + riid: *const GUID, + ppv: *mut *mut c_void, +) -> HRESULT { + if ppv.is_null() { + return E_FAIL; + } + *ppv = std::ptr::null_mut(); + + if *rclsid != CLSID_STIRLING_THUMBNAIL { + return CLASS_E_CLASSNOTAVAILABLE; + } + + let factory = ThumbnailProviderFactory; + let unknown: IUnknown = factory.into(); + + match unknown.query(&*riid, ppv).ok() { + Ok(()) => S_OK, + Err(e) => e.into(), + } +} + +#[no_mangle] +extern "system" fn DllCanUnloadNow() -> HRESULT { + if DLL_REF_COUNT.load(Ordering::SeqCst) == 0 { + S_OK + } else { + S_FALSE + } +} diff --git a/frontend/src-tauri/windows/wix/provisioning.wxs b/frontend/src-tauri/windows/wix/provisioning.wxs index 2d908815be..7c6801a973 100644 --- a/frontend/src-tauri/windows/wix/provisioning.wxs +++ b/frontend/src-tauri/windows/wix/provisioning.wxs @@ -13,10 +13,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + - - {children} - + {children} ); } diff --git a/frontend/src/core/components/AppLayout.tsx b/frontend/src/core/components/AppLayout.tsx index 39de5dc650..ca26374345 100644 --- a/frontend/src/core/components/AppLayout.tsx +++ b/frontend/src/core/components/AppLayout.tsx @@ -1,5 +1,6 @@ -import { ReactNode } from 'react'; -import { useBanner } from '@app/contexts/BannerContext'; +import { ReactNode } from "react"; +import { useBanner } from "@app/contexts/BannerContext"; +import NavigationWarningModal from "@app/components/shared/NavigationWarningModal"; interface AppLayoutProps { children: ReactNode; @@ -20,12 +21,13 @@ export function AppLayout({ children }: AppLayoutProps) { height: 100% !important; } `} -
+
{banner} -
- {children} -
+
{children}
+ ); } diff --git a/frontend/src/core/components/AppProviders.tsx b/frontend/src/core/components/AppProviders.tsx index 75c7d281c3..c32b00c401 100644 --- a/frontend/src/core/components/AppProviders.tsx +++ b/frontend/src/core/components/AppProviders.tsx @@ -7,8 +7,16 @@ import { FilesModalProvider } from "@app/contexts/FilesModalContext"; import { ToolWorkflowProvider } from "@app/contexts/ToolWorkflowContext"; import { HotkeyProvider } from "@app/contexts/HotkeyContext"; import { SidebarProvider } from "@app/contexts/SidebarContext"; -import { PreferencesProvider, usePreferences } from "@app/contexts/PreferencesContext"; -import { AppConfigProvider, AppConfigProviderProps, AppConfigRetryOptions, useAppConfig } from "@app/contexts/AppConfigContext"; +import { + PreferencesProvider, + usePreferences, +} from "@app/contexts/PreferencesContext"; +import { + AppConfigProvider, + AppConfigProviderProps, + AppConfigRetryOptions, + useAppConfig, +} from "@app/contexts/AppConfigContext"; import { RightRailProvider } from "@app/contexts/RightRailContext"; import { ViewerProvider } from "@app/contexts/ViewerContext"; import { SignatureProvider } from "@app/contexts/SignatureContext"; @@ -20,8 +28,8 @@ import { BannerProvider } from "@app/contexts/BannerContext"; import ErrorBoundary from "@app/components/shared/ErrorBoundary"; import { useScarfTracking } from "@app/hooks/useScarfTracking"; import { useAppInitialization } from "@app/hooks/useAppInitialization"; -import { useLogoAssets } from '@app/hooks/useLogoAssets'; -import AppConfigLoader from '@app/components/shared/AppConfigLoader'; +import { useLogoAssets } from "@app/hooks/useLogoAssets"; +import AppConfigLoader from "@app/components/shared/AppConfigLoader"; import { RedactionProvider } from "@app/contexts/RedactionContext"; import { FormFillProvider } from "@app/tools/formFill/FormFillContext"; @@ -41,14 +49,14 @@ function BrandingAssetManager() { const { favicon, logo192, manifestHref } = useLogoAssets(); useEffect(() => { - if (typeof document === 'undefined') { + if (typeof document === "undefined") { return; } const setLinkHref = (selector: string, href: string) => { const link = document.querySelector(selector); - if (link && link.getAttribute('href') !== href) { - link.setAttribute('href', href); + if (link && link.getAttribute("href") !== href) { + link.setAttribute("href", href); } }; @@ -62,7 +70,10 @@ function BrandingAssetManager() { } // Avoid requirement to have props which are required in app providers anyway -type AppConfigProviderOverrides = Omit; +type AppConfigProviderOverrides = Omit< + AppConfigProviderProps, + "children" | "retryOptions" +>; export interface AppProvidersProps { children: ReactNode; @@ -79,7 +90,8 @@ function ServerDefaultsSync() { if (config) { const serverDefaults = { hideUnavailableTools: config.defaultHideUnavailableTools ?? false, - hideUnavailableConversions: config.defaultHideUnavailableConversions ?? false, + hideUnavailableConversions: + config.defaultHideUnavailableConversions ?? false, }; updateServerDefaults(serverDefaults); } @@ -92,33 +104,40 @@ function ServerDefaultsSync() { * Core application providers * Contains all providers needed for the core */ -export function AppProviders({ children, appConfigRetryOptions, appConfigProviderProps }: AppProvidersProps) { +export function AppProviders({ + children, + appConfigRetryOptions, + appConfigProviderProps, +}: AppProvidersProps) { return ( - + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -128,19 +147,19 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/frontend/src/core/components/FileManager.tsx b/frontend/src/core/components/FileManager.tsx index e3c9a84661..0b56ddbb6c 100644 --- a/frontend/src/core/components/FileManager.tsx +++ b/frontend/src/core/components/FileManager.tsx @@ -1,26 +1,34 @@ -import React, { useState, useCallback, useEffect, useMemo } from 'react'; -import { Modal } from '@mantine/core'; -import { Dropzone } from '@mantine/dropzone'; -import { StirlingFileStub } from '@app/types/fileContext'; -import { useFileManager } from '@app/hooks/useFileManager'; -import { useFilesModalContext } from '@app/contexts/FilesModalContext'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import { Tool } from '@app/types/tool'; -import MobileLayout from '@app/components/fileManager/MobileLayout'; -import DesktopLayout from '@app/components/fileManager/DesktopLayout'; -import DragOverlay from '@app/components/fileManager/DragOverlay'; -import { FileManagerProvider } from '@app/contexts/FileManagerContext'; -import { Z_INDEX_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; -import { isGoogleDriveConfigured, extractGoogleDriveBackendConfig } from '@app/services/googleDrivePickerService'; -import { loadScript } from '@app/utils/scriptLoader'; -import { useAllFiles } from '@app/contexts/FileContext'; +import React, { useState, useCallback, useEffect, useMemo } from "react"; +import { Modal } from "@mantine/core"; +import { Dropzone } from "@mantine/dropzone"; +import { StirlingFileStub } from "@app/types/fileContext"; +import { useFileManager } from "@app/hooks/useFileManager"; +import { useFilesModalContext } from "@app/contexts/FilesModalContext"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { Tool } from "@app/types/tool"; +import MobileLayout from "@app/components/fileManager/MobileLayout"; +import DesktopLayout from "@app/components/fileManager/DesktopLayout"; +import DragOverlay from "@app/components/fileManager/DragOverlay"; +import { FileManagerProvider } from "@app/contexts/FileManagerContext"; +import { Z_INDEX_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; +import { + isGoogleDriveConfigured, + extractGoogleDriveBackendConfig, +} from "@app/services/googleDrivePickerService"; +import { loadScript } from "@app/utils/scriptLoader"; +import { useAllFiles } from "@app/contexts/FileContext"; interface FileManagerProps { selectedTool?: Tool | null; } const FileManager: React.FC = ({ selectedTool }) => { - const { isFilesModalOpen, closeFilesModal, onFileUpload, onRecentFileSelect } = useFilesModalContext(); + const { + isFilesModalOpen, + closeFilesModal, + onFileUpload, + onRecentFileSelect, + } = useFilesModalContext(); const { config } = useAppConfig(); const [recentFiles, setRecentFiles] = useState([]); const [isDragging, setIsDragging] = useState(false); @@ -32,47 +40,59 @@ const FileManager: React.FC = ({ selectedTool }) => { const { fileIds: activeFileIds } = useAllFiles(); // File management handlers - const isFileSupported = useCallback((fileName: string) => { - if (!selectedTool?.supportedFormats) return true; - const extension = fileName.split('.').pop()?.toLowerCase(); - return selectedTool.supportedFormats.includes(extension || ''); - }, [selectedTool?.supportedFormats]); + const isFileSupported = useCallback( + (fileName: string) => { + if (!selectedTool?.supportedFormats) return true; + const extension = fileName.split(".").pop()?.toLowerCase(); + return selectedTool.supportedFormats.includes(extension || ""); + }, + [selectedTool?.supportedFormats], + ); const refreshRecentFiles = useCallback(async () => { const files = await loadRecentFiles(); setRecentFiles(files); }, [loadRecentFiles]); - const handleRecentFilesSelected = useCallback(async (files: StirlingFileStub[]) => { - try { - // Use StirlingFileStubs directly - preserves all metadata! - onRecentFileSelect(files); - } catch (error) { - console.error('Failed to process selected files:', error); - } - }, [onRecentFileSelect]); - - const handleNewFileUpload = useCallback(async (files: File[]) => { - if (files.length > 0) { + const handleRecentFilesSelected = useCallback( + async (files: StirlingFileStub[]) => { try { - // Files will get IDs assigned through onFilesSelect -> FileContext addFiles - onFileUpload(files); - await refreshRecentFiles(); + // Use StirlingFileStubs directly - preserves all metadata! + onRecentFileSelect(files); } catch (error) { - console.error('Failed to process dropped files:', error); + console.error("Failed to process selected files:", error); } - } - }, [onFileUpload, refreshRecentFiles]); + }, + [onRecentFileSelect], + ); - const handleRemoveFileByIndex = useCallback(async (index: number) => { - await handleRemoveFile(index, recentFiles, setRecentFiles); - }, [handleRemoveFile, recentFiles]); + const handleNewFileUpload = useCallback( + async (files: File[]) => { + if (files.length > 0) { + try { + // Files will get IDs assigned through onFilesSelect -> FileContext addFiles + onFileUpload(files); + await refreshRecentFiles(); + } catch (error) { + console.error("Failed to process dropped files:", error); + } + } + }, + [onFileUpload, refreshRecentFiles], + ); + + const handleRemoveFileByIndex = useCallback( + async (index: number) => { + await handleRemoveFile(index, recentFiles, setRecentFiles); + }, + [handleRemoveFile, recentFiles], + ); useEffect(() => { const checkMobile = () => setIsMobile(window.innerWidth < 1030); checkMobile(); - window.addEventListener('resize', checkMobile); - return () => window.removeEventListener('resize', checkMobile); + window.addEventListener("resize", checkMobile); + return () => window.removeEventListener("resize", checkMobile); }, []); useEffect(() => { @@ -89,7 +109,9 @@ const FileManager: React.FC = ({ selectedTool }) => { return () => { // StoredFileMetadata doesn't have blob URLs, so no cleanup needed // Blob URLs are managed by FileContext and tool operations - console.log('FileManager unmounting - FileContext handles blob URL cleanup'); + console.log( + "FileManager unmounting - FileContext handles blob URL cleanup", + ); }; }, []); @@ -97,7 +119,12 @@ const FileManager: React.FC = ({ selectedTool }) => { // Use useMemo to only track Google Drive config changes, not all config updates const googleDriveBackendConfig = useMemo( () => extractGoogleDriveBackendConfig(config), - [config?.googleDriveEnabled, config?.googleDriveClientId, config?.googleDriveApiKey, config?.googleDriveAppId] + [ + config?.googleDriveEnabled, + config?.googleDriveClientId, + config?.googleDriveApiKey, + config?.googleDriveAppId, + ], ); useEffect(() => { @@ -105,29 +132,29 @@ const FileManager: React.FC = ({ selectedTool }) => { // Load scripts in parallel without blocking Promise.all([ loadScript({ - src: 'https://apis.google.com/js/api.js', - id: 'gapi-script', + src: "https://apis.google.com/js/api.js", + id: "gapi-script", async: true, defer: true, }), loadScript({ - src: 'https://accounts.google.com/gsi/client', - id: 'gis-script', + src: "https://accounts.google.com/gsi/client", + id: "gis-script", async: true, defer: true, }), ]).catch((error) => { - console.warn('Failed to preload Google Drive scripts:', error); + console.warn("Failed to preload Google Drive scripts:", error); }); } }, [googleDriveBackendConfig]); // Modal size constants for consistent scaling - const modalHeight = '80vh'; - const modalWidth = isMobile ? '100%' : '80vw'; - const modalMaxWidth = isMobile ? '100%' : '1200px'; - const modalMaxHeight = '1200px'; - const modalMinWidth = isMobile ? '320px' : '800px'; + const modalHeight = "80vh"; + const modalWidth = isMobile ? "100%" : "80vw"; + const modalMaxWidth = isMobile ? "100%" : "1200px"; + const modalMaxHeight = "1200px"; + const modalMinWidth = isMobile ? "320px" : "800px"; return ( = ({ selectedTool }) => { zIndex={Z_INDEX_FILE_MANAGER_MODAL} styles={{ content: { - position: 'relative', - margin: isMobile ? '1rem' : '2rem' + position: "relative", + margin: isMobile ? "1rem" : "2rem", }, body: { padding: 0 }, - header: { display: 'none' } + header: { display: "none" }, }} > -
+
setIsDragging(true)} @@ -165,14 +194,14 @@ const FileManager: React.FC = ({ selectedTool }) => { multiple={true} activateOnClick={false} style={{ - height: '100%', - width: '100%', - border: 'none', - borderRadius: 'var(--radius-md)', - backgroundColor: 'var(--bg-file-manager)' + height: "100%", + width: "100%", + border: "none", + borderRadius: "var(--radius-md)", + backgroundColor: "var(--bg-file-manager)", }} styles={{ - inner: { pointerEvents: 'all' } + inner: { pointerEvents: "all" }, }} > = ({
- {t("fileManager.storage", "Storage")}: {formatFileSize(storageStats.used)} + {t("fileManager.storage", "Storage")}:{" "} + {formatFileSize(storageStats.used)} {storageStats.quota && ` / ${formatFileSize(storageStats.quota)}`} {storageStats.quota && ( 80 ? "red" : storageUsagePercent > 60 ? "yellow" : "blue"} + color={ + storageUsagePercent > 80 + ? "red" + : storageUsagePercent > 60 + ? "yellow" + : "blue" + } size="sm" mt={4} /> )} - {storageStats.fileCount} {t("fileManager.filesStored", "files stored")} + {storageStats.fileCount}{" "} + {t("fileManager.filesStored", "files stored")}
@@ -73,4 +81,4 @@ const StorageStatsCard: React.FC = ({ ); }; -export default StorageStatsCard; \ No newline at end of file +export default StorageStatsCard; diff --git a/frontend/src/core/components/annotation/providers/PDFAnnotationProvider.tsx b/frontend/src/core/components/annotation/providers/PDFAnnotationProvider.tsx index 0979d59e35..bbc6484f2e 100644 --- a/frontend/src/core/components/annotation/providers/PDFAnnotationProvider.tsx +++ b/frontend/src/core/components/annotation/providers/PDFAnnotationProvider.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, ReactNode } from 'react'; +import React, { createContext, useContext, ReactNode } from "react"; interface PDFAnnotationContextValue { // Drawing mode management @@ -26,7 +26,9 @@ interface PDFAnnotationContextValue { setSignatureConfig: (config: any | null) => void; } -const PDFAnnotationContext = createContext(undefined); +const PDFAnnotationContext = createContext< + PDFAnnotationContextValue | undefined +>(undefined); interface PDFAnnotationProviderProps { children: ReactNode; @@ -58,7 +60,7 @@ export const PDFAnnotationProvider: React.FC = ({ getImageData, isPlacementMode, signatureConfig, - setSignatureConfig + setSignatureConfig, }) => { const contextValue: PDFAnnotationContextValue = { activateDrawMode, @@ -72,7 +74,7 @@ export const PDFAnnotationProvider: React.FC = ({ getImageData, isPlacementMode, signatureConfig, - setSignatureConfig + setSignatureConfig, }; return ( @@ -85,7 +87,9 @@ export const PDFAnnotationProvider: React.FC = ({ export const usePDFAnnotation = (): PDFAnnotationContextValue => { const context = useContext(PDFAnnotationContext); if (context === undefined) { - throw new Error('usePDFAnnotation must be used within a PDFAnnotationProvider'); + throw new Error( + "usePDFAnnotation must be used within a PDFAnnotationProvider", + ); } return context; -}; \ No newline at end of file +}; diff --git a/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx b/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx index ea093b5be8..4c0862a012 100644 --- a/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx +++ b/frontend/src/core/components/annotation/shared/BaseAnnotationTool.tsx @@ -1,10 +1,10 @@ -import React, { useEffect, useState } from 'react'; -import { Stack, Alert, Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { DrawingControls } from '@app/components/annotation/shared/DrawingControls'; -import { ColorPicker } from '@app/components/annotation/shared/ColorPicker'; -import { usePDFAnnotation } from '@app/components/annotation/providers/PDFAnnotationProvider'; -import { useSignature } from '@app/contexts/SignatureContext'; +import React, { useEffect, useState } from "react"; +import { Stack, Alert, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { DrawingControls } from "@app/components/annotation/shared/DrawingControls"; +import { ColorPicker } from "@app/components/annotation/shared/ColorPicker"; +import { usePDFAnnotation } from "@app/components/annotation/providers/PDFAnnotationProvider"; +import { useSignature } from "@app/contexts/SignatureContext"; export interface AnnotationToolConfig { enableDrawing?: boolean; @@ -25,20 +25,19 @@ export const BaseAnnotationTool: React.FC = ({ config, children, onSignatureDataChange, - disabled = false + disabled = false, }) => { const { t } = useTranslation(); - const { - activateSignaturePlacementMode, - undo, - redo - } = usePDFAnnotation(); + const { activateSignaturePlacementMode, undo, redo } = usePDFAnnotation(); const { historyApiRef } = useSignature(); - const [selectedColor, setSelectedColor] = useState('#000000'); + const [selectedColor, setSelectedColor] = useState("#000000"); const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); const [signatureData, setSignatureData] = useState(null); - const [historyAvailability, setHistoryAvailability] = useState({ canUndo: false, canRedo: false }); + const [historyAvailability, setHistoryAvailability] = useState({ + canUndo: false, + canRedo: false, + }); const historyApiInstance = historyApiRef.current; useEffect(() => { @@ -81,7 +80,9 @@ export const BaseAnnotationTool: React.FC = ({ onRedo={redo} canUndo={historyAvailability.canUndo} canRedo={historyAvailability.canRedo} - onPlaceSignature={config.showPlaceButton ? handlePlaceSignature : undefined} + onPlaceSignature={ + config.showPlaceButton ? handlePlaceSignature : undefined + } hasSignatureData={!!signatureData} disabled={disabled} showPlaceButton={config.showPlaceButton} @@ -94,11 +95,14 @@ export const BaseAnnotationTool: React.FC = ({ signatureData, onSignatureDataChange: handleSignatureDataChange, onColorSwatchClick: () => setIsColorPickerOpen(true), - disabled + disabled, })} {/* Instructions for placing signature */} - + Click anywhere on the PDF to place your annotation. diff --git a/frontend/src/core/components/annotation/shared/ColorControl.tsx b/frontend/src/core/components/annotation/shared/ColorControl.tsx index 16b3f845bb..ff2a3ff458 100644 --- a/frontend/src/core/components/annotation/shared/ColorControl.tsx +++ b/frontend/src/core/components/annotation/shared/ColorControl.tsx @@ -1,15 +1,24 @@ -import { ActionIcon, Tooltip, Popover, Stack, ColorSwatch, ColorPicker as MantineColorPicker, Group } from '@mantine/core'; -import { useState, useCallback, useEffect } from 'react'; -import ColorizeIcon from '@mui/icons-material/Colorize'; +import { + ActionIcon, + Tooltip, + Popover, + Stack, + ColorSwatch, + ColorPicker as MantineColorPicker, + Group, +} from "@mantine/core"; +import { useState, useCallback, useEffect } from "react"; +import ColorizeIcon from "@mui/icons-material/Colorize"; // safari and firefox do not support the eye dropper API, only edge, chrome and opera do. // the button is hidden in the UI if the API is not supported. -const supportsEyeDropper = typeof window !== 'undefined' && 'EyeDropper' in window; +const supportsEyeDropper = + typeof window !== "undefined" && "EyeDropper" in window; interface EyeDropper { open(): Promise<{ sRGBHex: string }>; } -declare const EyeDropper: { new(): EyeDropper }; +declare const EyeDropper: { new (): EyeDropper }; interface ColorControlProps { value: string; @@ -18,13 +27,20 @@ interface ColorControlProps { disabled?: boolean; } -export function ColorControl({ value, onChange, label, disabled = false }: ColorControlProps) { +export function ColorControl({ + value, + onChange, + label, + disabled = false, +}: ColorControlProps) { const [opened, setOpened] = useState(false); // Buffer the colour locally so the picker stays responsive during drag. // Only propagate to the parent (which triggers expensive annotation updates) // on onChangeEnd (mouse-up / swatch click), preventing infinite re-render loops. const [localColor, setLocalColor] = useState(value); - useEffect(() => { setLocalColor(value); }, [value]); + useEffect(() => { + setLocalColor(value); + }, [value]); const handleEyeDropper = useCallback(async () => { if (!supportsEyeDropper) return; @@ -38,7 +54,13 @@ export function ColorControl({ value, onChange, label, disabled = false }: Color }, [onChange]); return ( - + - + diff --git a/frontend/src/core/components/annotation/shared/ColorPicker.tsx b/frontend/src/core/components/annotation/shared/ColorPicker.tsx index 21656b1f23..e12db7f340 100644 --- a/frontend/src/core/components/annotation/shared/ColorPicker.tsx +++ b/frontend/src/core/components/annotation/shared/ColorPicker.tsx @@ -1,6 +1,15 @@ -import React from 'react'; -import { Modal, Stack, ColorPicker as MantineColorPicker, Group, Button, ColorSwatch, Slider, Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; +import React from "react"; +import { + Modal, + Stack, + ColorPicker as MantineColorPicker, + Group, + Button, + ColorSwatch, + Slider, + Text, +} from "@mantine/core"; +import { useTranslation } from "react-i18next"; interface ColorPickerProps { isOpen: boolean; @@ -26,8 +35,9 @@ export const ColorPicker: React.FC = ({ opacityLabel, }) => { const { t } = useTranslation(); - const resolvedTitle = title ?? t('colorPicker.title', 'Choose colour'); - const resolvedOpacityLabel = opacityLabel ?? t('annotation.opacity', 'Opacity'); + const resolvedTitle = title ?? t("colorPicker.title", "Choose colour"); + const resolvedOpacityLabel = + opacityLabel ?? t("annotation.opacity", "Opacity"); return ( = ({ format="hex" value={selectedColor} onChange={onColorChange} - swatches={['#000000', '#0066cc', '#cc0000', '#cc6600', '#009900', '#6600cc']} + swatches={[ + "#000000", + "#0066cc", + "#cc0000", + "#cc6600", + "#009900", + "#6600cc", + ]} swatchesPerRow={6} size="lg" fullWidth /> {showOpacity && onOpacityChange && opacity !== undefined && ( - {resolvedOpacityLabel} + + {resolvedOpacityLabel} + )} - + @@ -83,14 +100,14 @@ interface ColorSwatchButtonProps { export const ColorSwatchButton: React.FC = ({ color, onClick, - size = 24 + size = 24, }) => { return ( ); diff --git a/frontend/src/core/components/annotation/shared/DrawingCanvas.tsx b/frontend/src/core/components/annotation/shared/DrawingCanvas.tsx index fd8864be26..de6348f624 100644 --- a/frontend/src/core/components/annotation/shared/DrawingCanvas.tsx +++ b/frontend/src/core/components/annotation/shared/DrawingCanvas.tsx @@ -1,10 +1,10 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { Paper, Button, Modal, Stack, Text, Group } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { ColorSwatchButton } from '@app/components/annotation/shared/ColorPicker'; -import PenSizeSelector from '@app/components/tools/sign/PenSizeSelector'; -import SignaturePad from 'signature_pad'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; +import React, { useEffect, useRef, useState } from "react"; +import { Paper, Button, Modal, Stack, Text, Group } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { ColorSwatchButton } from "@app/components/annotation/shared/ColorPicker"; +import PenSizeSelector from "@app/components/tools/sign/PenSizeSelector"; +import SignaturePad from "signature_pad"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; interface DrawingCanvasProps { selectedColor: string; @@ -47,7 +47,9 @@ export const DrawingCanvas: React.FC = ({ const modalCanvasRef = useRef(null); const padRef = useRef(null); const [modalOpen, setModalOpen] = useState(false); - const [savedSignatureData, setSavedSignatureData] = useState(null); + const [savedSignatureData, setSavedSignatureData] = useState( + null, + ); const initPad = (canvas: HTMLCanvasElement) => { if (!padRef.current) { @@ -68,7 +70,7 @@ export const DrawingCanvas: React.FC = ({ if (savedSignatureData) { const img = new Image(); img.onload = () => { - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); if (ctx) { ctx.drawImage(img, 0, 0, canvas.width, canvas.height); } @@ -92,13 +94,16 @@ export const DrawingCanvas: React.FC = ({ }, [autoOpen]); const trimCanvas = (canvas: HTMLCanvasElement): string => { - const ctx = canvas.getContext('2d'); - if (!ctx) return canvas.toDataURL('image/png'); + const ctx = canvas.getContext("2d"); + if (!ctx) return canvas.toDataURL("image/png"); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const pixels = imageData.data; - let minX = canvas.width, minY = canvas.height, maxX = 0, maxY = 0; + let minX = canvas.width, + minY = canvas.height, + maxX = 0, + maxY = 0; // Find bounds of non-transparent pixels for (let y = 0; y < canvas.height; y++) { @@ -117,27 +122,40 @@ export const DrawingCanvas: React.FC = ({ const trimHeight = maxY - minY + 1; // Create trimmed canvas - const trimmedCanvas = document.createElement('canvas'); + const trimmedCanvas = document.createElement("canvas"); trimmedCanvas.width = trimWidth; trimmedCanvas.height = trimHeight; - const trimmedCtx = trimmedCanvas.getContext('2d'); + const trimmedCtx = trimmedCanvas.getContext("2d"); if (trimmedCtx) { - trimmedCtx.drawImage(canvas, minX, minY, trimWidth, trimHeight, 0, 0, trimWidth, trimHeight); + trimmedCtx.drawImage( + canvas, + minX, + minY, + trimWidth, + trimHeight, + 0, + 0, + trimWidth, + trimHeight, + ); } - return trimmedCanvas.toDataURL('image/png'); + return trimmedCanvas.toDataURL("image/png"); }; const renderPreview = (dataUrl: string) => { const canvas = previewCanvasRef.current; if (!canvas) return; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); if (!ctx) return; const img = new Image(); img.onload = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); - const scale = Math.min(canvas.width / img.width, canvas.height / img.height); + const scale = Math.min( + canvas.width / img.width, + canvas.height / img.height, + ); const scaledWidth = img.width * scale; const scaledHeight = img.height * scale; const x = (canvas.width - scaledWidth) / 2; @@ -153,7 +171,7 @@ export const DrawingCanvas: React.FC = ({ const canvas = modalCanvasRef.current; if (canvas) { const trimmedPng = trimCanvas(canvas); - const untrimmedPng = canvas.toDataURL('image/png'); + const untrimmedPng = canvas.toDataURL("image/png"); setSavedSignatureData(untrimmedPng); // Save untrimmed for restoration onSignatureDataChange(trimmedPng); renderPreview(trimmedPng); @@ -176,9 +194,14 @@ export const DrawingCanvas: React.FC = ({ padRef.current.clear(); } if (previewCanvasRef.current) { - const ctx = previewCanvasRef.current.getContext('2d'); + const ctx = previewCanvasRef.current.getContext("2d"); if (ctx) { - ctx.clearRect(0, 0, previewCanvasRef.current.width, previewCanvasRef.current.height); + ctx.clearRect( + 0, + 0, + previewCanvasRef.current.width, + previewCanvasRef.current.height, + ); } } setSavedSignatureData(null); // Clear saved signature @@ -209,7 +232,7 @@ export const DrawingCanvas: React.FC = ({ useEffect(() => { const canvas = previewCanvasRef.current; if (!canvas) return; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); if (!ctx) return; if (!initialSignatureData) { @@ -227,33 +250,41 @@ export const DrawingCanvas: React.FC = ({ - {t('sign.canvas.heading', 'Draw your signature')} - + + {t("sign.canvas.heading", "Draw your signature")} + + - {t('sign.canvas.clickToOpen', 'Click to open the drawing canvas')} + {t("sign.canvas.clickToOpen", "Click to open the drawing canvas")} - + - {t('sign.canvas.colorLabel', 'Colour')} + {t("sign.canvas.colorLabel", "Colour")} = ({ - {t('sign.canvas.penSizeLabel', 'Pen size')} + {t("sign.canvas.penSizeLabel", "Pen size")} = ({ updatePenSize(size); }} onInputChange={onPenSizeInputChange} - placeholder={t('sign.canvas.penSizePlaceholder', 'Size')} + placeholder={t("sign.canvas.penSizePlaceholder", "Size")} size="compact-sm" - style={{ width: '80px' }} + style={{ width: "80px" }} /> @@ -286,26 +317,24 @@ export const DrawingCanvas: React.FC = ({ if (el) initPad(el); }} style={{ - border: '1px solid #ccc', - borderRadius: '4px', - display: 'block', - touchAction: 'none', - backgroundColor: 'white', - width: '100%', - maxWidth: '50rem', - height: '25rem', - cursor: 'crosshair', + border: "1px solid #ccc", + borderRadius: "4px", + display: "block", + touchAction: "none", + backgroundColor: "white", + width: "100%", + maxWidth: "50rem", + height: "25rem", + cursor: "crosshair", }} /> -
+
- +
diff --git a/frontend/src/core/components/annotation/shared/DrawingControls.tsx b/frontend/src/core/components/annotation/shared/DrawingControls.tsx index 3c28a594e0..7118a817e8 100644 --- a/frontend/src/core/components/annotation/shared/DrawingControls.tsx +++ b/frontend/src/core/components/annotation/shared/DrawingControls.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { Group, Button, ActionIcon, Tooltip } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { LocalIcon } from '@app/components/shared/LocalIcon'; +import React from "react"; +import { Group, Button, ActionIcon, Tooltip } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { LocalIcon } from "@app/components/shared/LocalIcon"; interface DrawingControlsProps { onUndo?: () => void; @@ -35,30 +35,40 @@ export const DrawingControls: React.FC = ({ return ( {onUndo && ( - + - + )} {onRedo && ( - + - + )} diff --git a/frontend/src/core/components/annotation/shared/ImageUploader.tsx b/frontend/src/core/components/annotation/shared/ImageUploader.tsx index ee2cdd123f..00802893c3 100644 --- a/frontend/src/core/components/annotation/shared/ImageUploader.tsx +++ b/frontend/src/core/components/annotation/shared/ImageUploader.tsx @@ -1,9 +1,9 @@ -import React, { useState } from 'react'; -import { FileInput, Text, Stack, Checkbox } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; -import { removeWhiteBackground } from '@app/utils/imageTransparency'; -import { alert } from '@app/components/toast'; +import React, { useState } from "react"; +import { FileInput, Text, Stack, Checkbox } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; +import { removeWhiteBackground } from "@app/utils/imageTransparency"; +import { alert } from "@app/components/toast"; interface ImageUploaderProps { onImageChange: (file: File | null) => void; @@ -22,29 +22,43 @@ export const ImageUploader: React.FC = ({ placeholder, hint, allowBackgroundRemoval = false, - onProcessedImageData + onProcessedImageData, }) => { const { t } = useTranslation(); const [removeBackground, setRemoveBackground] = useState(false); const [currentFile, setCurrentFile] = useState(null); - const [originalImageData, setOriginalImageData] = useState(null); + const [originalImageData, setOriginalImageData] = useState( + null, + ); const [isProcessing, setIsProcessing] = useState(false); - const processImage = async (imageSource: File | string, shouldRemoveBackground: boolean): Promise => { + const processImage = async ( + imageSource: File | string, + shouldRemoveBackground: boolean, + ): Promise => { if (shouldRemoveBackground && allowBackgroundRemoval) { setIsProcessing(true); try { - const transparentImageDataUrl = await removeWhiteBackground(imageSource, { - autoDetectCorner: true, - tolerance: 15 - }); + const transparentImageDataUrl = await removeWhiteBackground( + imageSource, + { + autoDetectCorner: true, + tolerance: 15, + }, + ); onProcessedImageData?.(transparentImageDataUrl); } catch (error) { - console.error('Error removing background:', error); + console.error("Error removing background:", error); alert({ - title: t('sign.image.backgroundRemovalFailedTitle', 'Background removal failed'), - body: t('sign.image.backgroundRemovalFailedMessage', 'Could not remove the background from the image. Using original image instead.'), - alertType: 'error' + title: t( + "sign.image.backgroundRemovalFailedTitle", + "Background removal failed", + ), + body: t( + "sign.image.backgroundRemovalFailedMessage", + "Could not remove the background from the image. Using original image instead.", + ), + alertType: "error", }); onProcessedImageData?.(null); } finally { @@ -52,7 +66,7 @@ export const ImageUploader: React.FC = ({ } } else { // When background removal is disabled, return the original image data - if (typeof imageSource === 'string') { + if (typeof imageSource === "string") { onProcessedImageData?.(imageSource); } else { // Convert File to data URL if needed @@ -69,8 +83,11 @@ export const ImageUploader: React.FC = ({ if (file && !disabled) { try { // Validate that it's actually an image file or SVG - if (!file.type.startsWith('image/') && !file.name.toLowerCase().endsWith('.svg')) { - console.error('Selected file is not an image or SVG'); + if ( + !file.type.startsWith("image/") && + !file.name.toLowerCase().endsWith(".svg") + ) { + console.error("Selected file is not an image or SVG"); return; } @@ -78,10 +95,12 @@ export const ImageUploader: React.FC = ({ onImageChange(file); let dataUrlToProcess: string; - + // Check if file is SVG - const isSvg = file.type === 'image/svg+xml' || file.name.toLowerCase().endsWith('.svg'); - + const isSvg = + file.type === "image/svg+xml" || + file.name.toLowerCase().endsWith(".svg"); + if (isSvg) { // For SVG, convert to PNG so it can be embedded in PDF dataUrlToProcess = await convertSvgToPng(file); @@ -98,7 +117,7 @@ export const ImageUploader: React.FC = ({ setOriginalImageData(dataUrlToProcess); await processImage(dataUrlToProcess, removeBackground); } catch (error) { - console.error('Error processing image file:', error); + console.error("Error processing image file:", error); } } else if (!file) { // Clear image data when no file is selected @@ -116,33 +135,41 @@ export const ImageUploader: React.FC = ({ reader.onload = async (e) => { try { const svgText = e.target?.result as string; - + // Parse SVG to get dimensions const parser = new DOMParser(); - const svgDoc = parser.parseFromString(svgText, 'image/svg+xml'); + const svgDoc = parser.parseFromString(svgText, "image/svg+xml"); const svgElement = svgDoc.documentElement; - + // Get SVG dimensions - let width = 800; // Default width + let width = 800; // Default width let height = 600; // Default height - - if (svgElement.hasAttribute('width') && svgElement.hasAttribute('height')) { - width = parseFloat(svgElement.getAttribute('width') || '800'); - height = parseFloat(svgElement.getAttribute('height') || '600'); - } else if (svgElement.hasAttribute('viewBox')) { - const viewBox = svgElement.getAttribute('viewBox')?.split(/\s+|,/); + + if ( + svgElement.hasAttribute("width") && + svgElement.hasAttribute("height") + ) { + width = parseFloat(svgElement.getAttribute("width") || "800"); + height = parseFloat(svgElement.getAttribute("height") || "600"); + } else if (svgElement.hasAttribute("viewBox")) { + const viewBox = svgElement.getAttribute("viewBox")?.split(/\s+|,/); if (viewBox && viewBox.length === 4) { width = parseFloat(viewBox[2]); height = parseFloat(viewBox[3]); } } - + // Ensure reasonable dimensions - if (width === 0 || height === 0 || !isFinite(width) || !isFinite(height)) { + if ( + width === 0 || + height === 0 || + !isFinite(width) || + !isFinite(height) + ) { width = 800; height = 600; } - + // Scale large SVGs down const maxDimension = 2048; if (width > maxDimension || height > maxDimension) { @@ -150,68 +177,75 @@ export const ImageUploader: React.FC = ({ width *= scale; height *= scale; } - - console.log('Converting SVG to PNG:', { width, height }); - + + console.log("Converting SVG to PNG:", { width, height }); + // Create an image element to render SVG const img = new Image(); - const blob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' }); + const blob = new Blob([svgText], { + type: "image/svg+xml;charset=utf-8", + }); const url = URL.createObjectURL(blob); - + img.onload = () => { try { // Use computed dimensions or image natural dimensions const finalWidth = img.naturalWidth || img.width || width; const finalHeight = img.naturalHeight || img.height || height; - - console.log('Image loaded:', { naturalWidth: img.naturalWidth, naturalHeight: img.naturalHeight, finalWidth, finalHeight }); - + + console.log("Image loaded:", { + naturalWidth: img.naturalWidth, + naturalHeight: img.naturalHeight, + finalWidth, + finalHeight, + }); + // Create canvas to convert to PNG - const canvas = document.createElement('canvas'); + const canvas = document.createElement("canvas"); canvas.width = finalWidth; canvas.height = finalHeight; - - const ctx = canvas.getContext('2d'); + + const ctx = canvas.getContext("2d"); if (!ctx) { URL.revokeObjectURL(url); - reject(new Error('Failed to get canvas context')); + reject(new Error("Failed to get canvas context")); return; } - + // Fill with white background (optional, for transparency support) - ctx.fillStyle = 'white'; + ctx.fillStyle = "white"; ctx.fillRect(0, 0, finalWidth, finalHeight); - + // Draw SVG ctx.drawImage(img, 0, 0, finalWidth, finalHeight); URL.revokeObjectURL(url); - + // Convert canvas to PNG data URL - const pngDataUrl = canvas.toDataURL('image/png'); - console.log('SVG converted to PNG successfully'); + const pngDataUrl = canvas.toDataURL("image/png"); + console.log("SVG converted to PNG successfully"); resolve(pngDataUrl); } catch (error) { URL.revokeObjectURL(url); - console.error('Error during canvas rendering:', error); + console.error("Error during canvas rendering:", error); reject(error); } }; - + img.onerror = (error) => { URL.revokeObjectURL(url); - console.error('Failed to load SVG image:', error); - reject(new Error('Failed to load SVG image')); + console.error("Failed to load SVG image:", error); + reject(new Error("Failed to load SVG image")); }; - + img.src = url; } catch (error) { - console.error('Error parsing SVG:', error); + console.error("Error parsing SVG:", error); reject(error); } }; - + reader.onerror = () => { - console.error('Error reading file:', reader.error); + console.error("Error reading file:", reader.error); reject(reader.error); }; reader.readAsText(file); @@ -231,7 +265,9 @@ export const ImageUploader: React.FC = ({ = ({ {allowBackgroundRemoval && ( handleBackgroundRemovalChange(event.currentTarget.checked)} + onChange={(event) => + handleBackgroundRemovalChange(event.currentTarget.checked) + } disabled={disabled || !currentFile || isProcessing} /> )} @@ -252,7 +293,7 @@ export const ImageUploader: React.FC = ({ )} {isProcessing && ( - {t('sign.image.processing', 'Processing image...')} + {t("sign.image.processing", "Processing image...")} )} diff --git a/frontend/src/core/components/annotation/shared/OpacityControl.tsx b/frontend/src/core/components/annotation/shared/OpacityControl.tsx index 27b1f10dd9..496b5c4070 100644 --- a/frontend/src/core/components/annotation/shared/OpacityControl.tsx +++ b/frontend/src/core/components/annotation/shared/OpacityControl.tsx @@ -1,7 +1,14 @@ -import { ActionIcon, Tooltip, Popover, Stack, Slider, Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { useState } from 'react'; -import OpacityIcon from '@mui/icons-material/Opacity'; +import { + ActionIcon, + Tooltip, + Popover, + Stack, + Slider, + Text, +} from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; +import OpacityIcon from "@mui/icons-material/Opacity"; interface OpacityControlProps { value: number; // 0-100 @@ -9,14 +16,18 @@ interface OpacityControlProps { disabled?: boolean; } -export function OpacityControl({ value, onChange, disabled = false }: OpacityControlProps) { +export function OpacityControl({ + value, + onChange, + disabled = false, +}: OpacityControlProps) { const { t } = useTranslation(); const [opened, setOpened] = useState(false); return ( - + - {t('annotation.opacity', 'Opacity')} + {t("annotation.opacity", "Opacity")} - {t('annotation.fontSize', 'Font size')} + {t("annotation.fontSize", "Font size")} - {t('annotation.opacity', 'Opacity')} + {t("annotation.opacity", "Opacity")} - {t('annotation.textAlignment', 'Text Alignment')} + {t("annotation.textAlignment", "Text Alignment")} onUpdate({ textAlign: 0 })} size="md" > onUpdate({ textAlign: 1 })} size="md" > onUpdate({ textAlign: 2 })} size="md" > @@ -125,7 +136,7 @@ export function PropertiesPopover({ {/* Opacity */}
- {t('annotation.opacity', 'Opacity')} + {t("annotation.opacity", "Opacity")}
- {t('annotation.strokeWidth', 'Stroke')} + {t("annotation.strokeWidth", "Stroke")}
@@ -188,7 +199,7 @@ export function PropertiesPopover({ return ( - + - {(annotationType === 'text' || annotationType === 'note') && renderTextNoteControls()} - {annotationType === 'shape' && renderShapeControls()} + {(annotationType === "text" || annotationType === "note") && + renderTextNoteControls()} + {annotationType === "shape" && renderShapeControls()} ); diff --git a/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx b/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx index 2385357171..a1a07a95b7 100644 --- a/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx +++ b/frontend/src/core/components/annotation/shared/TextInputWithFont.tsx @@ -1,7 +1,16 @@ -import React, { useState, useEffect } from 'react'; -import { Stack, TextInput, Select, Combobox, useCombobox, Group, Box, SegmentedControl } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { ColorPicker } from '@app/components/annotation/shared/ColorPicker'; +import React, { useState, useEffect } from "react"; +import { + Stack, + TextInput, + Select, + Combobox, + useCombobox, + Group, + Box, + SegmentedControl, +} from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { ColorPicker } from "@app/components/annotation/shared/ColorPicker"; interface TextInputWithFontProps { text: string; @@ -12,8 +21,8 @@ interface TextInputWithFontProps { onFontFamilyChange: (family: string) => void; textColor?: string; onTextColorChange?: (color: string) => void; - textAlign?: 'left' | 'center' | 'right'; - onTextAlignChange?: (align: 'left' | 'center' | 'right') => void; + textAlign?: "left" | "center" | "right"; + onTextAlignChange?: (align: "left" | "center" | "right") => void; disabled?: boolean; label: string; placeholder: string; @@ -31,9 +40,9 @@ export const TextInputWithFont: React.FC = ({ onFontSizeChange, fontFamily, onFontFamilyChange, - textColor = '#000000', + textColor = "#000000", onTextColorChange, - textAlign = 'left', + textAlign = "left", onTextAlignChange, disabled = false, label, @@ -42,7 +51,7 @@ export const TextInputWithFont: React.FC = ({ fontSizeLabel, fontSizePlaceholder, colorLabel, - onAnyChange + onAnyChange, }) => { const { t } = useTranslation(); const [fontSizeInput, setFontSizeInput] = useState(fontSize.toString()); @@ -61,14 +70,37 @@ export const TextInputWithFont: React.FC = ({ }, [textColor]); const fontOptions = [ - { value: 'Helvetica', label: 'Helvetica' }, - { value: 'Times-Roman', label: 'Times' }, - { value: 'Courier', label: 'Courier' }, - { value: 'Arial', label: 'Arial' }, - { value: 'Georgia', label: 'Georgia' }, + { value: "Helvetica", label: "Helvetica" }, + { value: "Times-Roman", label: "Times" }, + { value: "Courier", label: "Courier" }, + { value: "Arial", label: "Arial" }, + { value: "Georgia", label: "Georgia" }, ]; - const fontSizeOptions = ['8', '12', '16', '20', '24', '28', '32', '36', '40', '48', '56', '64', '72', '80', '96', '112', '128', '144', '160', '176', '192', '200']; + const fontSizeOptions = [ + "8", + "12", + "16", + "20", + "24", + "28", + "32", + "36", + "40", + "48", + "56", + "64", + "72", + "80", + "96", + "112", + "128", + "144", + "160", + "176", + "192", + "200", + ]; // Validate hex color const isValidHexColor = (color: string): boolean => { @@ -94,7 +126,7 @@ export const TextInputWithFont: React.FC = ({ label={fontLabel} value={fontFamily} onChange={(value) => { - onFontFamilyChange(value || 'Helvetica'); + onFontFamilyChange(value || "Helvetica"); onAnyChange?.(); }} data={fontOptions} @@ -187,7 +219,7 @@ export const TextInputWithFont: React.FC = ({ setColorInput(textColor); } }} - style={{ width: '100%' }} + style={{ width: "100%" }} rightSection={ !disabled && setIsColorPickerOpen(true)} @@ -195,9 +227,9 @@ export const TextInputWithFont: React.FC = ({ width: 24, height: 24, backgroundColor: textColor, - border: '1px solid #ccc', + border: "1px solid #ccc", borderRadius: 4, - cursor: disabled ? 'default' : 'pointer' + cursor: disabled ? "default" : "pointer", }} /> } @@ -224,14 +256,14 @@ export const TextInputWithFont: React.FC = ({ { - onTextAlignChange(value as 'left' | 'center' | 'right'); + onTextAlignChange(value as "left" | "center" | "right"); onAnyChange?.(); }} disabled={disabled} data={[ - { label: t('textAlign.left', 'Left'), value: 'left' }, - { label: t('textAlign.center', 'Center'), value: 'center' }, - { label: t('textAlign.right', 'Right'), value: 'right' }, + { label: t("textAlign.left", "Left"), value: "left" }, + { label: t("textAlign.center", "Center"), value: "center" }, + { label: t("textAlign.right", "Right"), value: "right" }, ]} /> )} diff --git a/frontend/src/core/components/annotation/shared/WidthControl.tsx b/frontend/src/core/components/annotation/shared/WidthControl.tsx index b99d35c996..d4f063f604 100644 --- a/frontend/src/core/components/annotation/shared/WidthControl.tsx +++ b/frontend/src/core/components/annotation/shared/WidthControl.tsx @@ -1,7 +1,14 @@ -import { ActionIcon, Tooltip, Popover, Stack, Slider, Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { useState } from 'react'; -import LineWeightIcon from '@mui/icons-material/LineWeight'; +import { + ActionIcon, + Tooltip, + Popover, + Stack, + Slider, + Text, +} from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; +import LineWeightIcon from "@mui/icons-material/LineWeight"; interface WidthControlProps { value: number; @@ -11,14 +18,20 @@ interface WidthControlProps { disabled?: boolean; } -export function WidthControl({ value, onChange, min, max, disabled = false }: WidthControlProps) { +export function WidthControl({ + value, + onChange, + min, + max, + disabled = false, +}: WidthControlProps) { const { t } = useTranslation(); const [opened, setOpened] = useState(false); return ( - + - {t('annotation.width', 'Width')} + {t("annotation.width", "Width")} void; @@ -10,16 +10,16 @@ interface DrawingToolProps { export const DrawingTool: React.FC = ({ onDrawingChange, - disabled = false + disabled = false, }) => { - const [selectedColor] = useState('#000000'); + const [selectedColor] = useState("#000000"); const [penSize, setPenSize] = useState(2); - const [penSizeInput, setPenSizeInput] = useState('2'); + const [penSizeInput, setPenSizeInput] = useState("2"); const toolConfig = { enableDrawing: true, showPlaceButton: true, - placeButtonText: "Place Drawing" + placeButtonText: "Place Drawing", }; return ( @@ -42,4 +42,4 @@ export const DrawingTool: React.FC = ({ ); -}; \ No newline at end of file +}; diff --git a/frontend/src/core/components/annotation/tools/ImageTool.tsx b/frontend/src/core/components/annotation/tools/ImageTool.tsx index 0704546965..1303e2a4c6 100644 --- a/frontend/src/core/components/annotation/tools/ImageTool.tsx +++ b/frontend/src/core/components/annotation/tools/ImageTool.tsx @@ -1,7 +1,7 @@ -import React, { useState } from 'react'; -import { Stack } from '@mantine/core'; -import { BaseAnnotationTool } from '@app/components/annotation/shared/BaseAnnotationTool'; -import { ImageUploader } from '@app/components/annotation/shared/ImageUploader'; +import React, { useState } from "react"; +import { Stack } from "@mantine/core"; +import { BaseAnnotationTool } from "@app/components/annotation/shared/BaseAnnotationTool"; +import { ImageUploader } from "@app/components/annotation/shared/ImageUploader"; interface ImageToolProps { onImageChange?: (data: string | null) => void; @@ -10,7 +10,7 @@ interface ImageToolProps { export const ImageTool: React.FC = ({ onImageChange, - disabled = false + disabled = false, }) => { const [, setImageData] = useState(null); @@ -23,7 +23,7 @@ export const ImageTool: React.FC = ({ if (e.target?.result) { resolve(e.target.result as string); } else { - reject(new Error('Failed to read file')); + reject(new Error("Failed to read file")); } }; reader.onerror = () => reject(reader.error); @@ -33,7 +33,7 @@ export const ImageTool: React.FC = ({ setImageData(result); onImageChange?.(result); } catch (error) { - console.error('Error reading file:', error); + console.error("Error reading file:", error); } } else if (!file) { setImageData(null); @@ -44,7 +44,7 @@ export const ImageTool: React.FC = ({ const toolConfig = { enableImageUpload: true, showPlaceButton: true, - placeButtonText: "Place Image" + placeButtonText: "Place Image", }; return ( @@ -64,4 +64,4 @@ export const ImageTool: React.FC = ({ ); -}; \ No newline at end of file +}; diff --git a/frontend/src/core/components/fileEditor/AddFileCard.tsx b/frontend/src/core/components/fileEditor/AddFileCard.tsx index c5cafd7561..7a566b032a 100644 --- a/frontend/src/core/components/fileEditor/AddFileCard.tsx +++ b/frontend/src/core/components/fileEditor/AddFileCard.tsx @@ -1,14 +1,14 @@ -import React, { useRef, useState } from 'react'; -import { Button, Group, useMantineColorScheme } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import AddIcon from '@mui/icons-material/Add'; -import { useFilesModalContext } from '@app/contexts/FilesModalContext'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { useLogoAssets } from '@app/hooks/useLogoAssets'; -import styles from '@app/components/fileEditor/FileEditor.module.css'; -import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; -import { useFileActionIcons } from '@app/hooks/useFileActionIcons'; -import { openFilesFromDisk } from '@app/services/openFilesFromDisk'; +import React, { useRef, useState } from "react"; +import { Button, Group } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import AddIcon from "@mui/icons-material/Add"; +import { useFilesModalContext } from "@app/contexts/FilesModalContext"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { Wordmark } from "@app/components/shared/Wordmark"; +import styles from "@app/components/fileEditor/FileEditor.module.css"; +import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology"; +import { useFileActionIcons } from "@app/hooks/useFileActionIcons"; +import { openFilesFromDisk } from "@app/services/openFilesFromDisk"; interface AddFileCardProps { onFileSelect: (files: File[]) => void; @@ -19,14 +19,12 @@ interface AddFileCardProps { const AddFileCard = ({ onFileSelect, accept, - multiple = true + multiple = true, }: AddFileCardProps) => { const { t } = useTranslation(); const fileInputRef = useRef(null); const { openFilesModal } = useFilesModalContext(); - const { colorScheme } = useMantineColorScheme(); const [isUploadHover, setIsUploadHover] = useState(false); - const { wordmark } = useLogoAssets(); const terminology = useFileActionTerminology(); const icons = useFileActionIcons(); @@ -38,7 +36,7 @@ const AddFileCard = ({ e.stopPropagation(); const files = await openFilesFromDisk({ multiple, - onFallbackOpen: () => fileInputRef.current?.click() + onFallbackOpen: () => fileInputRef.current?.click(), }); if (files.length > 0) { onFileSelect(files); @@ -56,7 +54,7 @@ const AddFileCard = ({ onFileSelect(files); } // Reset input so same files can be selected again - event.target.value = ''; + event.target.value = ""; }; return ( @@ -67,17 +65,17 @@ const AddFileCard = ({ accept={accept} multiple={multiple} onChange={handleFileChange} - style={{ display: 'none' }} + style={{ display: "none" }} />
{ - if (e.key === 'Enter' || e.key === ' ') { + if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleCardClick(); } @@ -86,10 +84,10 @@ const AddFileCard = ({ {/* Header bar - matches FileEditorThumbnail structure */}
- +
- {t('fileEditor.addFiles', 'Add Files')} + {t("fileEditor.addFiles", "Add Files")}
@@ -98,75 +96,83 @@ const AddFileCard = ({
{/* Stirling PDF Branding */} - Stirling PDF {/* Add Files + Native Upload Buttons - styled like LandingPage */}
setIsUploadHover(false)} > - ) : ( <> - {t('confirmCloseMessage', 'Are you sure you want to close this file?')} + + {t( + "confirmCloseMessage", + "Are you sure you want to close this file?", + )} + {file.name} - @@ -604,20 +751,20 @@ const FileEditorThumbnail = ({ setShowSharedEditNotice(false)} - title={t('fileManager.sharedEditNoticeTitle', 'Read-only server copy')} + title={t("fileManager.sharedEditNoticeTitle", "Read-only server copy")} centered size="auto" > {t( - 'fileManager.sharedEditNoticeBody', - 'You do not have edit rights to the server version of this file. Any edits you make will be saved as a local copy.' + "fileManager.sharedEditNoticeBody", + "You do not have edit rights to the server version of this file. Any edits you make will be saved as a local copy.", )} diff --git a/frontend/src/core/components/fileEditor/fileEditorRightRailButtons.tsx b/frontend/src/core/components/fileEditor/fileEditorRightRailButtons.tsx index 122023af6f..8c5383457f 100644 --- a/frontend/src/core/components/fileEditor/fileEditorRightRailButtons.tsx +++ b/frontend/src/core/components/fileEditor/fileEditorRightRailButtons.tsx @@ -1,7 +1,10 @@ -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useRightRailButtons, RightRailButtonWithAction } from '@app/hooks/useRightRailButtons'; -import LocalIcon from '@app/components/shared/LocalIcon'; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { + useRightRailButtons, + RightRailButtonWithAction, +} from "@app/hooks/useRightRailButtons"; +import LocalIcon from "@app/components/shared/LocalIcon"; interface FileEditorRightRailButtonsParams { totalItems: number; @@ -20,41 +23,67 @@ export function useFileEditorRightRailButtons({ }: FileEditorRightRailButtonsParams) { const { t, i18n } = useTranslation(); - const buttons = useMemo(() => [ - { - id: 'file-select-all', - icon: , - tooltip: t('rightRail.selectAll', 'Select All'), - ariaLabel: typeof t === 'function' ? t('rightRail.selectAll', 'Select All') : 'Select All', - section: 'top' as const, - order: 10, - disabled: totalItems === 0 || selectedCount === totalItems, - visible: totalItems > 0, - onClick: onSelectAll, - }, - { - id: 'file-deselect-all', - icon: , - tooltip: t('rightRail.deselectAll', 'Deselect All'), - ariaLabel: typeof t === 'function' ? t('rightRail.deselectAll', 'Deselect All') : 'Deselect All', - section: 'top' as const, - order: 20, - disabled: selectedCount === 0, - visible: totalItems > 0, - onClick: onDeselectAll, - }, - { - id: 'file-close-selected', - icon: , - tooltip: t('rightRail.closeSelected', 'Close Selected Files'), - ariaLabel: typeof t === 'function' ? t('rightRail.closeSelected', 'Close Selected Files') : 'Close Selected Files', - section: 'top' as const, - order: 30, - disabled: selectedCount === 0, - visible: totalItems > 0, - onClick: onCloseSelected, - }, - ], [t, i18n.language, totalItems, selectedCount, onSelectAll, onDeselectAll, onCloseSelected]); + const buttons = useMemo( + () => [ + { + id: "file-select-all", + icon: , + tooltip: t("rightRail.selectAll", "Select All"), + ariaLabel: + typeof t === "function" + ? t("rightRail.selectAll", "Select All") + : "Select All", + section: "top" as const, + order: 10, + disabled: totalItems === 0 || selectedCount === totalItems, + visible: totalItems > 0, + onClick: onSelectAll, + }, + { + id: "file-deselect-all", + icon: ( + + ), + tooltip: t("rightRail.deselectAll", "Deselect All"), + ariaLabel: + typeof t === "function" + ? t("rightRail.deselectAll", "Deselect All") + : "Deselect All", + section: "top" as const, + order: 20, + disabled: selectedCount === 0, + visible: totalItems > 0, + onClick: onDeselectAll, + }, + { + id: "file-close-selected", + icon: , + tooltip: t("rightRail.closeSelected", "Close Selected Files"), + ariaLabel: + typeof t === "function" + ? t("rightRail.closeSelected", "Close Selected Files") + : "Close Selected Files", + section: "top" as const, + order: 30, + disabled: selectedCount === 0, + visible: totalItems > 0, + onClick: onCloseSelected, + }, + ], + [ + t, + i18n.language, + totalItems, + selectedCount, + onSelectAll, + onDeselectAll, + onCloseSelected, + ], + ); useRightRailButtons(buttons); } diff --git a/frontend/src/core/components/fileManager/CompactFileDetails.tsx b/frontend/src/core/components/fileManager/CompactFileDetails.tsx index 5156dccff9..d8717ee7e3 100644 --- a/frontend/src/core/components/fileManager/CompactFileDetails.tsx +++ b/frontend/src/core/components/fileManager/CompactFileDetails.tsx @@ -1,12 +1,12 @@ -import React from 'react'; -import { Stack, Box, Text, Button, ActionIcon, Center } from '@mantine/core'; -import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; -import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import { useTranslation } from 'react-i18next'; -import { getFileSize } from '@app/utils/fileUtils'; -import { StirlingFileStub } from '@app/types/fileContext'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; +import React from "react"; +import { Stack, Box, Text, Button, ActionIcon, Center } from "@mantine/core"; +import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf"; +import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import { useTranslation } from "react-i18next"; +import { getFileSize } from "@app/utils/fileUtils"; +import { StirlingFileStub } from "@app/types/fileContext"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; interface CompactFileDetailsProps { currentFile: StirlingFileStub | null; @@ -29,47 +29,63 @@ const CompactFileDetails: React.FC = ({ isAnimating, onPrevious, onNext, - onOpenFiles + onOpenFiles, }) => { const { t } = useTranslation(); const hasSelection = selectedFiles.length > 0; const hasMultipleFiles = numberOfFiles > 1; const showOwner = Boolean( currentFile && - (currentFile.remoteOwnedByCurrentUser === false || currentFile.remoteSharedViaLink) + (currentFile.remoteOwnedByCurrentUser === false || + currentFile.remoteSharedViaLink), ); const ownerLabel = currentFile - ? currentFile.remoteOwnerUsername || t('fileManager.ownerUnknown', 'Unknown') - : ''; + ? currentFile.remoteOwnerUsername || + t("fileManager.ownerUnknown", "Unknown") + : ""; return ( - + {/* Compact mobile layout */} - + {/* Small preview */} - + {currentFile && thumbnail ? ( {currentFile.name} ) : currentFile ? ( -
- +
+
) : null} @@ -77,10 +93,12 @@ const CompactFileDetails: React.FC = ({ {/* File info */} - {currentFile ? currentFile.name : 'No file selected'} + + {currentFile ? currentFile.name : "No file selected"} + - {currentFile ? getFileSize(currentFile) : ''} + {currentFile ? getFileSize(currentFile) : ""} {selectedFiles.length > 1 && ` • ${selectedFiles.length} files`} {currentFile && ` • v${currentFile.versionNumber || 1}`} @@ -92,19 +110,21 @@ const CompactFileDetails: React.FC = ({ {/* Compact tool chain for mobile */} {currentFile?.toolHistory && currentFile.toolHistory.length > 0 && ( - {currentFile.toolHistory.map((tool) => t(`home.${tool.toolId}.title`, tool.toolId)).join(' → ')} + {currentFile.toolHistory + .map((tool) => t(`home.${tool.toolId}.title`, tool.toolId)) + .join(" → ")} )} {currentFile && showOwner && ( - {t('fileManager.owner', 'Owner')}: {ownerLabel} + {t("fileManager.owner", "Owner")}: {ownerLabel} )} {/* Navigation arrows for multiple files */} {hasMultipleFiles && ( - + = ({ disabled={!hasSelection} fullWidth style={{ - backgroundColor: hasSelection ? 'var(--btn-open-file)' : 'var(--mantine-color-gray-4)', - color: 'white' + backgroundColor: hasSelection + ? "var(--btn-open-file)" + : "var(--mantine-color-gray-4)", + color: "white", }} > {selectedFiles.length > 1 - ? t('fileManager.openFiles', `Open ${selectedFiles.length} Files`) - : t('fileManager.openFile', 'Open File') - } + ? t("fileManager.openFiles", `Open ${selectedFiles.length} Files`) + : t("fileManager.openFile", "Open File")} ); diff --git a/frontend/src/core/components/fileManager/DesktopLayout.tsx b/frontend/src/core/components/fileManager/DesktopLayout.tsx index 9926592c84..46376421fb 100644 --- a/frontend/src/core/components/fileManager/DesktopLayout.tsx +++ b/frontend/src/core/components/fileManager/DesktopLayout.tsx @@ -1,79 +1,97 @@ -import React from 'react'; -import { Grid } from '@mantine/core'; -import FileSourceButtons from '@app/components/fileManager/FileSourceButtons'; -import FileDetails from '@app/components/fileManager/FileDetails'; -import SearchInput from '@app/components/fileManager/SearchInput'; -import FileListArea from '@app/components/fileManager/FileListArea'; -import FileActions from '@app/components/fileManager/FileActions'; -import HiddenFileInput from '@app/components/fileManager/HiddenFileInput'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; +import React from "react"; +import { Grid } from "@mantine/core"; +import FileSourceButtons from "@app/components/fileManager/FileSourceButtons"; +import FileDetails from "@app/components/fileManager/FileDetails"; +import SearchInput from "@app/components/fileManager/SearchInput"; +import FileListArea from "@app/components/fileManager/FileListArea"; +import FileActions from "@app/components/fileManager/FileActions"; +import HiddenFileInput from "@app/components/fileManager/HiddenFileInput"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; const DesktopLayout: React.FC = () => { - const { - activeSource, - recentFiles, - modalHeight, - } = useFileManagerContext(); + const { activeSource, recentFiles, modalHeight } = useFileManagerContext(); return ( - + {/* Column 1: File Sources */} - + {/* Column 2: File List */} - -
- {activeSource === 'recent' && ( + +
+ {activeSource === "recent" && ( <> -
+
-
+
)} -
+
0 - ? `calc(${modalHeight} - 7rem)` - : '100%'} - scrollAreaStyle={{ - height: activeSource === 'recent' && recentFiles.length > 0 + scrollAreaHeight={ + activeSource === "recent" && recentFiles.length > 0 ? `calc(${modalHeight} - 7rem)` - : '100%', - backgroundColor: 'transparent', - border: 'none', - borderRadius: 0 + : "100%" + } + scrollAreaStyle={{ + height: + activeSource === "recent" && recentFiles.length > 0 + ? `calc(${modalHeight} - 7rem)` + : "100%", + backgroundColor: "transparent", + border: "none", + borderRadius: 0, }} />
@@ -81,14 +99,18 @@ const DesktopLayout: React.FC = () => { {/* Column 3: File Details */} - -
+ +
diff --git a/frontend/src/core/components/fileManager/DragOverlay.tsx b/frontend/src/core/components/fileManager/DragOverlay.tsx index 976bb940e9..023bb59d14 100644 --- a/frontend/src/core/components/fileManager/DragOverlay.tsx +++ b/frontend/src/core/components/fileManager/DragOverlay.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { Stack, Text, useMantineTheme, alpha } from '@mantine/core'; -import UploadFileIcon from '@mui/icons-material/UploadFile'; -import { useTranslation } from 'react-i18next'; +import React from "react"; +import { Stack, Text, useMantineTheme, alpha } from "@mantine/core"; +import UploadFileIcon from "@mui/icons-material/UploadFile"; +import { useTranslation } from "react-i18next"; interface DragOverlayProps { isVisible: boolean; @@ -16,29 +16,31 @@ const DragOverlay: React.FC = ({ isVisible }) => { return (
- + - {t('fileManager.dropFilesHere', 'Drop files here to upload')} + {t("fileManager.dropFilesHere", "Drop files here to upload")}
); }; -export default DragOverlay; \ No newline at end of file +export default DragOverlay; diff --git a/frontend/src/core/components/fileManager/EmptyFilesState.tsx b/frontend/src/core/components/fileManager/EmptyFilesState.tsx index 24e79fb120..777b80085c 100644 --- a/frontend/src/core/components/fileManager/EmptyFilesState.tsx +++ b/frontend/src/core/components/fileManager/EmptyFilesState.tsx @@ -1,19 +1,17 @@ -import React, { useState } from 'react'; -import { Button, Group, Text, Stack, useMantineColorScheme } from '@mantine/core'; -import HistoryIcon from '@mui/icons-material/History'; -import { useTranslation } from 'react-i18next'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { useLogoAssets } from '@app/hooks/useLogoAssets'; -import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; -import { useFileActionIcons } from '@app/hooks/useFileActionIcons'; +import React, { useState } from "react"; +import { Button, Group, Text, Stack } from "@mantine/core"; +import HistoryIcon from "@mui/icons-material/History"; +import { useTranslation } from "react-i18next"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { Wordmark } from "@app/components/shared/Wordmark"; +import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology"; +import { useFileActionIcons } from "@app/hooks/useFileActionIcons"; const EmptyFilesState: React.FC = () => { const { t } = useTranslation(); - const { colorScheme } = useMantineColorScheme(); const { onLocalFileClick } = useFileManagerContext(); const [isUploadHover, setIsUploadHover] = useState(false); - const { wordmark } = useLogoAssets(); const terminology = useFileActionTerminology(); const icons = useFileActionIcons(); @@ -24,81 +22,89 @@ const EmptyFilesState: React.FC = () => { return (
{/* Container */}
{/* No Recent Files Message */} - + - {t('fileManager.noRecentFiles', 'No recent files')} + {t("fileManager.noRecentFiles", "No recent files")} {/* Stirling PDF Logo */} - Stirling PDF {/* Upload Button */}
setIsUploadHover(false)} > ); diff --git a/frontend/src/core/components/fileManager/FileHistoryGroup.tsx b/frontend/src/core/components/fileManager/FileHistoryGroup.tsx index 75d8f0e50d..cda54edf71 100644 --- a/frontend/src/core/components/fileManager/FileHistoryGroup.tsx +++ b/frontend/src/core/components/fileManager/FileHistoryGroup.tsx @@ -1,8 +1,8 @@ -import React from 'react'; -import { Box, Text, Collapse, Group } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { StirlingFileStub } from '@app/types/fileContext'; -import FileListItem from '@app/components/fileManager/FileListItem'; +import React from "react"; +import { Box, Text, Collapse, Group } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { StirlingFileStub } from "@app/types/fileContext"; +import FileListItem from "@app/components/fileManager/FileListItem"; interface FileHistoryGroupProps { leafFile: StirlingFileStub; @@ -27,7 +27,7 @@ const FileHistoryGroup: React.FC = ({ // Sort history files by version number (oldest first, excluding the current leaf file) const sortedHistory = historyFiles - .filter(file => file.id !== leafFile.id) // Exclude the leaf file itself + .filter((file) => file.id !== leafFile.id) // Exclude the leaf file itself .sort((a, b) => (b.versionNumber || 1) - (a.versionNumber || 1)); if (!isExpanded || sortedHistory.length === 0) { @@ -39,7 +39,8 @@ const FileHistoryGroup: React.FC = ({ - {t('fileManager.fileHistory', 'File History')} ({sortedHistory.length}) + {t("fileManager.fileHistory", "File History")} ( + {sortedHistory.length}) diff --git a/frontend/src/core/components/fileManager/FileInfoCard.tsx b/frontend/src/core/components/fileManager/FileInfoCard.tsx index 182fcded9d..594442aefb 100644 --- a/frontend/src/core/components/fileManager/FileInfoCard.tsx +++ b/frontend/src/core/components/fileManager/FileInfoCard.tsx @@ -1,13 +1,23 @@ -import React, { useMemo, useState } from 'react'; -import { Stack, Card, Box, Text, Badge, Group, Divider, ScrollArea, Button } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { detectFileExtension, getFileSize } from '@app/utils/fileUtils'; -import { StirlingFileStub } from '@app/types/fileContext'; -import ToolChain from '@app/components/shared/ToolChain'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; -import ShareManagementModal from '@app/components/shared/ShareManagementModal'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; +import React, { useMemo, useState } from "react"; +import { + Stack, + Card, + Box, + Text, + Badge, + Group, + Divider, + ScrollArea, + Button, +} from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { detectFileExtension, getFileSize } from "@app/utils/fileUtils"; +import { StirlingFileStub } from "@app/types/fileContext"; +import ToolChain from "@app/components/shared/ToolChain"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; +import ShareManagementModal from "@app/components/shared/ShareManagementModal"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; interface FileInfoCardProps { currentFile: StirlingFileStub | null; @@ -16,7 +26,7 @@ interface FileInfoCardProps { const FileInfoCard: React.FC = ({ currentFile, - modalHeight + modalHeight, }) => { const { t } = useTranslation(); const { config } = useAppConfig(); @@ -24,57 +34,93 @@ const FileInfoCard: React.FC = ({ const [showShareManageModal, setShowShareManageModal] = useState(false); const isSharedWithYou = useMemo(() => { if (!currentFile) return false; - return currentFile.remoteOwnedByCurrentUser === false || currentFile.remoteSharedViaLink; + return ( + currentFile.remoteOwnedByCurrentUser === false || + currentFile.remoteSharedViaLink + ); }, [currentFile]); const isOwnedRemote = useMemo(() => { if (!currentFile) return false; - return Boolean(currentFile.remoteStorageId) && currentFile.remoteOwnedByCurrentUser !== false; + return ( + Boolean(currentFile.remoteStorageId) && + currentFile.remoteOwnedByCurrentUser !== false + ); }, [currentFile]); - const localUpdatedAt = currentFile?.createdAt ?? currentFile?.lastModified ?? 0; + const localUpdatedAt = + currentFile?.createdAt ?? currentFile?.lastModified ?? 0; const remoteUpdatedAt = currentFile?.remoteStorageUpdatedAt ?? 0; const isUploaded = Boolean(currentFile?.remoteStorageId); const isUpToDate = isUploaded && remoteUpdatedAt >= localUpdatedAt; const isOutOfSync = isUploaded && !isUpToDate && isOwnedRemote; - const isLocalOnly = !currentFile?.remoteStorageId && !currentFile?.remoteSharedViaLink; + const isLocalOnly = + !currentFile?.remoteStorageId && !currentFile?.remoteSharedViaLink; const isSharedByYou = useMemo(() => { if (!currentFile) return false; return isOwnedRemote && Boolean(currentFile.remoteHasShareLinks); }, [currentFile, isOwnedRemote]); const uploadEnabled = config?.storageEnabled === true; - const sharingEnabled = uploadEnabled && config?.storageSharingEnabled === true; + const sharingEnabled = + uploadEnabled && config?.storageSharingEnabled === true; const ownerLabel = useMemo(() => { - if (!currentFile) return ''; + if (!currentFile) return ""; if (currentFile.remoteOwnerUsername) { return currentFile.remoteOwnerUsername; } - return t('fileManager.ownerUnknown', 'Unknown'); + return t("fileManager.ownerUnknown", "Unknown"); }, [currentFile, t]); const lastSyncedLabel = useMemo(() => { - if (!currentFile?.remoteStorageUpdatedAt) return ''; + if (!currentFile?.remoteStorageUpdatedAt) return ""; return new Date(currentFile.remoteStorageUpdatedAt).toLocaleString(); }, [currentFile?.remoteStorageUpdatedAt]); return ( - - + + - {t('fileManager.details', 'File Details')} + {t("fileManager.details", "File Details")} - {t('fileManager.fileName', 'Name')} + + {t("fileManager.fileName", "Name")} + - - {currentFile ? currentFile.name : ''} + + {currentFile ? currentFile.name : ""} - {t('fileManager.fileFormat', 'Format')} + + {t("fileManager.fileFormat", "Format")} + {currentFile ? ( {detectFileExtension(currentFile.name).toUpperCase()} @@ -86,38 +132,54 @@ const FileInfoCard: React.FC = ({ - {t('fileManager.fileSize', 'Size')} + + {t("fileManager.fileSize", "Size")} + - {currentFile ? getFileSize(currentFile) : ''} + {currentFile ? getFileSize(currentFile) : ""} - {t('fileManager.lastModified', 'Last modified')} + + {t("fileManager.lastModified", "Last modified")} + - {currentFile ? new Date(currentFile.lastModified).toLocaleDateString() : ''} + {currentFile + ? new Date(currentFile.lastModified).toLocaleDateString() + : ""} - {t('fileManager.fileVersion', 'Version')} - {currentFile && - - v{currentFile ? (currentFile.versionNumber || 1) : ''} - } - + + {t("fileManager.fileVersion", "Version")} + + {currentFile && ( + + v{currentFile ? currentFile.versionNumber || 1 : ""} + + )} {sharingEnabled && isSharedWithYou && ( <> - {t('fileManager.owner', 'Owner')} + + {t("fileManager.owner", "Owner")} + - {ownerLabel} + + {ownerLabel} + - {t('fileManager.sharedWithYou', 'Shared with you')} + {t("fileManager.sharedWithYou", "Shared with you")} @@ -129,7 +191,9 @@ const FileInfoCard: React.FC = ({ <> - {t('fileManager.toolChain', 'Tools Applied')} + + {t("fileManager.toolChain", "Tools Applied")} + = ({ onClick={() => onMakeCopy(currentFile)} fullWidth > - {t('fileManager.makeCopy', 'Make a copy')} + {t("fileManager.makeCopy", "Make a copy")} )} @@ -157,30 +221,41 @@ const FileInfoCard: React.FC = ({ <> - {t('fileManager.cloudFile', 'Cloud file')} + + {t("fileManager.cloudFile", "Cloud file")} + {uploadEnabled && isOutOfSync ? ( - {t('fileManager.changesNotUploaded', 'Changes not uploaded')} + {t( + "fileManager.changesNotUploaded", + "Changes not uploaded", + )} ) : uploadEnabled ? ( - {t('fileManager.synced', 'Synced')} + {t("fileManager.synced", "Synced")} ) : null} {lastSyncedLabel && ( - {t('fileManager.lastSynced', 'Last synced')} - {lastSyncedLabel} + + {t("fileManager.lastSynced", "Last synced")} + + + {lastSyncedLabel} + )} {isSharedByYou && sharingEnabled && ( <> - {t('fileManager.sharing', 'Sharing')} + + {t("fileManager.sharing", "Sharing")} + - {t('fileManager.sharedByYou', 'Shared by you')} + {t("fileManager.sharedByYou", "Shared by you")} )} @@ -199,9 +274,16 @@ const FileInfoCard: React.FC = ({ <> - {t('fileManager.storageState', 'Storage')} - - {t('fileManager.localOnly', 'Local only')} + + {t("fileManager.storageState", "Storage")} + + + {t("fileManager.localOnly", "Local only")} diff --git a/frontend/src/core/components/fileManager/FileListArea.tsx b/frontend/src/core/components/fileManager/FileListArea.tsx index 1964dad542..467ac398e1 100644 --- a/frontend/src/core/components/fileManager/FileListArea.tsx +++ b/frontend/src/core/components/fileManager/FileListArea.tsx @@ -1,11 +1,11 @@ -import React from 'react'; -import { Center, ScrollArea, Text, Stack } from '@mantine/core'; -import CloudIcon from '@mui/icons-material/Cloud'; -import { useTranslation } from 'react-i18next'; -import FileListItem from '@app/components/fileManager/FileListItem'; -import FileHistoryGroup from '@app/components/fileManager/FileHistoryGroup'; -import EmptyFilesState from '@app/components/fileManager/EmptyFilesState'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; +import React from "react"; +import { Center, ScrollArea, Text, Stack } from "@mantine/core"; +import CloudIcon from "@mui/icons-material/Cloud"; +import { useTranslation } from "react-i18next"; +import FileListItem from "@app/components/fileManager/FileListItem"; +import FileHistoryGroup from "@app/components/fileManager/FileHistoryGroup"; +import EmptyFilesState from "@app/components/fileManager/EmptyFilesState"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; interface FileListAreaProps { scrollAreaHeight: string; @@ -34,12 +34,12 @@ const FileListArea: React.FC = ({ } = useFileManagerContext(); const { t } = useTranslation(); - if (activeSource === 'recent') { + if (activeSource === "recent") { return ( = ({ {recentFiles.length === 0 && !isLoading ? ( ) : recentFiles.length === 0 && isLoading ? ( -
- {t('fileManager.loadingFiles', 'Loading files...')} +
+ + {t("fileManager.loadingFiles", "Loading files...")} +
) : ( filteredFiles.map((file, index) => { @@ -93,10 +95,17 @@ const FileListArea: React.FC = ({ // Google Drive placeholder return ( -
+
- - {t('fileManager.googleDriveNotAvailable', 'Google Drive integration coming soon')} + + + {t( + "fileManager.googleDriveNotAvailable", + "Google Drive integration coming soon", + )} +
); diff --git a/frontend/src/core/components/fileManager/FileListItem.tsx b/frontend/src/core/components/fileManager/FileListItem.tsx index 73ae8bd2a2..88b2b4e58b 100644 --- a/frontend/src/core/components/fileManager/FileListItem.tsx +++ b/frontend/src/core/components/fileManager/FileListItem.tsx @@ -1,31 +1,40 @@ -import React, { useCallback, useMemo, useState } from 'react'; -import { Group, Box, Text, ActionIcon, Checkbox, Divider, Menu, Badge } from '@mantine/core'; -import MoreVertIcon from '@mui/icons-material/MoreVert'; -import DeleteIcon from '@mui/icons-material/Delete'; -import DownloadIcon from '@mui/icons-material/Download'; -import HistoryIcon from '@mui/icons-material/History'; -import RestoreIcon from '@mui/icons-material/Restore'; -import UnarchiveIcon from '@mui/icons-material/Unarchive'; -import CloseIcon from '@mui/icons-material/Close'; -import CloudUploadIcon from '@mui/icons-material/CloudUpload'; -import CloudDoneIcon from '@mui/icons-material/CloudDone'; -import LinkIcon from '@mui/icons-material/Link'; -import { useTranslation } from 'react-i18next'; -import { getFileSize, getFileDate } from '@app/utils/fileUtils'; -import { FileId, StirlingFileStub } from '@app/types/fileContext'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; -import { zipFileService } from '@app/services/zipFileService'; -import ToolChain from '@app/components/shared/ToolChain'; -import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; -import { useFileManagement } from '@app/contexts/FileContext'; -import UploadToServerModal from '@app/components/shared/UploadToServerModal'; -import ShareFileModal from '@app/components/shared/ShareFileModal'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import ShareManagementModal from '@app/components/shared/ShareManagementModal'; -import apiClient from '@app/services/apiClient'; -import { absoluteWithBasePath } from '@app/constants/app'; -import { alert } from '@app/components/toast'; +import React, { useCallback, useMemo, useState } from "react"; +import { + Group, + Box, + Text, + ActionIcon, + Checkbox, + Divider, + Menu, + Badge, +} from "@mantine/core"; +import MoreVertIcon from "@mui/icons-material/MoreVert"; +import DeleteIcon from "@mui/icons-material/Delete"; +import DownloadIcon from "@mui/icons-material/Download"; +import HistoryIcon from "@mui/icons-material/History"; +import RestoreIcon from "@mui/icons-material/Restore"; +import UnarchiveIcon from "@mui/icons-material/Unarchive"; +import CloseIcon from "@mui/icons-material/Close"; +import CloudUploadIcon from "@mui/icons-material/CloudUpload"; +import CloudDoneIcon from "@mui/icons-material/CloudDone"; +import LinkIcon from "@mui/icons-material/Link"; +import { useTranslation } from "react-i18next"; +import { getFileSize, getFileDate } from "@app/utils/fileUtils"; +import { FileId, StirlingFileStub } from "@app/types/fileContext"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; +import { zipFileService } from "@app/services/zipFileService"; +import ToolChain from "@app/components/shared/ToolChain"; +import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; +import { useFileManagement } from "@app/contexts/FileContext"; +import UploadToServerModal from "@app/components/shared/UploadToServerModal"; +import ShareFileModal from "@app/components/shared/ShareFileModal"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import ShareManagementModal from "@app/components/shared/ShareManagementModal"; +import apiClient from "@app/services/apiClient"; +import { absoluteWithBasePath } from "@app/constants/app"; +import { alert } from "@app/components/toast"; interface FileListItemProps { file: StirlingFileStub; @@ -51,7 +60,7 @@ const FileListItem: React.FC = ({ onDoubleClick, isHistoryFile = false, isLatestVersion = false, - isActive = false + isActive = false, }) => { const [isHovered, setIsHovered] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -60,70 +69,96 @@ const FileListItem: React.FC = ({ const [showShareManageModal, setShowShareManageModal] = useState(false); const { t } = useTranslation(); const { config } = useAppConfig(); - const {expandedFileIds, onToggleExpansion, onUnzipFile, refreshRecentFiles } = useFileManagerContext(); + const { + expandedFileIds, + onToggleExpansion, + onUnzipFile, + refreshRecentFiles, + } = useFileManagerContext(); const { removeFiles } = useFileManagement(); // Check if this is a ZIP file const isZipFile = zipFileService.isZipFileStub(file); // Check file extension - const extLower = (file.name?.match(/\.([a-z0-9]+)$/i)?.[1] || '').toLowerCase(); - const isCBZ = extLower === 'cbz'; - const isCBR = extLower === 'cbr'; + const extLower = ( + file.name?.match(/\.([a-z0-9]+)$/i)?.[1] || "" + ).toLowerCase(); + const isCBZ = extLower === "cbz"; + const isCBR = extLower === "cbr"; // Keep item in hovered state if menu is open const shouldShowHovered = isHovered || isMenuOpen; // Get version information for this file - const leafFileId = (isLatestVersion ? file.id : (file.originalFileId || file.id)) as FileId; + const leafFileId = ( + isLatestVersion ? file.id : file.originalFileId || file.id + ) as FileId; const hasVersionHistory = (file.versionNumber || 1) > 1; // Show history for any processed file (v2+) const currentVersion = file.versionNumber || 1; // Display original files as v1 const isExpanded = expandedFileIds.has(leafFileId); const uploadEnabled = config?.storageEnabled === true; - const sharingEnabled = uploadEnabled && config?.storageSharingEnabled === true; - const shareLinksEnabled = sharingEnabled && config?.storageShareLinksEnabled === true; + const sharingEnabled = + uploadEnabled && config?.storageSharingEnabled === true; + const shareLinksEnabled = + sharingEnabled && config?.storageShareLinksEnabled === true; const isOwnedOrLocal = file.remoteOwnedByCurrentUser !== false; const isSharedWithYou = - sharingEnabled && (file.remoteOwnedByCurrentUser === false || file.remoteSharedViaLink); + sharingEnabled && + (file.remoteOwnedByCurrentUser === false || file.remoteSharedViaLink); const localUpdatedAt = file.createdAt ?? file.lastModified ?? 0; const remoteUpdatedAt = file.remoteStorageUpdatedAt ?? 0; const isUploaded = Boolean(file.remoteStorageId); const isUpToDate = isUploaded && remoteUpdatedAt >= localUpdatedAt; const isOutOfSync = isUploaded && !isUpToDate && isOwnedOrLocal; const isLocalOnly = !file.remoteStorageId && !file.remoteSharedViaLink; - const accessRole = (isOwnedOrLocal ? 'editor' : (file.remoteAccessRole ?? 'viewer')).toLowerCase(); - const hasReadAccess = isOwnedOrLocal || accessRole === 'editor' || accessRole === 'commenter' || accessRole === 'viewer'; - const canUpload = uploadEnabled && isOwnedOrLocal && isLatestVersion && (!isUploaded || !isUpToDate); + const accessRole = ( + isOwnedOrLocal ? "editor" : (file.remoteAccessRole ?? "viewer") + ).toLowerCase(); + const hasReadAccess = + isOwnedOrLocal || + accessRole === "editor" || + accessRole === "commenter" || + accessRole === "viewer"; + const canUpload = + uploadEnabled && + isOwnedOrLocal && + isLatestVersion && + (!isUploaded || !isUpToDate); const canShare = shareLinksEnabled && isOwnedOrLocal && isLatestVersion; - const canManageShare = sharingEnabled && isOwnedOrLocal && Boolean(file.remoteStorageId); + const canManageShare = + sharingEnabled && isOwnedOrLocal && Boolean(file.remoteStorageId); const canCopyShareLink = - shareLinksEnabled && Boolean(file.remoteHasShareLinks) && Boolean(file.remoteStorageId); + shareLinksEnabled && + Boolean(file.remoteHasShareLinks) && + Boolean(file.remoteStorageId); const canDownloadFile = Boolean(onDownload) && hasReadAccess; const shareBaseUrl = useMemo(() => { - const frontendUrl = (config?.frontendUrl || '').trim(); + const frontendUrl = (config?.frontendUrl || "").trim(); if (frontendUrl) { - const normalized = frontendUrl.endsWith('/') + const normalized = frontendUrl.endsWith("/") ? frontendUrl.slice(0, -1) : frontendUrl; return `${normalized}/share/`; } - return absoluteWithBasePath('/share/'); + return absoluteWithBasePath("/share/"); }, [config?.frontendUrl]); const handleCopyShareLink = useCallback(async () => { if (!file.remoteStorageId) return; try { - const response = await apiClient.get<{ shareLinks?: Array<{ token?: string }> }>( - `/api/v1/storage/files/${file.remoteStorageId}`, - { suppressErrorToast: true } as any - ); + const response = await apiClient.get<{ + shareLinks?: Array<{ token?: string }>; + }>(`/api/v1/storage/files/${file.remoteStorageId}`, { + suppressErrorToast: true, + } as any); const links = response.data?.shareLinks ?? []; const token = links[links.length - 1]?.token; if (!token) { alert({ - alertType: 'warning', - title: t('storageShare.noLinks', 'No active share links yet.'), + alertType: "warning", + title: t("storageShare.noLinks", "No active share links yet."), expandable: false, durationMs: 2500, }); @@ -131,16 +166,16 @@ const FileListItem: React.FC = ({ } await navigator.clipboard.writeText(`${shareBaseUrl}${token}`); alert({ - alertType: 'success', - title: t('storageShare.copied', 'Link copied to clipboard'), + alertType: "success", + title: t("storageShare.copied", "Link copied to clipboard"), expandable: false, durationMs: 2000, }); } catch (error) { - console.error('Failed to copy share link:', error); + console.error("Failed to copy share link:", error); alert({ - alertType: 'warning', - title: t('storageShare.copyFailed', 'Copy failed'), + alertType: "warning", + title: t("storageShare.copyFailed", "Copy failed"), expandable: false, durationMs: 2500, }); @@ -152,22 +187,28 @@ const FileListItem: React.FC = ({ onSelect(e.shiftKey)} + onClick={ + isHistoryFile || isActive ? undefined : (e) => onSelect(e.shiftKey) + } onDoubleClick={onDoubleClick} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} @@ -186,8 +227,8 @@ const FileListItem: React.FC = ({ color={isActive ? "green" : undefined} styles={{ input: { - cursor: isActive ? 'not-allowed' : 'pointer' - } + cursor: isActive ? "not-allowed" : "pointer", + }, }} /> @@ -203,12 +244,12 @@ const FileListItem: React.FC = ({ size="xs" variant="light" style={{ - backgroundColor: 'var(--file-active-badge-bg)', - color: 'var(--file-active-badge-fg)', - border: '1px solid var(--file-active-badge-border)' + backgroundColor: "var(--file-active-badge-bg)", + color: "var(--file-active-badge-fg)", + border: "1px solid var(--file-active-badge-border)", }} > - {t('fileManager.active', 'Active')} + {t("fileManager.active", "Active")} )} @@ -216,18 +257,26 @@ const FileListItem: React.FC = ({ {sharingEnabled && isSharedWithYou ? ( - {t('fileManager.sharedWithYou', 'Shared with you')} + {t("fileManager.sharedWithYou", "Shared with you")} ) : null} - {sharingEnabled && isSharedWithYou && accessRole && accessRole !== 'editor' ? ( + {sharingEnabled && + isSharedWithYou && + accessRole && + accessRole !== "editor" ? ( - {accessRole === 'commenter' - ? t('storageShare.roleCommenter', 'Commenter') - : t('storageShare.roleViewer', 'Viewer')} + {accessRole === "commenter" + ? t("storageShare.roleCommenter", "Commenter") + : t("storageShare.roleViewer", "Viewer")} ) : isLocalOnly ? ( - - {t('fileManager.localOnly', 'Local only')} + + {t("fileManager.localOnly", "Local only")} ) : uploadEnabled && isOutOfSync ? ( = ({ color="yellow" leftSection={} > - {t('fileManager.changesNotUploaded', 'Changes not uploaded')} + {t("fileManager.changesNotUploaded", "Changes not uploaded")} ) : uploadEnabled && isUploaded ? ( = ({ color="teal" leftSection={} > - {t('fileManager.synced', 'Synced')} + {t("fileManager.synced", "Synced")} ) : null} - {sharingEnabled && file.remoteOwnedByCurrentUser !== false && file.remoteHasShareLinks && ( - - {t('fileManager.sharedByYou', 'Shared by you')} - - )} - + {sharingEnabled && + file.remoteOwnedByCurrentUser !== false && + file.remoteHasShareLinks && ( + + {t("fileManager.sharedByYou", "Shared by you")} + + )} @@ -264,7 +314,7 @@ const FileListItem: React.FC = ({ {file.toolHistory && file.toolHistory.length > 0 && ( @@ -288,9 +338,9 @@ const FileListItem: React.FC = ({ onClick={(e) => e.stopPropagation()} style={{ opacity: shouldShowHovered ? 1 : 0, - transform: shouldShowHovered ? 'scale(1)' : 'scale(0.8)', - transition: 'opacity 0.3s ease, transform 0.3s ease', - pointerEvents: shouldShowHovered ? 'auto' : 'none' + transform: shouldShowHovered ? "scale(1)" : "scale(0.8)", + transition: "opacity 0.3s ease, transform 0.3s ease", + pointerEvents: shouldShowHovered ? "auto" : "none", }} > @@ -308,7 +358,7 @@ const FileListItem: React.FC = ({ removeFiles([file.id]); }} > - {t('fileManager.closeFile', 'Close File')} + {t("fileManager.closeFile", "Close File")} @@ -322,7 +372,7 @@ const FileListItem: React.FC = ({ onDownload?.(); }} > - {t('fileManager.download', 'Download')} + {t("fileManager.download", "Download")} )} @@ -335,8 +385,8 @@ const FileListItem: React.FC = ({ }} > {isUploaded - ? t('fileManager.updateOnServer', 'Update on Server') - : t('fileManager.uploadToServer', 'Upload to Server')} + ? t("fileManager.updateOnServer", "Update on Server") + : t("fileManager.uploadToServer", "Upload to Server")} )} @@ -348,7 +398,7 @@ const FileListItem: React.FC = ({ setShowShareModal(true); }} > - {t('fileManager.share', 'Share')} + {t("fileManager.share", "Share")} )} @@ -360,7 +410,7 @@ const FileListItem: React.FC = ({ void handleCopyShareLink(); }} > - {t('storageShare.copyLink', 'Copy share link')} + {t("storageShare.copyLink", "Copy share link")} )} @@ -372,7 +422,7 @@ const FileListItem: React.FC = ({ setShowShareManageModal(true); }} > - {t('storageShare.manage', 'Manage sharing')} + {t("storageShare.manage", "Manage sharing")} )} @@ -380,20 +430,15 @@ const FileListItem: React.FC = ({ {isLatestVersion && hasVersionHistory && ( <> - } + leftSection={} onClick={(e) => { e.stopPropagation(); onToggleExpansion(leafFileId); }} > - { - (isExpanded ? - t('fileManager.hideHistory', 'Hide History') : - t('fileManager.showHistory', 'Show History') - ) - } + {isExpanded + ? t("fileManager.hideHistory", "Hide History") + : t("fileManager.showHistory", "Show History")} @@ -408,7 +453,7 @@ const FileListItem: React.FC = ({ e.stopPropagation(); }} > - {t('fileManager.restore', 'Restore')} + {t("fileManager.restore", "Restore")} @@ -424,7 +469,7 @@ const FileListItem: React.FC = ({ onUnzipFile(file); }} > - {t('fileManager.unzip', 'Unzip')} + {t("fileManager.unzip", "Unzip")} @@ -437,14 +482,13 @@ const FileListItem: React.FC = ({ onRemove(); }} > - {t('fileManager.delete', 'Delete')} + {t("fileManager.delete", "Delete")} - - { } + {} {canUpload && ( = ({ disabled }) => ( src="/images/google-drive.svg" alt="Google Drive" style={{ - width: '20px', - height: '20px', + width: "20px", + height: "20px", opacity: disabled ? 0.5 : 1, - filter: disabled ? 'grayscale(100%)' : 'none', + filter: disabled ? "grayscale(100%)" : "none", }} /> ); const FileSourceButtons: React.FC = ({ - horizontal = false + horizontal = false, }) => { - const { activeSource, onSourceChange, onLocalFileClick, onGoogleDriveSelect, onNewFilesSelect } = useFileManagerContext(); + const { + activeSource, + onSourceChange, + onLocalFileClick, + onGoogleDriveSelect, + onNewFilesSelect, + } = useFileManagerContext(); const { t } = useTranslation(); - const { isEnabled: isGoogleDriveEnabled, openPicker: openGoogleDrivePicker } = useGoogleDrivePicker(); + const { isEnabled: isGoogleDriveEnabled, openPicker: openGoogleDrivePicker } = + useGoogleDrivePicker(); const terminology = useFileActionTerminology(); const icons = useFileActionIcons(); const UploadIcon = icons.upload; @@ -53,7 +60,7 @@ const FileSourceButtons: React.FC = ({ onGoogleDriveSelect(files); } } catch (error) { - console.error('Failed to pick files from Google Drive:', error); + console.error("Failed to pick files from Google Drive:", error); } }; @@ -68,24 +75,32 @@ const FileSourceButtons: React.FC = ({ }; // Determine visibility of Google Drive button - const shouldHideGoogleDrive = !isGoogleDriveEnabled && config?.hideDisabledToolsGoogleDrive; + const shouldHideGoogleDrive = + !isGoogleDriveEnabled && config?.hideDisabledToolsGoogleDrive; // Determine visibility of Mobile QR Scanner button - const shouldHideMobileQR = !isMobileUploadEnabled && config?.hideDisabledToolsMobileQRScanner; + const shouldHideMobileQR = + !isMobileUploadEnabled && config?.hideDisabledToolsMobileQRScanner; const buttonProps = { - variant: (source: string) => activeSource === source ? 'filled' : 'subtle', - getColor: (source: string) => activeSource === source ? 'var(--mantine-color-gray-2)' : undefined, + variant: (source: string) => + activeSource === source ? "filled" : "subtle", + getColor: (source: string) => + activeSource === source ? "var(--mantine-color-gray-2)" : undefined, getStyles: (source: string) => ({ root: { - backgroundColor: activeSource === source ? undefined : 'transparent', - color: activeSource === source ? 'var(--mantine-color-gray-9)' : 'var(--mantine-color-gray-6)', - border: 'none', - '&:hover': { - backgroundColor: activeSource === source ? undefined : 'var(--mantine-color-gray-0)' - } - } - }) + backgroundColor: activeSource === source ? undefined : "transparent", + color: + activeSource === source + ? "var(--mantine-color-gray-9)" + : "var(--mantine-color-gray-6)", + border: "none", + "&:hover": { + backgroundColor: + activeSource === source ? undefined : "var(--mantine-color-gray-0)", + }, + }, + }), }; const buttons = ( @@ -93,18 +108,20 @@ const FileSourceButtons: React.FC = ({ )} {!shouldHideMobileQR && ( )} @@ -178,7 +217,7 @@ const FileSourceButtons: React.FC = ({ if (horizontal) { return ( <> - + {buttons} = ({ return ( <> - - - {t('fileManager.myFiles', 'My Files')} + + + {t("fileManager.myFiles", "My Files")} {buttons} diff --git a/frontend/src/core/components/fileManager/HiddenFileInput.tsx b/frontend/src/core/components/fileManager/HiddenFileInput.tsx index 27482df519..fce23187cd 100644 --- a/frontend/src/core/components/fileManager/HiddenFileInput.tsx +++ b/frontend/src/core/components/fileManager/HiddenFileInput.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; +import React from "react"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; const HiddenFileInput: React.FC = () => { const { fileInputRef, onFileInputChange } = useFileManagerContext(); @@ -10,7 +10,7 @@ const HiddenFileInput: React.FC = () => { type="file" multiple={true} onChange={onFileInputChange} - style={{ display: 'none' }} + style={{ display: "none" }} data-testid="file-input" /> ); diff --git a/frontend/src/core/components/fileManager/MobileLayout.tsx b/frontend/src/core/components/fileManager/MobileLayout.tsx index 0701874852..2545a9d604 100644 --- a/frontend/src/core/components/fileManager/MobileLayout.tsx +++ b/frontend/src/core/components/fileManager/MobileLayout.tsx @@ -1,19 +1,15 @@ -import React from 'react'; -import { Box } from '@mantine/core'; -import FileSourceButtons from '@app/components/fileManager/FileSourceButtons'; -import FileDetails from '@app/components/fileManager/FileDetails'; -import SearchInput from '@app/components/fileManager/SearchInput'; -import FileListArea from '@app/components/fileManager/FileListArea'; -import FileActions from '@app/components/fileManager/FileActions'; -import HiddenFileInput from '@app/components/fileManager/HiddenFileInput'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; +import React from "react"; +import { Box } from "@mantine/core"; +import FileSourceButtons from "@app/components/fileManager/FileSourceButtons"; +import FileDetails from "@app/components/fileManager/FileDetails"; +import SearchInput from "@app/components/fileManager/SearchInput"; +import FileListArea from "@app/components/fileManager/FileListArea"; +import FileActions from "@app/components/fileManager/FileActions"; +import HiddenFileInput from "@app/components/fileManager/HiddenFileInput"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; const MobileLayout: React.FC = () => { - const { - activeSource, - selectedFiles, - modalHeight, - } = useFileManagerContext(); + const { activeSource, selectedFiles, modalHeight } = useFileManagerContext(); // Calculate the height more accurately based on actual content const calculateFileListHeight = () => { @@ -21,17 +17,21 @@ const MobileLayout: React.FC = () => { const baseHeight = `calc(${modalHeight} - 2rem)`; // Account for Stack padding // Estimate heights of fixed components - const fileSourceHeight = '3rem'; // FileSourceButtons height - const fileDetailsHeight = selectedFiles.length > 0 ? '10rem' : '8rem'; // FileDetails compact height - const fileActionsHeight = activeSource === 'recent' ? '3rem' : '0rem'; // FileActions height (now at bottom) - const searchHeight = activeSource === 'recent' ? '3rem' : '0rem'; // SearchInput height - const gapHeight = activeSource === 'recent' ? '3.75rem' : '2rem'; // Stack gaps + const fileSourceHeight = "3rem"; // FileSourceButtons height + const fileDetailsHeight = selectedFiles.length > 0 ? "10rem" : "8rem"; // FileDetails compact height + const fileActionsHeight = activeSource === "recent" ? "3rem" : "0rem"; // FileActions height (now at bottom) + const searchHeight = activeSource === "recent" ? "3rem" : "0rem"; // SearchInput height + const gapHeight = activeSource === "recent" ? "3.75rem" : "2rem"; // Stack gaps return `calc(${baseHeight} - ${fileSourceHeight} - ${fileDetailsHeight} - ${fileActionsHeight} - ${searchHeight} - ${gapHeight})`; }; return ( - + {/* Section 1: File Sources - Fixed at top */} @@ -42,28 +42,34 @@ const MobileLayout: React.FC = () => { {/* Section 3 & 4: Search Bar + File List - Unified background extending to modal edge */} - - {activeSource === 'recent' && ( + + {activeSource === "recent" && ( <> - + - + @@ -74,11 +80,11 @@ const MobileLayout: React.FC = () => { scrollAreaHeight={calculateFileListHeight()} scrollAreaStyle={{ height: calculateFileListHeight(), - maxHeight: '60vh', - minHeight: '9.375rem', - backgroundColor: 'transparent', - border: 'none', - borderRadius: 0 + maxHeight: "60vh", + minHeight: "9.375rem", + backgroundColor: "transparent", + border: "none", + borderRadius: 0, }} /> diff --git a/frontend/src/core/components/fileManager/SearchInput.tsx b/frontend/src/core/components/fileManager/SearchInput.tsx index 2b318604c6..b7dbf9306c 100644 --- a/frontend/src/core/components/fileManager/SearchInput.tsx +++ b/frontend/src/core/components/fileManager/SearchInput.tsx @@ -1,8 +1,8 @@ -import React from 'react'; -import { TextInput } from '@mantine/core'; -import SearchIcon from '@mui/icons-material/Search'; -import { useTranslation } from 'react-i18next'; -import { useFileManagerContext } from '@app/contexts/FileManagerContext'; +import React from "react"; +import { TextInput } from "@mantine/core"; +import SearchIcon from "@mui/icons-material/Search"; +import { useTranslation } from "react-i18next"; +import { useFileManagerContext } from "@app/contexts/FileManagerContext"; interface SearchInputProps { style?: React.CSSProperties; @@ -14,20 +14,19 @@ const SearchInput: React.FC = ({ style }) => { return ( } value={searchTerm} onChange={(e) => onSearchChange(e.target.value)} - - style={{ padding: '0.5rem', ...style }} + style={{ padding: "0.5rem", ...style }} styles={{ input: { - border: 'none', - backgroundColor: 'transparent' - } + border: "none", + backgroundColor: "transparent", + }, }} /> ); }; -export default SearchInput; \ No newline at end of file +export default SearchInput; diff --git a/frontend/src/core/components/hotkeys/HotkeyDisplay.tsx b/frontend/src/core/components/hotkeys/HotkeyDisplay.tsx index 7f6d9f26dd..5141499989 100644 --- a/frontend/src/core/components/hotkeys/HotkeyDisplay.tsx +++ b/frontend/src/core/components/hotkeys/HotkeyDisplay.tsx @@ -1,29 +1,33 @@ -import React from 'react'; -import { HotkeyBinding } from '@app/utils/hotkeys'; -import { useHotkeys } from '@app/contexts/HotkeyContext'; +import React from "react"; +import { HotkeyBinding } from "@app/utils/hotkeys"; +import { useHotkeys } from "@app/contexts/HotkeyContext"; interface HotkeyDisplayProps { binding: HotkeyBinding | null | undefined; - size?: 'sm' | 'md'; + size?: "sm" | "md"; muted?: boolean; } const baseKeyStyle: React.CSSProperties = { - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - borderRadius: '0.375rem', - background: 'var(--mantine-color-gray-1)', - border: '1px solid var(--mantine-color-gray-3)', - padding: '0.125rem 0.35rem', - fontSize: '0.75rem', + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + borderRadius: "0.375rem", + background: "var(--mantine-color-gray-1)", + border: "1px solid var(--mantine-color-gray-3)", + padding: "0.125rem 0.35rem", + fontSize: "0.75rem", lineHeight: 1, - fontFamily: 'var(--mantine-font-family-monospace, monospace)', - minWidth: '1.35rem', - color: 'var(--mantine-color-text)', + fontFamily: "var(--mantine-font-family-monospace, monospace)", + minWidth: "1.35rem", + color: "var(--mantine-color-text)", }; -export const HotkeyDisplay: React.FC = ({ binding, size = 'sm', muted = false }) => { +export const HotkeyDisplay: React.FC = ({ + binding, + size = "sm", + muted = false, +}) => { const { getDisplayParts } = useHotkeys(); const parts = getDisplayParts(binding); @@ -31,24 +35,29 @@ export const HotkeyDisplay: React.FC = ({ binding, size = 's return null; } - const keyStyle = size === 'md' - ? { ...baseKeyStyle, fontSize: '0.85rem', padding: '0.2rem 0.5rem' } - : baseKeyStyle; + const keyStyle = + size === "md" + ? { ...baseKeyStyle, fontSize: "0.85rem", padding: "0.2rem 0.5rem" } + : baseKeyStyle; return ( {parts.map((part, index) => ( {part} - {index < parts.length - 1 && +} + {index < parts.length - 1 && ( + + + + + )} ))} diff --git a/frontend/src/core/components/layout/Workbench.tsx b/frontend/src/core/components/layout/Workbench.tsx index 521ac5db15..97ec71c85e 100644 --- a/frontend/src/core/components/layout/Workbench.tsx +++ b/frontend/src/core/components/layout/Workbench.tsx @@ -1,24 +1,28 @@ -import { useCallback } from 'react'; -import { Box } from '@mantine/core'; -import { useRainbowThemeContext } from '@app/components/shared/RainbowThemeProvider'; -import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; -import { useFileHandler } from '@app/hooks/useFileHandler'; -import { useFileState, useFileActions } from '@app/contexts/FileContext'; -import { useNavigationState, useNavigationActions, useNavigationGuard } from '@app/contexts/NavigationContext'; -import { isBaseWorkbench } from '@app/types/workbench'; -import { useViewer } from '@app/contexts/ViewerContext'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import { FileId } from '@app/types/file'; -import styles from '@app/components/layout/Workbench.module.css'; +import { useCallback } from "react"; +import { Box } from "@mantine/core"; +import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider"; +import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext"; +import { useFileHandler } from "@app/hooks/useFileHandler"; +import { useFileState, useFileActions } from "@app/contexts/FileContext"; +import { + useNavigationState, + useNavigationActions, + useNavigationGuard, +} from "@app/contexts/NavigationContext"; +import { isBaseWorkbench } from "@app/types/workbench"; +import { useViewer } from "@app/contexts/ViewerContext"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { FileId } from "@app/types/file"; +import styles from "@app/components/layout/Workbench.module.css"; -import TopControls from '@app/components/shared/TopControls'; -import FileEditor from '@app/components/fileEditor/FileEditor'; -import PageEditor from '@app/components/pageEditor/PageEditor'; -import PageEditorControls from '@app/components/pageEditor/PageEditorControls'; -import Viewer from '@app/components/viewer/Viewer'; -import LandingPage from '@app/components/shared/LandingPage'; -import Footer from '@app/components/shared/Footer'; -import DismissAllErrorsButton from '@app/components/shared/DismissAllErrorsButton'; +import TopControls from "@app/components/shared/TopControls"; +import FileEditor from "@app/components/fileEditor/FileEditor"; +import PageEditor from "@app/components/pageEditor/PageEditor"; +import PageEditorControls from "@app/components/pageEditor/PageEditorControls"; +import Viewer from "@app/components/viewer/Viewer"; +import LandingPage from "@app/components/shared/LandingPage"; +import Footer from "@app/components/shared/Footer"; +import DismissAllErrorsButton from "@app/components/shared/DismissAllErrorsButton"; // No props needed - component uses contexts directly export default function Workbench() { @@ -54,41 +58,47 @@ export default function Workbench() { // Get active file index from ViewerContext const { activeFileIndex, setActiveFileIndex } = useViewer(); - + // Get navigation guard for unsaved changes check when switching files const { requestNavigation } = useNavigationGuard(); // Wrap file selection to check for unsaved changes before switching // requestNavigation will show the modal if there are unsaved changes, otherwise navigate immediately - const handleFileSelect = useCallback((index: number) => { - // Don't do anything if selecting the same file - if (index === activeFileIndex) return; + const handleFileSelect = useCallback( + (index: number) => { + // Don't do anything if selecting the same file + if (index === activeFileIndex) return; - // requestNavigation handles the unsaved changes check internally - requestNavigation(() => { - setActiveFileIndex(index); - }); - }, [activeFileIndex, requestNavigation, setActiveFileIndex]); + // requestNavigation handles the unsaved changes check internally + requestNavigation(() => { + setActiveFileIndex(index); + }); + }, + [activeFileIndex, requestNavigation, setActiveFileIndex], + ); - const handleFileRemove = useCallback(async (fileId: FileId) => { - await fileActions.removeFiles([fileId], false); // false = don't delete from IndexedDB, just remove from context - }, [fileActions]); + const handleFileRemove = useCallback( + async (fileId: FileId) => { + await fileActions.removeFiles([fileId], false); // false = don't delete from IndexedDB, just remove from context + }, + [fileActions], + ); const handlePreviewClose = () => { setPreviewFile(null); - const previousMode = sessionStorage.getItem('previousMode'); - if (previousMode === 'split') { + const previousMode = sessionStorage.getItem("previousMode"); + if (previousMode === "split") { // Use context's handleToolSelect which coordinates tool selection and view changes - handleToolSelect('split'); - sessionStorage.removeItem('previousMode'); - } else if (previousMode === 'compress') { - handleToolSelect('compress'); - sessionStorage.removeItem('previousMode'); - } else if (previousMode === 'convert') { - handleToolSelect('convert'); - sessionStorage.removeItem('previousMode'); + handleToolSelect("split"); + sessionStorage.removeItem("previousMode"); + } else if (previousMode === "compress") { + handleToolSelect("compress"); + sessionStorage.removeItem("previousMode"); + } else if (previousMode === "convert") { + handleToolSelect("convert"); + sessionStorage.removeItem("previousMode"); } else { - setCurrentView('fileEditor'); + setCurrentView("fileEditor"); } }; @@ -96,7 +106,9 @@ export default function Workbench() { // Check if we're showing a custom workbench first // Custom workbenches may not require files in FileContext (e.g., sign request workbench) if (!isBaseWorkbench(currentView)) { - const customView = customWorkbenchViews.find((view) => view.workbenchId === currentView && view.data != null); + const customView = customWorkbenchViews.find( + (view) => view.workbenchId === currentView && view.data != null, + ); if (customView) { const CustomComponent = customView.component; return ; @@ -104,15 +116,11 @@ export default function Workbench() { } if (activeFiles.length === 0) { - return ( - - ); + return ; } switch (currentView) { case "fileEditor": - return ( { addFiles(filesToMerge); setCurrentView("viewer"); - } + }, })} /> ); case "viewer": - return ( - +
+ {pageEditorFunctions && ( -
+
+ onClosePdf={pageEditorFunctions.closePdf} + onUndo={pageEditorFunctions.handleUndo} + onRedo={pageEditorFunctions.handleRedo} + canUndo={pageEditorFunctions.canUndo} + canRedo={pageEditorFunctions.canRedo} + onRotate={pageEditorFunctions.handleRotate} + onDelete={pageEditorFunctions.handleDelete} + onSplit={pageEditorFunctions.handleSplit} + onSplitAll={pageEditorFunctions.handleSplitAll} + onPageBreak={pageEditorFunctions.handlePageBreak} + onPageBreakAll={pageEditorFunctions.handlePageBreakAll} + onExportAll={pageEditorFunctions.onExportAll} + exportLoading={pageEditorFunctions.exportLoading} + selectionMode={pageEditorFunctions.selectionMode} + selectedPageIds={pageEditorFunctions.selectedPageIds} + displayDocument={pageEditorFunctions.displayDocument} + splitPositions={pageEditorFunctions.splitPositions} + totalPages={pageEditorFunctions.totalPages} + />
)}
@@ -188,34 +200,40 @@ export default function Workbench() { style={ isRainbowMode ? {} // No background color in rainbow mode - : { backgroundColor: 'var(--bg-background)' } + : { backgroundColor: "var(--bg-background)" } } > {/* Top Controls */} - {activeFiles.length > 0 && !customWorkbenchViews.find(v => v.workbenchId === currentView)?.hideTopControls && ( - { - const stub = selectors.getStirlingFileStub(f.fileId); - return { fileId: f.fileId, name: f.name, versionNumber: stub?.versionNumber }; - })} - currentFileIndex={activeFileIndex} - onFileSelect={handleFileSelect} - onFileRemove={handleFileRemove} - /> - )} + {activeFiles.length > 0 && + !customWorkbenchViews.find((v) => v.workbenchId === currentView) + ?.hideTopControls && ( + { + const stub = selectors.getStirlingFileStub(f.fileId); + return { + fileId: f.fileId, + name: f.name, + versionNumber: stub?.versionNumber, + }; + })} + currentFileIndex={activeFileIndex} + onFileSelect={handleFileSelect} + onFileRemove={handleFileRemove} + /> + )} {/* Dismiss All Errors Button */} {/* Main content area */} {renderMainContent()} diff --git a/frontend/src/core/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css b/frontend/src/core/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css index c507653092..5b3aec2d91 100644 --- a/frontend/src/core/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css +++ b/frontend/src/core/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css @@ -118,7 +118,6 @@ } } - .heroIconsContainer { display: flex; gap: 32px; @@ -141,7 +140,9 @@ border: none; padding: 0; cursor: pointer; - transition: transform 0.2s ease, opacity 0.2s ease; + transition: + transform 0.2s ease, + opacity 0.2s ease; display: flex; align-items: center; justify-content: center; @@ -181,7 +182,14 @@ } .iconLabel { - font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif; + font-family: + "Inter", + system-ui, + -apple-system, + "Segoe UI", + Roboto, + Arial, + sans-serif; font-size: 14px; font-weight: 500; color: rgba(255, 255, 255, 0.9); @@ -266,7 +274,7 @@ opacity: 1; border: 1px solid rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9); - color: #1F2933; + color: #1f2933; box-shadow: 0 0 8px rgba(255, 255, 255, 0.7); } @@ -282,7 +290,14 @@ /* Title styles */ .titleText { - font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif; + font-family: + "Inter", + system-ui, + -apple-system, + "Segoe UI", + Roboto, + Arial, + sans-serif; font-weight: 600; font-size: 22px; color: var(--onboarding-title); @@ -290,7 +305,14 @@ /* Body text styles */ .bodyText { - font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif; + font-family: + "Inter", + system-ui, + -apple-system, + "Segoe UI", + Roboto, + Arial, + sans-serif; font-size: 16px; color: var(--onboarding-body); line-height: 1.5; @@ -314,8 +336,8 @@ } .v2Badge { - background: #DBEFFF; - color: #2A4BFF; + background: #dbefff; + color: #2a4bff; padding: 4px 12px; border-radius: 6px; font-size: 14px; diff --git a/frontend/src/core/components/onboarding/InitialOnboardingModal/renderButtons.tsx b/frontend/src/core/components/onboarding/InitialOnboardingModal/renderButtons.tsx index c50ae7b43e..fc7914db55 100644 --- a/frontend/src/core/components/onboarding/InitialOnboardingModal/renderButtons.tsx +++ b/frontend/src/core/components/onboarding/InitialOnboardingModal/renderButtons.tsx @@ -1,10 +1,13 @@ -import React from 'react'; -import { Button, Group, ActionIcon } from '@mantine/core'; -import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; -import { useTranslation } from 'react-i18next'; -import { ButtonDefinition, type FlowState } from '@app/components/onboarding/onboardingFlowConfig'; -import type { LicenseNotice } from '@app/types/types'; -import type { ButtonAction } from '@app/components/onboarding/onboardingFlowConfig'; +import React from "react"; +import { Button, Group, ActionIcon } from "@mantine/core"; +import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; +import { useTranslation } from "react-i18next"; +import { + ButtonDefinition, + type FlowState, +} from "@app/components/onboarding/onboardingFlowConfig"; +import type { LicenseNotice } from "@app/types/types"; +import type { ButtonAction } from "@app/components/onboarding/onboardingFlowConfig"; interface SlideButtonsProps { slideDefinition: { @@ -16,51 +19,60 @@ interface SlideButtonsProps { onAction: (action: ButtonAction) => void; } -export function SlideButtons({ slideDefinition, licenseNotice, flowState, onAction }: SlideButtonsProps) { +export function SlideButtons({ + slideDefinition, + licenseNotice, + flowState, + onAction, +}: SlideButtonsProps) { const { t } = useTranslation(); - const leftButtons = slideDefinition.buttons.filter((btn) => btn.group === 'left'); - const rightButtons = slideDefinition.buttons.filter((btn) => btn.group === 'right'); + const leftButtons = slideDefinition.buttons.filter( + (btn) => btn.group === "left", + ); + const rightButtons = slideDefinition.buttons.filter( + (btn) => btn.group === "right", + ); - const buttonStyles = (variant: ButtonDefinition['variant']) => - variant === 'primary' + const buttonStyles = (variant: ButtonDefinition["variant"]) => + variant === "primary" ? { root: { - background: 'var(--onboarding-primary-button-bg)', - color: 'var(--onboarding-primary-button-text)', + background: "var(--onboarding-primary-button-bg)", + color: "var(--onboarding-primary-button-text)", }, } : { root: { - background: 'var(--onboarding-secondary-button-bg)', - border: '1px solid var(--onboarding-secondary-button-border)', - color: 'var(--onboarding-secondary-button-text)', + background: "var(--onboarding-secondary-button-bg)", + border: "1px solid var(--onboarding-secondary-button-border)", + color: "var(--onboarding-secondary-button-text)", }, }; const resolveButtonLabel = (button: ButtonDefinition) => { // Special case: override "See Plans" with "Upgrade now" when over limit if ( - button.type === 'button' && - slideDefinition.id === 'server-license' && - button.action === 'see-plans' && + button.type === "button" && + slideDefinition.id === "server-license" && + button.action === "see-plans" && licenseNotice.isOverLimit ) { - return t('onboarding.serverLicense.upgrade', 'Upgrade now →'); + return t("onboarding.serverLicense.upgrade", "Upgrade now →"); } // Translate the label (it's a translation key) - const label = button.label ?? ''; - if (!label) return ''; + const label = button.label ?? ""; + if (!label) return ""; // Extract fallback text from translation key (e.g., 'onboarding.buttons.next' -> 'Next') - const fallback = label.split('.').pop() || label; + const fallback = label.split(".").pop() || label; return t(label, fallback); }; const renderButton = (button: ButtonDefinition) => { const disabled = button.disabledWhen?.(flowState) ?? false; - if (button.type === 'icon') { + if (button.type === "icon") { return ( - {button.icon === 'chevron-left' && } + {button.icon === "chevron-left" && ( + + )} ); } - const variant = button.variant ?? 'secondary'; + const variant = button.variant ?? "secondary"; const label = resolveButtonLabel(button); return ( - ); diff --git a/frontend/src/core/components/onboarding/Onboarding.tsx b/frontend/src/core/components/onboarding/Onboarding.tsx index d098a801d5..d215c0e734 100644 --- a/frontend/src/core/components/onboarding/Onboarding.tsx +++ b/frontend/src/core/components/onboarding/Onboarding.tsx @@ -1,33 +1,40 @@ -import { useEffect, useMemo, useCallback, useState } from 'react'; -import { type StepType } from '@reactour/tour'; -import { useTranslation } from 'react-i18next'; -import { useNavigate, useLocation } from 'react-router-dom'; -import { isAuthRoute } from '@app/constants/routes'; -import { dispatchTourState } from '@app/constants/events'; -import { useOnboardingOrchestrator } from '@app/components/onboarding/orchestrator/useOnboardingOrchestrator'; -import { useBypassOnboarding } from '@app/components/onboarding/useBypassOnboarding'; -import OnboardingTour, { type AdvanceArgs, type CloseArgs } from '@app/components/onboarding/OnboardingTour'; -import OnboardingModalSlide from '@app/components/onboarding/OnboardingModalSlide'; +import { useEffect, useMemo, useCallback, useState } from "react"; +import { type StepType } from "@reactour/tour"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useLocation } from "react-router-dom"; +import { isAuthRoute } from "@app/constants/routes"; +import { dispatchTourState } from "@app/constants/events"; +import { useOnboardingOrchestrator } from "@app/components/onboarding/orchestrator/useOnboardingOrchestrator"; +import { useBypassOnboarding } from "@app/components/onboarding/useBypassOnboarding"; +import OnboardingTour, { + type AdvanceArgs, + type CloseArgs, +} from "@app/components/onboarding/OnboardingTour"; +import OnboardingModalSlide from "@app/components/onboarding/OnboardingModalSlide"; import { useServerLicenseRequest, useTourRequest, -} from '@app/components/onboarding/useOnboardingEffects'; -import { useOnboardingDownload } from '@app/components/onboarding/useOnboardingDownload'; -import { SLIDE_DEFINITIONS, type SlideId, type ButtonAction } from '@app/components/onboarding/onboardingFlowConfig'; -import ToolPanelModePrompt from '@app/components/tools/ToolPanelModePrompt'; -import { useTourOrchestration } from '@app/contexts/TourOrchestrationContext'; -import { useAdminTourOrchestration } from '@app/contexts/AdminTourOrchestrationContext'; -import { createUserStepsConfig } from '@app/components/onboarding/userStepsConfig'; -import { createAdminStepsConfig } from '@app/components/onboarding/adminStepsConfig'; -import { createWhatsNewStepsConfig } from '@app/components/onboarding/whatsNewStepsConfig'; -import { removeAllGlows } from '@app/components/onboarding/tourGlow'; -import { useFilesModalContext } from '@app/contexts/FilesModalContext'; -import { useServerExperience } from '@app/hooks/useServerExperience'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import apiClient from '@app/services/apiClient'; -import '@app/components/onboarding/OnboardingTour.css'; -import { useAccountLogout } from '@app/extensions/accountLogout'; -import { useAuth } from '@app/auth/UseSession'; +} from "@app/components/onboarding/useOnboardingEffects"; +import { useOnboardingDownload } from "@app/components/onboarding/useOnboardingDownload"; +import { + SLIDE_DEFINITIONS, + type SlideId, + type ButtonAction, +} from "@app/components/onboarding/onboardingFlowConfig"; +import ToolPanelModePrompt from "@app/components/tools/ToolPanelModePrompt"; +import { useTourOrchestration } from "@app/contexts/TourOrchestrationContext"; +import { useAdminTourOrchestration } from "@app/contexts/AdminTourOrchestrationContext"; +import { createUserStepsConfig } from "@app/components/onboarding/userStepsConfig"; +import { createAdminStepsConfig } from "@app/components/onboarding/adminStepsConfig"; +import { createWhatsNewStepsConfig } from "@app/components/onboarding/whatsNewStepsConfig"; +import { removeAllGlows } from "@app/components/onboarding/tourGlow"; +import { useFilesModalContext } from "@app/contexts/FilesModalContext"; +import { useServerExperience } from "@app/hooks/useServerExperience"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import apiClient from "@app/services/apiClient"; +import "@app/components/onboarding/OnboardingTour.css"; +import { useAccountLogout } from "@app/extensions/accountLogout"; +import { useAuth } from "@app/auth/UseSession"; export default function Onboarding() { const { t } = useTranslation(); @@ -39,9 +46,18 @@ export default function Onboarding() { const onAuthRoute = isAuthRoute(location.pathname); const { currentStep, isActive, isLoading, runtimeState, activeFlow } = state; - const { osInfo, osOptions, setSelectedDownloadUrl, handleDownloadSelected } = useOnboardingDownload(); - const { showLicenseSlide, licenseNotice: externalLicenseNotice, closeLicenseSlide } = useServerLicenseRequest(); - const { tourRequested: externalTourRequested, requestedTourType, clearTourRequest } = useTourRequest(); + const { osInfo, osOptions, setSelectedDownloadUrl, handleDownloadSelected } = + useOnboardingDownload(); + const { + showLicenseSlide, + licenseNotice: externalLicenseNotice, + closeLicenseSlide, + } = useServerLicenseRequest(); + const { + tourRequested: externalTourRequested, + requestedTourType, + clearTourRequest, + } = useTourRequest(); const { config, refetch: refetchConfig } = useAppConfig(); const [analyticsError, setAnalyticsError] = useState(null); const [analyticsLoading, setAnalyticsLoading] = useState(false); @@ -52,13 +68,16 @@ export default function Onboarding() { const accountLogout = useAccountLogout(); const { signOut } = useAuth(); - const handleRoleSelect = useCallback((role: 'admin' | 'user' | null) => { - actions.updateRuntimeState({ selectedRole: role }); - serverExperience.setSelfReportedAdmin(role === 'admin'); - }, [actions, serverExperience]); + const handleRoleSelect = useCallback( + (role: "admin" | "user" | null) => { + actions.updateRuntimeState({ selectedRole: role }); + serverExperience.setSelfReportedAdmin(role === "admin"); + }, + [actions, serverExperience], + ); const redirectToLogin = useCallback(() => { - window.location.assign('/login'); + window.location.assign("/login"); }, []); const handlePasswordChanged = useCallback(async () => { @@ -75,89 +94,124 @@ export default function Onboarding() { // Check if we should show analytics modal before onboarding useEffect(() => { - if (!isLoading && !analyticsModalDismissed && serverExperience.effectiveIsAdmin && config?.enableAnalytics == null) { + if ( + !isLoading && + !analyticsModalDismissed && + serverExperience.effectiveIsAdmin && + config?.enableAnalytics == null + ) { setShowAnalyticsModal(true); } - }, [isLoading, analyticsModalDismissed, serverExperience.effectiveIsAdmin, config?.enableAnalytics]); + }, [ + isLoading, + analyticsModalDismissed, + serverExperience.effectiveIsAdmin, + config?.enableAnalytics, + ]); - const handleAnalyticsChoice = useCallback(async (enableAnalytics: boolean) => { - if (analyticsLoading) return; - setAnalyticsLoading(true); - setAnalyticsError(null); + const handleAnalyticsChoice = useCallback( + async (enableAnalytics: boolean) => { + if (analyticsLoading) return; + setAnalyticsLoading(true); + setAnalyticsError(null); - const formData = new FormData(); - formData.append('enabled', enableAnalytics.toString()); + const formData = new FormData(); + formData.append("enabled", enableAnalytics.toString()); - try { - await apiClient.post('/api/v1/settings/update-enable-analytics', formData); - await refetchConfig(); - setShowAnalyticsModal(false); - setAnalyticsModalDismissed(true); - } catch (error) { - setAnalyticsError(error instanceof Error ? error.message : 'Unknown error'); - } finally { - setAnalyticsLoading(false); - } - }, [analyticsLoading, refetchConfig]); - - const handleButtonAction = useCallback(async (action: ButtonAction) => { - switch (action) { - case 'next': - case 'complete-close': - actions.complete(); - break; - case 'prev': - actions.prev(); - break; - case 'close': - actions.skip(); - break; - case 'download-selected': - handleDownloadSelected(); - actions.complete(); - break; - case 'security-next': - if (!runtimeState.selectedRole) return; - if (runtimeState.selectedRole !== 'admin') { - actions.updateRuntimeState({ tourType: 'whatsnew' }); - setIsTourOpen(true); - } - actions.complete(); - break; - case 'launch-admin': - actions.updateRuntimeState({ tourType: 'admin' }); - setIsTourOpen(true); - break; - case 'launch-tools': - actions.updateRuntimeState({ tourType: 'whatsnew' }); - setIsTourOpen(true); - break; - case 'launch-auto': { - const tourType = serverExperience.effectiveIsAdmin || runtimeState.selectedRole === 'admin' ? 'admin' : 'whatsnew'; - actions.updateRuntimeState({ tourType }); - setIsTourOpen(true); - break; + try { + await apiClient.post( + "/api/v1/settings/update-enable-analytics", + formData, + ); + await refetchConfig(); + setShowAnalyticsModal(false); + setAnalyticsModalDismissed(true); + } catch (error) { + setAnalyticsError( + error instanceof Error ? error.message : "Unknown error", + ); + } finally { + setAnalyticsLoading(false); } - case 'skip-to-license': - actions.complete(); - break; - case 'skip-tour': - actions.complete(); - break; - case 'see-plans': - actions.complete(); - navigate('/settings/adminPlan'); - break; - case 'enable-analytics': - await handleAnalyticsChoice(true); - break; - case 'disable-analytics': - await handleAnalyticsChoice(false); - break; - } - }, [actions, handleAnalyticsChoice, handleDownloadSelected, navigate, runtimeState.selectedRole, serverExperience.effectiveIsAdmin]); + }, + [analyticsLoading, refetchConfig], + ); - const isRTL = typeof document !== 'undefined' ? document.documentElement.dir === 'rtl' : false; + const handleButtonAction = useCallback( + async (action: ButtonAction) => { + switch (action) { + case "next": + case "complete-close": + actions.complete(); + break; + case "prev": + actions.prev(); + break; + case "close": + actions.skip(); + break; + case "download-selected": + handleDownloadSelected(); + actions.complete(); + break; + case "security-next": + if (!runtimeState.selectedRole) return; + if (runtimeState.selectedRole !== "admin") { + actions.updateRuntimeState({ tourType: "whatsnew" }); + setIsTourOpen(true); + } + actions.complete(); + break; + case "launch-admin": + actions.updateRuntimeState({ tourType: "admin" }); + setIsTourOpen(true); + break; + case "launch-tools": + actions.updateRuntimeState({ tourType: "whatsnew" }); + setIsTourOpen(true); + break; + case "launch-auto": { + const tourType = + serverExperience.effectiveIsAdmin || + runtimeState.selectedRole === "admin" + ? "admin" + : "whatsnew"; + actions.updateRuntimeState({ tourType }); + setIsTourOpen(true); + break; + } + case "skip-to-license": + actions.complete(); + break; + case "skip-tour": + actions.complete(); + break; + case "see-plans": + actions.complete(); + navigate("/settings/adminPlan"); + break; + case "enable-analytics": + await handleAnalyticsChoice(true); + break; + case "disable-analytics": + await handleAnalyticsChoice(false); + break; + } + }, + [ + actions, + handleAnalyticsChoice, + handleDownloadSelected, + navigate, + runtimeState.selectedRole, + serverExperience.effectiveIsAdmin, + ], + ); + + const isRTL = + typeof document !== "undefined" + ? document.documentElement.dir === "rtl" + : false; const [isTourOpen, setIsTourOpen] = useState(false); useEffect(() => dispatchTourState(isTourOpen), [isTourOpen]); @@ -167,65 +221,73 @@ export default function Onboarding() { const adminTourOrch = useAdminTourOrchestration(); const userStepsConfig = useMemo( - () => createUserStepsConfig({ - t, - actions: { - saveWorkbenchState: tourOrch.saveWorkbenchState, - closeFilesModal, - backToAllTools: tourOrch.backToAllTools, - selectCropTool: tourOrch.selectCropTool, - loadSampleFile: tourOrch.loadSampleFile, - switchToActiveFiles: tourOrch.switchToActiveFiles, - pinFile: tourOrch.pinFile, - modifyCropSettings: tourOrch.modifyCropSettings, - executeTool: tourOrch.executeTool, - openFilesModal, - }, - }), - [t, tourOrch, closeFilesModal, openFilesModal] + () => + createUserStepsConfig({ + t, + actions: { + saveWorkbenchState: tourOrch.saveWorkbenchState, + closeFilesModal, + backToAllTools: tourOrch.backToAllTools, + selectCropTool: tourOrch.selectCropTool, + loadSampleFile: tourOrch.loadSampleFile, + switchToActiveFiles: tourOrch.switchToActiveFiles, + pinFile: tourOrch.pinFile, + modifyCropSettings: tourOrch.modifyCropSettings, + executeTool: tourOrch.executeTool, + openFilesModal, + }, + }), + [t, tourOrch, closeFilesModal, openFilesModal], ); const whatsNewStepsConfig = useMemo( - () => createWhatsNewStepsConfig({ - t, - actions: { - saveWorkbenchState: tourOrch.saveWorkbenchState, - closeFilesModal, - backToAllTools: tourOrch.backToAllTools, - openFilesModal, - loadSampleFile: tourOrch.loadSampleFile, - switchToViewer: tourOrch.switchToViewer, - switchToPageEditor: tourOrch.switchToPageEditor, - switchToActiveFiles: tourOrch.switchToActiveFiles, - selectFirstFile: tourOrch.selectFirstFile, - }, - }), - [t, tourOrch, closeFilesModal, openFilesModal] + () => + createWhatsNewStepsConfig({ + t, + actions: { + saveWorkbenchState: tourOrch.saveWorkbenchState, + closeFilesModal, + backToAllTools: tourOrch.backToAllTools, + openFilesModal, + loadSampleFile: tourOrch.loadSampleFile, + switchToViewer: tourOrch.switchToViewer, + switchToPageEditor: tourOrch.switchToPageEditor, + switchToActiveFiles: tourOrch.switchToActiveFiles, + selectFirstFile: tourOrch.selectFirstFile, + }, + }), + [t, tourOrch, closeFilesModal, openFilesModal], ); const adminStepsConfig = useMemo( - () => createAdminStepsConfig({ - t, - actions: { - saveAdminState: adminTourOrch.saveAdminState, - openConfigModal: adminTourOrch.openConfigModal, - navigateToSection: adminTourOrch.navigateToSection, - scrollNavToSection: adminTourOrch.scrollNavToSection, - }, - }), - [t, adminTourOrch] + () => + createAdminStepsConfig({ + t, + actions: { + saveAdminState: adminTourOrch.saveAdminState, + openConfigModal: adminTourOrch.openConfigModal, + navigateToSection: adminTourOrch.navigateToSection, + scrollNavToSection: adminTourOrch.scrollNavToSection, + }, + }), + [t, adminTourOrch], ); const tourSteps = useMemo(() => { switch (runtimeState.tourType) { - case 'admin': + case "admin": return Object.values(adminStepsConfig); - case 'whatsnew': + case "whatsnew": return Object.values(whatsNewStepsConfig); default: return Object.values(userStepsConfig); } - }, [adminStepsConfig, runtimeState.tourType, userStepsConfig, whatsNewStepsConfig]); + }, [ + adminStepsConfig, + runtimeState.tourType, + userStepsConfig, + whatsNewStepsConfig, + ]); useEffect(() => { if (externalTourRequested) { @@ -242,8 +304,8 @@ export default function Onboarding() { // Handle first-login password change modal useEffect(() => { - if(runtimeState.requiresPasswordChange === true) { - console.log('[Onboarding] User requires password change on first login.'); + if (runtimeState.requiresPasswordChange === true) { + console.log("[Onboarding] User requires password change on first login."); setFirstLoginModalOpen(true); } else { setFirstLoginModalOpen(false); @@ -252,18 +314,18 @@ export default function Onboarding() { // Handle MFA setup modal useEffect(() => { - if(runtimeState.requiresMfaSetup === true) { - console.log('[Onboarding] User requires MFA setup.'); + if (runtimeState.requiresMfaSetup === true) { + console.log("[Onboarding] User requires MFA setup."); setMfaModalOpen(true); } else { - console.log('[Onboarding] User does not require MFA setup.'); + console.log("[Onboarding] User does not require MFA setup."); setMfaModalOpen(false); } }, [runtimeState.requiresMfaSetup]); const finishTour = useCallback(() => { setIsTourOpen(false); - if (runtimeState.tourType === 'admin') { + if (runtimeState.tourType === "admin") { adminTourOrch.restoreAdminState(); } else { tourOrch.restoreWorkbenchState(); @@ -272,23 +334,38 @@ export default function Onboarding() { actions.complete(); }, [actions, adminTourOrch, runtimeState.tourType, tourOrch]); - const handleAdvanceTour = useCallback((args: AdvanceArgs) => { - const { setCurrentStep, currentStep: tourCurrentStep, steps, setIsOpen } = args; - if (steps && tourCurrentStep === steps.length - 1) { - setIsOpen(false); - finishTour(); - } else if (steps) { - setCurrentStep((s) => (s === steps.length - 1 ? 0 : s + 1)); - } - }, [finishTour]); + const handleAdvanceTour = useCallback( + (args: AdvanceArgs) => { + const { + setCurrentStep, + currentStep: tourCurrentStep, + steps, + setIsOpen, + } = args; + if (steps && tourCurrentStep === steps.length - 1) { + setIsOpen(false); + finishTour(); + } else if (steps) { + setCurrentStep((s) => (s === steps.length - 1 ? 0 : s + 1)); + } + }, + [finishTour], + ); - const handleCloseTour = useCallback((args: CloseArgs) => { - args.setIsOpen(false); - finishTour(); - }, [finishTour]); + const handleCloseTour = useCallback( + (args: CloseArgs) => { + args.setIsOpen(false); + finishTour(); + }, + [finishTour], + ); const currentSlideDefinition = useMemo(() => { - if (!currentStep || currentStep.type !== 'modal-slide' || !currentStep.slideId) { + if ( + !currentStep || + currentStep.type !== "modal-slide" || + !currentStep.slideId + ) { return null; } return SLIDE_DEFINITIONS[currentStep.slideId as SlideId]; @@ -312,15 +389,31 @@ export default function Onboarding() { analyticsLoading, onMfaSetupComplete: handleMfaSetupComplete, }); - }, [analyticsError, analyticsLoading, currentSlideDefinition, osInfo, osOptions, runtimeState.selectedRole, runtimeState.licenseNotice, handleRoleSelect, serverExperience.loginEnabled, setSelectedDownloadUrl, runtimeState.firstLoginUsername, handlePasswordChanged, handleMfaSetupComplete]); + }, [ + analyticsError, + analyticsLoading, + currentSlideDefinition, + osInfo, + osOptions, + runtimeState.selectedRole, + runtimeState.licenseNotice, + handleRoleSelect, + serverExperience.loginEnabled, + setSelectedDownloadUrl, + runtimeState.firstLoginUsername, + handlePasswordChanged, + handleMfaSetupComplete, + ]); const modalSlideCount = useMemo(() => { - return activeFlow.filter((step) => step.type === 'modal-slide').length; + return activeFlow.filter((step) => step.type === "modal-slide").length; }, [activeFlow]); const currentModalSlideIndex = useMemo(() => { - if (!currentStep || currentStep.type !== 'modal-slide') return 0; - const modalSlides = activeFlow.filter((step) => step.type === 'modal-slide'); + if (!currentStep || currentStep.type !== "modal-slide") return 0; + const modalSlides = activeFlow.filter( + (step) => step.type === "modal-slide", + ); return modalSlides.findIndex((step) => step.id === currentStep.id); }, [activeFlow, currentStep]); @@ -334,10 +427,10 @@ export default function Onboarding() { // Show analytics modal before onboarding if needed if (showAnalyticsModal) { - const slideDefinition = SLIDE_DEFINITIONS['analytics-choice']; + const slideDefinition = SLIDE_DEFINITIONS["analytics-choice"]; const slideContent = slideDefinition.createSlide({ - osLabel: '', - osUrl: '', + osLabel: "", + osUrl: "", selectedRole: null, onRoleSelect: () => {}, analyticsError, @@ -353,9 +446,9 @@ export default function Onboarding() { currentModalSlideIndex={0} onSkip={() => {}} // No skip allowed onAction={async (action) => { - if (action === 'enable-analytics') { + if (action === "enable-analytics") { await handleAnalyticsChoice(true); - } else if (action === 'disable-analytics') { + } else if (action === "disable-analytics") { await handleAnalyticsChoice(false); } }} @@ -365,10 +458,10 @@ export default function Onboarding() { } if (firstLoginModalOpen) { - const baseSlideDefinition = SLIDE_DEFINITIONS['first-login']; + const baseSlideDefinition = SLIDE_DEFINITIONS["first-login"]; const slideContent = baseSlideDefinition.createSlide({ - osLabel: '', - osUrl: '', + osLabel: "", + osUrl: "", selectedRole: null, onRoleSelect: () => {}, firstLoginUsername: runtimeState.firstLoginUsername, @@ -385,7 +478,7 @@ export default function Onboarding() { currentModalSlideIndex={0} onSkip={() => {}} onAction={async (action) => { - if (action === 'complete-close') { + if (action === "complete-close") { handlePasswordChanged(); } }} @@ -395,11 +488,11 @@ export default function Onboarding() { } if (mfaModalOpen) { - console.log('[Onboarding] Rendering MFA setup modal slide.'); - const baseSlideDefinition = SLIDE_DEFINITIONS['mfa-setup']; + console.log("[Onboarding] Rendering MFA setup modal slide."); + const baseSlideDefinition = SLIDE_DEFINITIONS["mfa-setup"]; const slideContent = baseSlideDefinition.createSlide({ - osLabel: '', - osUrl: '', + osLabel: "", + osUrl: "", selectedRole: null, onRoleSelect: () => {}, onMfaSetupComplete: handleMfaSetupComplete, @@ -414,7 +507,7 @@ export default function Onboarding() { currentModalSlideIndex={0} onSkip={() => {}} onAction={async (action) => { - if (action === 'complete-close') { + if (action === "complete-close") { handleMfaSetupComplete(); } }} @@ -424,16 +517,19 @@ export default function Onboarding() { } if (showLicenseSlide) { - const baseSlideDefinition = SLIDE_DEFINITIONS['server-license']; + const baseSlideDefinition = SLIDE_DEFINITIONS["server-license"]; // Remove back button for external license notice const slideDefinition = { ...baseSlideDefinition, - buttons: baseSlideDefinition.buttons.filter(btn => btn.key !== 'license-back') + buttons: baseSlideDefinition.buttons.filter( + (btn) => btn.key !== "license-back", + ), }; - const effectiveLicenseNotice = externalLicenseNotice || runtimeState.licenseNotice; + const effectiveLicenseNotice = + externalLicenseNotice || runtimeState.licenseNotice; const slideContent = slideDefinition.createSlide({ - osLabel: '', - osUrl: '', + osLabel: "", + osUrl: "", osOptions: [], onDownloadUrlChange: () => {}, selectedRole: null, @@ -446,14 +542,17 @@ export default function Onboarding() { { - if (action === 'see-plans') { + if (action === "see-plans") { closeLicenseSlide(); - navigate('/settings/adminPlan'); + navigate("/settings/adminPlan"); } else { closeLicenseSlide(); } @@ -487,10 +586,12 @@ export default function Onboarding() { // Render the current onboarding step switch (currentStep.type) { - case 'tool-prompt': - return ; + case "tool-prompt": + return ( + + ); - case 'modal-slide': + case "modal-slide": if (!currentSlideDefinition || !currentSlideContent) return null; return ( { - if (slideDefinition.hero.type === 'dual-icon') { + if (slideDefinition.hero.type === "dual-icon") { return (
- Stirling icon + Stirling icon
); @@ -56,21 +62,46 @@ export default function OnboardingModalSlide({ return (
- {slideDefinition.hero.type === 'rocket' && ( - + {slideDefinition.hero.type === "rocket" && ( + )} - {slideDefinition.hero.type === 'shield' && ( - + {slideDefinition.hero.type === "shield" && ( + )} - {slideDefinition.hero.type === 'lock' && ( - + {slideDefinition.hero.type === "lock" && ( + )} - {slideDefinition.hero.type === 'analytics' && ( - + {slideDefinition.hero.type === "analytics" && ( + )} - {slideDefinition.hero.type === 'diamond' && } - {slideDefinition.hero.type === 'logo' && ( - Stirling logo + {slideDefinition.hero.type === "diamond" && ( + + )} + {slideDefinition.hero.type === "logo" && ( + Stirling logo )}
); @@ -88,8 +119,13 @@ export default function OnboardingModalSlide({ withCloseButton={false} zIndex={Z_INDEX_OVER_FULLSCREEN_SURFACE} styles={{ - body: { padding: 0, maxHeight: '90vh', overflow: 'hidden' }, - content: { overflow: 'hidden', border: 'none', background: 'var(--bg-surface)', maxHeight: '90vh' }, + body: { padding: 0, maxHeight: "90vh", overflow: "hidden" }, + content: { + overflow: "hidden", + border: "none", + background: "var(--bg-surface)", + maxHeight: "90vh", + }, }} > @@ -106,18 +142,18 @@ export default function OnboardingModalSlide({ radius="md" size={36} style={{ - position: 'absolute', + position: "absolute", top: 16, right: 16, - backgroundColor: 'rgba(255, 255, 255, 0.2)', - color: 'white', - backdropFilter: 'blur(4px)', + backgroundColor: "rgba(255, 255, 255, 0.2)", + color: "white", + backdropFilter: "blur(4px)", zIndex: 10, }} styles={{ root: { - '&:hover': { - backgroundColor: 'rgba(255, 255, 255, 0.3)', + "&:hover": { + backgroundColor: "rgba(255, 255, 255, 0.3)", }, }, }} @@ -130,7 +166,10 @@ export default function OnboardingModalSlide({
-
+
-
+
{slideContent.body}
{modalSlideCount > 1 && ( - + )}
@@ -164,4 +209,3 @@ export default function OnboardingModalSlide({ ); } - diff --git a/frontend/src/core/components/onboarding/OnboardingStepper.tsx b/frontend/src/core/components/onboarding/OnboardingStepper.tsx index ec6767d8ad..c7b74e9a71 100644 --- a/frontend/src/core/components/onboarding/OnboardingStepper.tsx +++ b/frontend/src/core/components/onboarding/OnboardingStepper.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React from "react"; interface OnboardingStepperProps { totalSteps: number; @@ -10,25 +10,29 @@ interface OnboardingStepperProps { * Renders a progress indicator where the active step is a pill and others are dots. * Colors come from theme.css variables. */ -export function OnboardingStepper({ totalSteps, activeStep, className }: OnboardingStepperProps) { +export function OnboardingStepper({ + totalSteps, + activeStep, + className, +}: OnboardingStepperProps) { const items = Array.from({ length: totalSteps }, (_, index) => index); return (
{items.map((index) => { const isActive = index === activeStep; const baseStyles: React.CSSProperties = { background: isActive - ? 'var(--onboarding-step-active)' - : 'var(--onboarding-step-inactive)', + ? "var(--onboarding-step-active)" + : "var(--onboarding-step-inactive)", }; return ( @@ -48,5 +52,3 @@ export function OnboardingStepper({ totalSteps, activeStep, className }: Onboard } export default OnboardingStepper; - - diff --git a/frontend/src/core/components/onboarding/OnboardingTour.css b/frontend/src/core/components/onboarding/OnboardingTour.css index 54ad69d68d..a1cbd4f3d8 100644 --- a/frontend/src/core/components/onboarding/OnboardingTour.css +++ b/frontend/src/core/components/onboarding/OnboardingTour.css @@ -18,7 +18,8 @@ } @keyframes pulse-glow { - 0%, 100% { + 0%, + 100% { box-shadow: 0 0 0 3px var(--mantine-primary-color-filled), 0 0 20px var(--mantine-primary-color-filled), @@ -33,13 +34,13 @@ } /* RTL: mirror step indicator and controls in Reactour popovers */ -:root[dir='rtl'] .reactour__popover { +:root[dir="rtl"] .reactour__popover { direction: rtl; } /* Minimal overrides retained for glow only */ -:root[dir='rtl'] .reactour__badge { +:root[dir="rtl"] .reactour__badge { left: auto; right: 16px; } diff --git a/frontend/src/core/components/onboarding/OnboardingTour.tsx b/frontend/src/core/components/onboarding/OnboardingTour.tsx index 85df0a9fdb..1f3ca67692 100644 --- a/frontend/src/core/components/onboarding/OnboardingTour.tsx +++ b/frontend/src/core/components/onboarding/OnboardingTour.tsx @@ -1,19 +1,19 @@ /** * OnboardingTour Component - * + * * Reusable tour wrapper that encapsulates all Reactour configuration. * Used by the main Onboarding component for both the 'tour' step and * when the tour is open but onboarding is inactive. */ -import React from 'react'; -import { TourProvider, useTour, type StepType } from '@reactour/tour'; -import { CloseButton, ActionIcon } from '@mantine/core'; -import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import CheckIcon from '@mui/icons-material/Check'; -import type { TFunction } from 'i18next'; -import i18n from '@app/i18n'; +import React from "react"; +import { TourProvider, useTour, type StepType } from "@reactour/tour"; +import { CloseButton, ActionIcon } from "@mantine/core"; +import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import CheckIcon from "@mui/icons-material/Check"; +import type { TFunction } from "i18next"; +import i18n from "@app/i18n"; /** * TourContent - Controls the tour visibility @@ -49,7 +49,7 @@ interface CloseArgs { interface OnboardingTourProps { tourSteps: StepType[]; - tourType: 'admin' | 'tools' | 'whatsnew'; + tourType: "admin" | "tools" | "whatsnew"; isRTL: boolean; t: TFunction; isOpen: boolean; @@ -72,7 +72,7 @@ export default function OnboardingTour({ { @@ -80,10 +80,10 @@ export default function OnboardingTour({ onAdvance(clickProps); }} keyboardHandler={(e, clickProps, status) => { - if (e.key === 'ArrowRight' && !status?.isRightDisabled && clickProps) { + if (e.key === "ArrowRight" && !status?.isRightDisabled && clickProps) { e.preventDefault(); onAdvance(clickProps); - } else if (e.key === 'Escape' && !status?.isEscDisabled && clickProps) { + } else if (e.key === "Escape" && !status?.isEscDisabled && clickProps) { e.preventDefault(); onClose(clickProps); } @@ -92,12 +92,12 @@ export default function OnboardingTour({ styles={{ popover: (base) => ({ ...base, - backgroundColor: 'var(--mantine-color-body)', - color: 'var(--mantine-color-text)', - borderRadius: '8px', - padding: '20px', - boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', - maxWidth: '400px', + backgroundColor: "var(--mantine-color-body)", + color: "var(--mantine-color-text)", + borderRadius: "8px", + padding: "20px", + boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)", + maxWidth: "400px", }), maskArea: (base) => ({ ...base, @@ -105,11 +105,11 @@ export default function OnboardingTour({ }), badge: (base) => ({ ...base, - backgroundColor: 'var(--mantine-primary-color-filled)', + backgroundColor: "var(--mantine-primary-color-filled)", }), controls: (base) => ({ ...base, - justifyContent: 'center', + justifyContent: "center", }), }} highlightedMaskClassName="tour-highlight-glow" @@ -119,15 +119,31 @@ export default function OnboardingTour({ disableInteraction={true} disableDotsNavigation={false} prevButton={() => null} - nextButton={({ currentStep: tourCurrentStep, stepsLength, setCurrentStep, setIsOpen }) => { + nextButton={({ + currentStep: tourCurrentStep, + stepsLength, + setCurrentStep, + setIsOpen, + }) => { const isLast = tourCurrentStep === stepsLength - 1; const ArrowIcon = isRTL ? ArrowBackIcon : ArrowForwardIcon; return ( onAdvance({ setCurrentStep, currentStep: tourCurrentStep, steps: tourSteps, setIsOpen })} + onClick={() => + onAdvance({ + setCurrentStep, + currentStep: tourCurrentStep, + steps: tourSteps, + setIsOpen, + }) + } variant="subtle" size="lg" - aria-label={isLast ? t('onboarding.finish', 'Finish') : t('onboarding.next', 'Next')} + aria-label={ + isLast + ? t("onboarding.finish", "Finish") + : t("onboarding.next", "Next") + } > {isLast ? : } @@ -135,10 +151,17 @@ export default function OnboardingTour({ }} components={{ Close: ({ onClick }) => ( - + ), Content: ({ content }: { content: string }) => ( -
+
), }} > @@ -148,4 +171,3 @@ export default function OnboardingTour({ } export type { AdvanceArgs, CloseArgs }; - diff --git a/frontend/src/core/components/onboarding/adminStepsConfig.ts b/frontend/src/core/components/onboarding/adminStepsConfig.ts index b7a5c0b66c..4d33abb57c 100644 --- a/frontend/src/core/components/onboarding/adminStepsConfig.ts +++ b/frontend/src/core/components/onboarding/adminStepsConfig.ts @@ -1,6 +1,9 @@ -import type { StepType } from '@reactour/tour'; -import type { TFunction } from 'i18next'; -import { addGlowToElements, removeAllGlows } from '@app/components/onboarding/tourGlow'; +import type { StepType } from "@reactour/tour"; +import type { TFunction } from "i18next"; +import { + addGlowToElements, + removeAllGlows, +} from "@app/components/onboarding/tourGlow"; export enum AdminTourStep { WELCOME, @@ -14,7 +17,7 @@ export enum AdminTourStep { WRAP_UP, } -interface AdminStepActions { +interface AdminStepActions { saveAdminState: () => void; openConfigModal: () => void; navigateToSection: (section: string) => void; @@ -26,14 +29,25 @@ interface CreateAdminStepsConfigArgs { actions: AdminStepActions; } -export function createAdminStepsConfig({ t, actions }: CreateAdminStepsConfigArgs): Record { - const { saveAdminState, openConfigModal, navigateToSection, scrollNavToSection } = actions; +export function createAdminStepsConfig({ + t, + actions, +}: CreateAdminStepsConfigArgs): Record { + const { + saveAdminState, + openConfigModal, + navigateToSection, + scrollNavToSection, + } = actions; return { [AdminTourStep.WELCOME]: { selector: '[data-tour="config-button"]', - content: t('adminOnboarding.welcome', "Welcome to the Admin Tour! Let's explore the powerful enterprise features and settings available to system administrators."), - position: 'right', + content: t( + "adminOnboarding.welcome", + "Welcome to the Admin Tour! Let's explore the powerful enterprise features and settings available to system administrators.", + ), + position: "right", padding: 10, action: () => { saveAdminState(); @@ -41,17 +55,23 @@ export function createAdminStepsConfig({ t, actions }: CreateAdminStepsConfigArg }, [AdminTourStep.CONFIG_BUTTON]: { selector: '[data-tour="config-button"]', - content: t('adminOnboarding.configButton', "Click the Config button to access all system settings and administrative controls."), - position: 'right', + content: t( + "adminOnboarding.configButton", + "Click the Config button to access all system settings and administrative controls.", + ), + position: "right", padding: 10, actionAfter: () => { openConfigModal(); }, }, [AdminTourStep.SETTINGS_OVERVIEW]: { - selector: '.modal-nav', - content: t('adminOnboarding.settingsOverview', "This is the Settings Panel. Admin settings are organised by category for easy navigation."), - position: 'right', + selector: ".modal-nav", + content: t( + "adminOnboarding.settingsOverview", + "This is the Settings Panel. Admin settings are organised by category for easy navigation.", + ), + position: "right", padding: 0, action: () => { removeAllGlows(); @@ -59,81 +79,137 @@ export function createAdminStepsConfig({ t, actions }: CreateAdminStepsConfigArg }, [AdminTourStep.TEAMS_AND_USERS]: { selector: '[data-tour="admin-people-nav"]', - highlightedSelectors: ['[data-tour="admin-people-nav"]', '[data-tour="admin-teams-nav"]', '[data-tour="settings-content-area"]'], - content: t('adminOnboarding.teamsAndUsers', "Manage Teams and individual users here. You can invite new users via email, shareable links, or create custom accounts for them yourself."), - position: 'right', + highlightedSelectors: [ + '[data-tour="admin-people-nav"]', + '[data-tour="admin-teams-nav"]', + '[data-tour="settings-content-area"]', + ], + content: t( + "adminOnboarding.teamsAndUsers", + "Manage Teams and individual users here. You can invite new users via email, shareable links, or create custom accounts for them yourself.", + ), + position: "right", padding: 10, action: () => { removeAllGlows(); - navigateToSection('people'); + navigateToSection("people"); setTimeout(() => { - addGlowToElements(['[data-tour="admin-people-nav"]', '[data-tour="admin-teams-nav"]', '[data-tour="settings-content-area"]']); + addGlowToElements([ + '[data-tour="admin-people-nav"]', + '[data-tour="admin-teams-nav"]', + '[data-tour="settings-content-area"]', + ]); }, 100); }, }, [AdminTourStep.SYSTEM_CUSTOMIZATION]: { selector: '[data-tour="admin-adminGeneral-nav"]', - highlightedSelectors: ['[data-tour="admin-adminGeneral-nav"]', '[data-tour="admin-adminFeatures-nav"]', '[data-tour="admin-adminEndpoints-nav"]', '[data-tour="settings-content-area"]'], - content: t('adminOnboarding.systemCustomization', "We have extensive ways to customise the UI: System Settings let you change the app name and languages, Features allows server certificate management, and Endpoints lets you enable or disable specific tools for your users."), - position: 'right', + highlightedSelectors: [ + '[data-tour="admin-adminGeneral-nav"]', + '[data-tour="admin-adminFeatures-nav"]', + '[data-tour="admin-adminEndpoints-nav"]', + '[data-tour="settings-content-area"]', + ], + content: t( + "adminOnboarding.systemCustomization", + "We have extensive ways to customise the UI: System Settings let you change the app name and languages, Features allows server certificate management, and Endpoints lets you enable or disable specific tools for your users.", + ), + position: "right", padding: 10, action: () => { removeAllGlows(); - navigateToSection('adminGeneral'); + navigateToSection("adminGeneral"); setTimeout(() => { - addGlowToElements(['[data-tour="admin-adminGeneral-nav"]', '[data-tour="admin-adminFeatures-nav"]', '[data-tour="admin-adminEndpoints-nav"]', '[data-tour="settings-content-area"]']); + addGlowToElements([ + '[data-tour="admin-adminGeneral-nav"]', + '[data-tour="admin-adminFeatures-nav"]', + '[data-tour="admin-adminEndpoints-nav"]', + '[data-tour="settings-content-area"]', + ]); }, 100); }, }, [AdminTourStep.DATABASE_SECTION]: { selector: '[data-tour="admin-adminDatabase-nav"]', - highlightedSelectors: ['[data-tour="admin-adminDatabase-nav"]', '[data-tour="settings-content-area"]'], - content: t('adminOnboarding.databaseSection', "For advanced production environments, we have settings to allow external database hookups so you can integrate with your existing infrastructure."), - position: 'right', + highlightedSelectors: [ + '[data-tour="admin-adminDatabase-nav"]', + '[data-tour="settings-content-area"]', + ], + content: t( + "adminOnboarding.databaseSection", + "For advanced production environments, we have settings to allow external database hookups so you can integrate with your existing infrastructure.", + ), + position: "right", padding: 10, action: () => { removeAllGlows(); - navigateToSection('adminDatabase'); + navigateToSection("adminDatabase"); setTimeout(() => { - addGlowToElements(['[data-tour="admin-adminDatabase-nav"]', '[data-tour="settings-content-area"]']); + addGlowToElements([ + '[data-tour="admin-adminDatabase-nav"]', + '[data-tour="settings-content-area"]', + ]); }, 100); }, }, [AdminTourStep.CONNECTIONS_SECTION]: { selector: '[data-tour="admin-adminConnections-nav"]', - highlightedSelectors: ['[data-tour="admin-adminConnections-nav"]', '[data-tour="settings-content-area"]'], - content: t('adminOnboarding.connectionsSection', "The Connections section supports various login methods including custom SSO and SAML providers like Google and GitHub, plus email integrations for notifications and communications."), - position: 'right', + highlightedSelectors: [ + '[data-tour="admin-adminConnections-nav"]', + '[data-tour="settings-content-area"]', + ], + content: t( + "adminOnboarding.connectionsSection", + "The Connections section supports various login methods including custom SSO and SAML providers like Google and GitHub, plus email integrations for notifications and communications.", + ), + position: "right", padding: 10, action: () => { removeAllGlows(); - navigateToSection('adminConnections'); + navigateToSection("adminConnections"); setTimeout(() => { - addGlowToElements(['[data-tour="admin-adminConnections-nav"]', '[data-tour="settings-content-area"]']); + addGlowToElements([ + '[data-tour="admin-adminConnections-nav"]', + '[data-tour="settings-content-area"]', + ]); }, 100); }, actionAfter: async () => { - await scrollNavToSection('adminAudit'); + await scrollNavToSection("adminAudit"); }, }, [AdminTourStep.ADMIN_TOOLS]: { selector: '[data-tour="admin-adminAudit-nav"]', - highlightedSelectors: ['[data-tour="admin-adminAudit-nav"]', '[data-tour="admin-adminUsage-nav"]', '[data-tour="settings-content-area"]'], - content: t('adminOnboarding.adminTools', "Finally, we have advanced administration tools like Auditing to track system activity and Usage Analytics to monitor how your users interact with the platform."), - position: 'right', + highlightedSelectors: [ + '[data-tour="admin-adminAudit-nav"]', + '[data-tour="admin-adminUsage-nav"]', + '[data-tour="settings-content-area"]', + ], + content: t( + "adminOnboarding.adminTools", + "Finally, we have advanced administration tools like Auditing to track system activity and Usage Analytics to monitor how your users interact with the platform.", + ), + position: "right", padding: 10, action: () => { removeAllGlows(); - navigateToSection('adminAudit'); + navigateToSection("adminAudit"); setTimeout(() => { - addGlowToElements(['[data-tour="admin-adminAudit-nav"]', '[data-tour="admin-adminUsage-nav"]', '[data-tour="settings-content-area"]']); + addGlowToElements([ + '[data-tour="admin-adminAudit-nav"]', + '[data-tour="admin-adminUsage-nav"]', + '[data-tour="settings-content-area"]', + ]); }, 100); }, }, [AdminTourStep.WRAP_UP]: { selector: '[data-tour="help-button"]', - content: t('adminOnboarding.wrapUp', "That's the admin tour! You've seen the enterprise features that make Stirling PDF a powerful, customisable solution for organisations. Access this tour anytime from the Help menu."), - position: 'right', + content: t( + "adminOnboarding.wrapUp", + "That's the admin tour! You've seen the enterprise features that make Stirling PDF a powerful, customisable solution for organisations. Access this tour anytime from the Help menu.", + ), + position: "right", padding: 10, action: () => { removeAllGlows(); @@ -141,4 +217,3 @@ export function createAdminStepsConfig({ t, actions }: CreateAdminStepsConfigArg }, }; } - diff --git a/frontend/src/core/components/onboarding/onboardingFlowConfig.ts b/frontend/src/core/components/onboarding/onboardingFlowConfig.ts index 101fb13eef..5f5a9be408 100644 --- a/frontend/src/core/components/onboarding/onboardingFlowConfig.ts +++ b/frontend/src/core/components/onboarding/onboardingFlowConfig.ts @@ -1,45 +1,52 @@ -import WelcomeSlide from '@app/components/onboarding/slides/WelcomeSlide'; -import DesktopInstallSlide from '@app/components/onboarding/slides/DesktopInstallSlide'; -import SecurityCheckSlide from '@app/components/onboarding/slides/SecurityCheckSlide'; -import PlanOverviewSlide from '@app/components/onboarding/slides/PlanOverviewSlide'; -import ServerLicenseSlide from '@app/components/onboarding/slides/ServerLicenseSlide'; -import FirstLoginSlide from '@app/components/onboarding/slides/FirstLoginSlide'; -import TourOverviewSlide from '@app/components/onboarding/slides/TourOverviewSlide'; -import AnalyticsChoiceSlide from '@app/components/onboarding/slides/AnalyticsChoiceSlide'; -import MFASetupSlide from '@app/components/onboarding/slides/MFASetupSlide'; -import { SlideConfig, LicenseNotice } from '@app/types/types'; +import WelcomeSlide from "@app/components/onboarding/slides/WelcomeSlide"; +import DesktopInstallSlide from "@app/components/onboarding/slides/DesktopInstallSlide"; +import SecurityCheckSlide from "@app/components/onboarding/slides/SecurityCheckSlide"; +import PlanOverviewSlide from "@app/components/onboarding/slides/PlanOverviewSlide"; +import ServerLicenseSlide from "@app/components/onboarding/slides/ServerLicenseSlide"; +import FirstLoginSlide from "@app/components/onboarding/slides/FirstLoginSlide"; +import TourOverviewSlide from "@app/components/onboarding/slides/TourOverviewSlide"; +import AnalyticsChoiceSlide from "@app/components/onboarding/slides/AnalyticsChoiceSlide"; +import MFASetupSlide from "@app/components/onboarding/slides/MFASetupSlide"; +import { SlideConfig, LicenseNotice } from "@app/types/types"; export type SlideId = - | 'first-login' - | 'welcome' - | 'desktop-install' - | 'security-check' - | 'admin-overview' - | 'server-license' - | 'tour-overview' - | 'analytics-choice' - | 'mfa-setup'; + | "first-login" + | "welcome" + | "desktop-install" + | "security-check" + | "admin-overview" + | "server-license" + | "tour-overview" + | "analytics-choice" + | "mfa-setup"; -export type HeroType = 'rocket' | 'dual-icon' | 'shield' | 'diamond' | 'logo' | 'lock' | 'analytics'; +export type HeroType = + | "rocket" + | "dual-icon" + | "shield" + | "diamond" + | "logo" + | "lock" + | "analytics"; export type ButtonAction = - | 'next' - | 'prev' - | 'close' - | 'complete-close' - | 'download-selected' - | 'security-next' - | 'launch-admin' - | 'launch-tools' - | 'launch-auto' - | 'see-plans' - | 'skip-to-license' - | 'skip-tour' - | 'enable-analytics' - | 'disable-analytics'; + | "next" + | "prev" + | "close" + | "complete-close" + | "download-selected" + | "security-next" + | "launch-admin" + | "launch-tools" + | "launch-auto" + | "see-plans" + | "skip-to-license" + | "skip-tour" + | "enable-analytics" + | "disable-analytics"; export interface FlowState { - selectedRole: 'admin' | 'user' | null; + selectedRole: "admin" | "user" | null; } export interface OSOption { @@ -53,8 +60,8 @@ export interface SlideFactoryParams { osUrl: string; osOptions?: OSOption[]; onDownloadUrlChange?: (url: string) => void; - selectedRole: 'admin' | 'user' | null; - onRoleSelect: (role: 'admin' | 'user' | null) => void; + selectedRole: "admin" | "user" | null; + onRoleSelect: (role: "admin" | "user" | null) => void; licenseNotice?: LicenseNotice; loginEnabled?: boolean; // First login params @@ -72,11 +79,11 @@ export interface HeroDefinition { export interface ButtonDefinition { key: string; - type: 'button' | 'icon'; + type: "button" | "icon"; label?: string; - icon?: 'chevron-left'; - variant?: 'primary' | 'secondary' | 'default'; - group: 'left' | 'right'; + icon?: "chevron-left"; + variant?: "primary" | "secondary" | "default"; + group: "left" | "right"; action: ButtonAction; disabledWhen?: (state: FlowState) => boolean; } @@ -89,206 +96,212 @@ export interface SlideDefinition { } export const SLIDE_DEFINITIONS: Record = { - 'first-login': { - id: 'first-login', - createSlide: ({ firstLoginUsername, onPasswordChanged, usingDefaultCredentials }) => + "first-login": { + id: "first-login", + createSlide: ({ + firstLoginUsername, + onPasswordChanged, + usingDefaultCredentials, + }) => FirstLoginSlide({ - username: firstLoginUsername || '', + username: firstLoginUsername || "", onPasswordChanged: onPasswordChanged || (() => {}), usingDefaultCredentials: usingDefaultCredentials || false, }), - hero: { type: 'lock' }, + hero: { type: "lock" }, buttons: [], // Form has its own submit button }, - 'welcome': { - id: 'welcome', + welcome: { + id: "welcome", createSlide: () => WelcomeSlide(), - hero: { type: 'rocket' }, + hero: { type: "rocket" }, buttons: [ { - key: 'welcome-next', - type: 'button', - label: 'onboarding.buttons.next', - variant: 'primary', - group: 'right', - action: 'next', + key: "welcome-next", + type: "button", + label: "onboarding.buttons.next", + variant: "primary", + group: "right", + action: "next", }, ], }, - 'desktop-install': { - id: 'desktop-install', - createSlide: ({ osLabel, osUrl, osOptions, onDownloadUrlChange }) => DesktopInstallSlide({ osLabel, osUrl, osOptions, onDownloadUrlChange }), - hero: { type: 'dual-icon' }, + "desktop-install": { + id: "desktop-install", + createSlide: ({ osLabel, osUrl, osOptions, onDownloadUrlChange }) => + DesktopInstallSlide({ osLabel, osUrl, osOptions, onDownloadUrlChange }), + hero: { type: "dual-icon" }, buttons: [ { - key: 'desktop-back', - type: 'icon', - icon: 'chevron-left', - group: 'left', - action: 'prev', + key: "desktop-back", + type: "icon", + icon: "chevron-left", + group: "left", + action: "prev", }, { - key: 'desktop-skip', - type: 'button', - label: 'onboarding.buttons.skipForNow', - variant: 'secondary', - group: 'left', - action: 'next', + key: "desktop-skip", + type: "button", + label: "onboarding.buttons.skipForNow", + variant: "secondary", + group: "left", + action: "next", }, { - key: 'desktop-download', - type: 'button', - label: 'onboarding.buttons.download', - variant: 'primary', - group: 'right', - action: 'download-selected', + key: "desktop-download", + type: "button", + label: "onboarding.buttons.download", + variant: "primary", + group: "right", + action: "download-selected", }, ], }, - 'security-check': { - id: 'security-check', + "security-check": { + id: "security-check", createSlide: ({ selectedRole, onRoleSelect }) => SecurityCheckSlide({ selectedRole, onRoleSelect }), - hero: { type: 'shield' }, + hero: { type: "shield" }, buttons: [ { - key: 'security-back', - type: 'button', - label: 'onboarding.buttons.back', - variant: 'secondary', - group: 'left', - action: 'prev', + key: "security-back", + type: "button", + label: "onboarding.buttons.back", + variant: "secondary", + group: "left", + action: "prev", }, { - key: 'security-next', - type: 'button', - label: 'onboarding.buttons.next', - variant: 'primary', - group: 'right', - action: 'security-next', + key: "security-next", + type: "button", + label: "onboarding.buttons.next", + variant: "primary", + group: "right", + action: "security-next", disabledWhen: (state) => !state.selectedRole, }, ], }, - 'admin-overview': { - id: 'admin-overview', + "admin-overview": { + id: "admin-overview", createSlide: ({ licenseNotice, loginEnabled }) => PlanOverviewSlide({ isAdmin: true, licenseNotice, loginEnabled }), - hero: { type: 'diamond' }, + hero: { type: "diamond" }, buttons: [ { - key: 'admin-back', - type: 'icon', - icon: 'chevron-left', - group: 'left', - action: 'prev', + key: "admin-back", + type: "icon", + icon: "chevron-left", + group: "left", + action: "prev", }, { - key: 'admin-show', - type: 'button', - label: 'onboarding.buttons.showMeAround', - variant: 'primary', - group: 'right', - action: 'launch-admin', + key: "admin-show", + type: "button", + label: "onboarding.buttons.showMeAround", + variant: "primary", + group: "right", + action: "launch-admin", }, { - key: 'admin-skip', - type: 'button', - label: 'onboarding.buttons.skipTheTour', - variant: 'secondary', - group: 'left', - action: 'skip-to-license', + key: "admin-skip", + type: "button", + label: "onboarding.buttons.skipTheTour", + variant: "secondary", + group: "left", + action: "skip-to-license", }, ], }, - 'server-license': { - id: 'server-license', + "server-license": { + id: "server-license", createSlide: ({ licenseNotice }) => ServerLicenseSlide({ licenseNotice }), - hero: { type: 'dual-icon' }, + hero: { type: "dual-icon" }, buttons: [ { - key: 'license-back', - type: 'icon', - icon: 'chevron-left', - group: 'left', - action: 'prev', + key: "license-back", + type: "icon", + icon: "chevron-left", + group: "left", + action: "prev", }, { - key: 'license-close', - type: 'button', - label: 'onboarding.buttons.skipForNow', - variant: 'secondary', - group: 'left', - action: 'close', + key: "license-close", + type: "button", + label: "onboarding.buttons.skipForNow", + variant: "secondary", + group: "left", + action: "close", }, { - key: 'license-see-plans', - type: 'button', - label: 'onboarding.serverLicense.seePlans', - variant: 'primary', - group: 'right', - action: 'see-plans', + key: "license-see-plans", + type: "button", + label: "onboarding.serverLicense.seePlans", + variant: "primary", + group: "right", + action: "see-plans", }, ], }, - 'tour-overview': { - id: 'tour-overview', + "tour-overview": { + id: "tour-overview", createSlide: () => TourOverviewSlide(), - hero: { type: 'rocket' }, + hero: { type: "rocket" }, buttons: [ { - key: 'tour-overview-back', - type: 'icon', - icon: 'chevron-left', - group: 'left', - action: 'prev', + key: "tour-overview-back", + type: "icon", + icon: "chevron-left", + group: "left", + action: "prev", }, { - key: 'tour-overview-skip', - type: 'button', - label: 'onboarding.buttons.skipForNow', - variant: 'secondary', - group: 'left', - action: 'skip-tour', + key: "tour-overview-skip", + type: "button", + label: "onboarding.buttons.skipForNow", + variant: "secondary", + group: "left", + action: "skip-tour", }, { - key: 'tour-overview-show', - type: 'button', - label: 'onboarding.buttons.showMeAround', - variant: 'primary', - group: 'right', - action: 'launch-tools', + key: "tour-overview-show", + type: "button", + label: "onboarding.buttons.showMeAround", + variant: "primary", + group: "right", + action: "launch-tools", }, ], }, - 'analytics-choice': { - id: 'analytics-choice', - createSlide: ({ analyticsError }) => AnalyticsChoiceSlide({ analyticsError }), - hero: { type: 'analytics' }, + "analytics-choice": { + id: "analytics-choice", + createSlide: ({ analyticsError }) => + AnalyticsChoiceSlide({ analyticsError }), + hero: { type: "analytics" }, buttons: [ { - key: 'analytics-disable', - type: 'button', - label: 'no', - variant: 'secondary', - group: 'left', - action: 'disable-analytics', + key: "analytics-disable", + type: "button", + label: "no", + variant: "secondary", + group: "left", + action: "disable-analytics", }, { - key: 'analytics-enable', - type: 'button', - label: 'yes', - variant: 'primary', - group: 'right', - action: 'enable-analytics', + key: "analytics-enable", + type: "button", + label: "yes", + variant: "primary", + group: "right", + action: "enable-analytics", }, ], }, - 'mfa-setup': { - id: 'mfa-setup', - createSlide: ({ onMfaSetupComplete = () => {} }: SlideFactoryParams) => MFASetupSlide({ onMfaSetupComplete }), - hero: { type: 'lock' }, + "mfa-setup": { + id: "mfa-setup", + createSlide: ({ onMfaSetupComplete = () => {} }: SlideFactoryParams) => + MFASetupSlide({ onMfaSetupComplete }), + hero: { type: "lock" }, buttons: [], // Form has its own submit button }, }; - diff --git a/frontend/src/core/components/onboarding/orchestrator/onboardingConfig.ts b/frontend/src/core/components/onboarding/orchestrator/onboardingConfig.ts index 9528694c3e..6e10ec97f3 100644 --- a/frontend/src/core/components/onboarding/orchestrator/onboardingConfig.ts +++ b/frontend/src/core/components/onboarding/orchestrator/onboardingConfig.ts @@ -1,23 +1,21 @@ export type OnboardingStepId = - | 'first-login' - | 'welcome' - | 'desktop-install' - | 'security-check' - | 'admin-overview' - | 'tool-layout' - | 'tour-overview' - | 'server-license' - | 'analytics-choice' - | 'mfa-setup'; + | "first-login" + | "welcome" + | "desktop-install" + | "security-check" + | "admin-overview" + | "tool-layout" + | "tour-overview" + | "server-license" + | "analytics-choice" + | "mfa-setup"; -export type OnboardingStepType = - | 'modal-slide' - | 'tool-prompt'; +export type OnboardingStepType = "modal-slide" | "tool-prompt"; export interface OnboardingRuntimeState { - selectedRole: 'admin' | 'user' | null; + selectedRole: "admin" | "user" | null; tourRequested: boolean; - tourType: 'admin' | 'tools' | 'whatsnew'; + tourType: "admin" | "tools" | "whatsnew"; isDesktopApp: boolean; desktopSlideEnabled: boolean; analyticsNotConfigured: boolean; @@ -43,14 +41,23 @@ export interface OnboardingStep { id: OnboardingStepId; type: OnboardingStepType; condition: (ctx: OnboardingConditionContext) => boolean; - slideId?: 'first-login' | 'welcome' | 'desktop-install' | 'security-check' | 'admin-overview' | 'server-license' | 'tour-overview' | 'analytics-choice' | 'mfa-setup'; + slideId?: + | "first-login" + | "welcome" + | "desktop-install" + | "security-check" + | "admin-overview" + | "server-license" + | "tour-overview" + | "analytics-choice" + | "mfa-setup"; allowDismiss?: boolean; } export const DEFAULT_RUNTIME_STATE: OnboardingRuntimeState = { selectedRole: null, tourRequested: false, - tourType: 'whatsnew', + tourType: "whatsnew", isDesktopApp: false, analyticsNotConfigured: false, analyticsEnabled: false, @@ -61,7 +68,7 @@ export const DEFAULT_RUNTIME_STATE: OnboardingRuntimeState = { requiresLicense: false, }, requiresPasswordChange: false, - firstLoginUsername: '', + firstLoginUsername: "", usingDefaultCredentials: false, desktopSlideEnabled: true, requiresMfaSetup: false, @@ -69,59 +76,61 @@ export const DEFAULT_RUNTIME_STATE: OnboardingRuntimeState = { export const ONBOARDING_STEPS: OnboardingStep[] = [ { - id: 'first-login', - type: 'modal-slide', - slideId: 'first-login', + id: "first-login", + type: "modal-slide", + slideId: "first-login", condition: (ctx) => ctx.requiresPasswordChange, }, { - id: 'welcome', - type: 'modal-slide', - slideId: 'welcome', + id: "welcome", + type: "modal-slide", + slideId: "welcome", // Desktop has its own onboarding modal (DesktopOnboardingModal) condition: (ctx) => !ctx.isDesktopApp, }, { - id: 'admin-overview', - type: 'modal-slide', - slideId: 'admin-overview', + id: "admin-overview", + type: "modal-slide", + slideId: "admin-overview", condition: (ctx) => ctx.effectiveIsAdmin, }, { - id: 'desktop-install', - type: 'modal-slide', - slideId: 'desktop-install', + id: "desktop-install", + type: "modal-slide", + slideId: "desktop-install", condition: (ctx) => !ctx.isDesktopApp && ctx.desktopSlideEnabled, }, { - id: 'security-check', - type: 'modal-slide', - slideId: 'security-check', + id: "security-check", + type: "modal-slide", + slideId: "security-check", condition: () => false, }, { - id: 'tool-layout', - type: 'tool-prompt', + id: "tool-layout", + type: "tool-prompt", condition: () => false, }, { - id: 'tour-overview', - type: 'modal-slide', - slideId: 'tour-overview', - condition: (ctx) => !ctx.effectiveIsAdmin && ctx.tourType !== 'admin' && !ctx.isDesktopApp, + id: "tour-overview", + type: "modal-slide", + slideId: "tour-overview", + condition: (ctx) => + !ctx.effectiveIsAdmin && ctx.tourType !== "admin" && !ctx.isDesktopApp, }, { - id: 'server-license', - type: 'modal-slide', - slideId: 'server-license', - condition: (ctx) => ctx.effectiveIsAdmin && ctx.licenseNotice.requiresLicense, + id: "server-license", + type: "modal-slide", + slideId: "server-license", + condition: (ctx) => + ctx.effectiveIsAdmin && ctx.licenseNotice.requiresLicense, }, { - id: 'mfa-setup', - type: 'modal-slide', - slideId: 'mfa-setup', + id: "mfa-setup", + type: "modal-slide", + slideId: "mfa-setup", condition: (ctx) => ctx.requiresMfaSetup, - } + }, ]; export function getStepById(id: OnboardingStepId): OnboardingStep | undefined { @@ -131,4 +140,3 @@ export function getStepById(id: OnboardingStepId): OnboardingStep | undefined { export function getStepIndex(id: OnboardingStepId): number { return ONBOARDING_STEPS.findIndex((step) => step.id === id); } - diff --git a/frontend/src/core/components/onboarding/orchestrator/onboardingStorage.ts b/frontend/src/core/components/onboarding/orchestrator/onboardingStorage.ts index 9e3065614a..73f45cb1ea 100644 --- a/frontend/src/core/components/onboarding/orchestrator/onboardingStorage.ts +++ b/frontend/src/core/components/onboarding/orchestrator/onboardingStorage.ts @@ -1,73 +1,85 @@ -const STORAGE_PREFIX = 'onboarding'; +const STORAGE_PREFIX = "onboarding"; const TOURS_TOOLTIP_KEY = `${STORAGE_PREFIX}::tours-tooltip-shown`; const ONBOARDING_COMPLETED_KEY = `${STORAGE_PREFIX}::completed`; export function isOnboardingCompleted(): boolean { - if (typeof window === 'undefined') return false; + if (typeof window === "undefined") return false; try { - return localStorage.getItem(ONBOARDING_COMPLETED_KEY) === 'true'; + return localStorage.getItem(ONBOARDING_COMPLETED_KEY) === "true"; } catch { return false; } } export function markOnboardingCompleted(): void { - if (typeof window === 'undefined') return; + if (typeof window === "undefined") return; try { - localStorage.setItem(ONBOARDING_COMPLETED_KEY, 'true'); + localStorage.setItem(ONBOARDING_COMPLETED_KEY, "true"); } catch (error) { - console.error('[onboardingStorage] Error marking onboarding as completed:', error); + console.error( + "[onboardingStorage] Error marking onboarding as completed:", + error, + ); } } export function resetOnboardingProgress(): void { - if (typeof window === 'undefined') return; + if (typeof window === "undefined") return; try { localStorage.removeItem(ONBOARDING_COMPLETED_KEY); } catch (error) { - console.error('[onboardingStorage] Error resetting onboarding progress:', error); + console.error( + "[onboardingStorage] Error resetting onboarding progress:", + error, + ); } } export function hasShownToursTooltip(): boolean { - if (typeof window === 'undefined') return false; + if (typeof window === "undefined") return false; try { - return localStorage.getItem(TOURS_TOOLTIP_KEY) === 'true'; + return localStorage.getItem(TOURS_TOOLTIP_KEY) === "true"; } catch { return false; } } export function markToursTooltipShown(): void { - if (typeof window === 'undefined') return; + if (typeof window === "undefined") return; try { - localStorage.setItem(TOURS_TOOLTIP_KEY, 'true'); + localStorage.setItem(TOURS_TOOLTIP_KEY, "true"); } catch (error) { - console.error('[onboardingStorage] Error marking tours tooltip as shown:', error); + console.error( + "[onboardingStorage] Error marking tours tooltip as shown:", + error, + ); } } export function migrateFromLegacyPreferences(): void { - if (typeof window === 'undefined') return; + if (typeof window === "undefined") return; const migrationKey = `${STORAGE_PREFIX}::migrated`; try { // Skip if already migrated - if (localStorage.getItem(migrationKey) === 'true') return; + if (localStorage.getItem(migrationKey) === "true") return; - const prefsRaw = localStorage.getItem('stirlingpdf_preferences'); + const prefsRaw = localStorage.getItem("stirlingpdf_preferences"); if (prefsRaw) { const prefs = JSON.parse(prefsRaw) as Record; // If user had completed onboarding in old system, mark new system as complete - if (prefs.hasCompletedOnboarding === true || prefs.hasSeenIntroOnboarding === true) { + if ( + prefs.hasCompletedOnboarding === true || + prefs.hasSeenIntroOnboarding === true + ) { markOnboardingCompleted(); } } // Mark migration complete - localStorage.setItem(migrationKey, 'true'); + localStorage.setItem(migrationKey, "true"); } catch { // If migration fails, onboarding will show again - safer than hiding it } diff --git a/frontend/src/core/components/onboarding/orchestrator/useOnboardingOrchestrator.ts b/frontend/src/core/components/onboarding/orchestrator/useOnboardingOrchestrator.ts index 54d1d2acf4..44052636d1 100644 --- a/frontend/src/core/components/onboarding/orchestrator/useOnboardingOrchestrator.ts +++ b/frontend/src/core/components/onboarding/orchestrator/useOnboardingOrchestrator.ts @@ -1,7 +1,7 @@ -import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; -import { useLocation } from 'react-router-dom'; -import { useServerExperience } from '@app/hooks/useServerExperience'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; +import { useState, useCallback, useMemo, useEffect, useRef } from "react"; +import { useLocation } from "react-router-dom"; +import { useServerExperience } from "@app/hooks/useServerExperience"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; import { ONBOARDING_STEPS, @@ -10,39 +10,48 @@ import { type OnboardingRuntimeState, type OnboardingConditionContext, DEFAULT_RUNTIME_STATE, -} from '@app/components/onboarding/orchestrator/onboardingConfig'; +} from "@app/components/onboarding/orchestrator/onboardingConfig"; import { isOnboardingCompleted, markOnboardingCompleted, migrateFromLegacyPreferences, -} from '@app/components/onboarding/orchestrator/onboardingStorage'; -import { accountService } from '@app/services/accountService'; -import { useBypassOnboarding } from '@app/components/onboarding/useBypassOnboarding'; +} from "@app/components/onboarding/orchestrator/onboardingStorage"; +import { accountService } from "@app/services/accountService"; +import { useBypassOnboarding } from "@app/components/onboarding/useBypassOnboarding"; -const AUTH_ROUTES = ['/login', '/signup', '/auth', '/invite']; -const SESSION_TOUR_REQUESTED = 'onboarding::session::tour-requested'; -const SESSION_TOUR_TYPE = 'onboarding::session::tour-type'; -const SESSION_SELECTED_ROLE = 'onboarding::session::selected-role'; +const AUTH_ROUTES = ["/login", "/signup", "/auth", "/invite"]; +const SESSION_TOUR_REQUESTED = "onboarding::session::tour-requested"; +const SESSION_TOUR_TYPE = "onboarding::session::tour-type"; +const SESSION_SELECTED_ROLE = "onboarding::session::selected-role"; // Check if user has an auth token (to avoid flash before redirect) function hasAuthToken(): boolean { - if (typeof window === 'undefined') return false; - return !!localStorage.getItem('stirling_jwt'); + if (typeof window === "undefined") return false; + return !!localStorage.getItem("stirling_jwt"); } // Get initial runtime state from session storage (survives remounts) -function getInitialRuntimeState(baseState: OnboardingRuntimeState): OnboardingRuntimeState { - if (typeof window === 'undefined') { +function getInitialRuntimeState( + baseState: OnboardingRuntimeState, +): OnboardingRuntimeState { + if (typeof window === "undefined") { return baseState; } try { - const tourRequested = sessionStorage.getItem(SESSION_TOUR_REQUESTED) === 'true'; + const tourRequested = + sessionStorage.getItem(SESSION_TOUR_REQUESTED) === "true"; const sessionTourType = sessionStorage.getItem(SESSION_TOUR_TYPE); - const tourType = (sessionTourType === 'admin' || sessionTourType === 'tools' || sessionTourType === 'whatsnew') - ? sessionTourType - : 'whatsnew'; - const selectedRole = sessionStorage.getItem(SESSION_SELECTED_ROLE) as 'admin' | 'user' | null; + const tourType = + sessionTourType === "admin" || + sessionTourType === "tools" || + sessionTourType === "whatsnew" + ? sessionTourType + : "whatsnew"; + const selectedRole = sessionStorage.getItem(SESSION_SELECTED_ROLE) as + | "admin" + | "user" + | null; return { ...baseState, @@ -56,11 +65,14 @@ function getInitialRuntimeState(baseState: OnboardingRuntimeState): OnboardingRu } function persistRuntimeState(state: Partial): void { - if (typeof window === 'undefined') return; + if (typeof window === "undefined") return; try { if (state.tourRequested !== undefined) { - sessionStorage.setItem(SESSION_TOUR_REQUESTED, state.tourRequested ? 'true' : 'false'); + sessionStorage.setItem( + SESSION_TOUR_REQUESTED, + state.tourRequested ? "true" : "false", + ); } if (state.tourType !== undefined) { sessionStorage.setItem(SESSION_TOUR_TYPE, state.tourType); @@ -73,12 +85,15 @@ function persistRuntimeState(state: Partial): void { } } } catch (error) { - console.error('[useOnboardingOrchestrator] Error persisting runtime state:', error); + console.error( + "[useOnboardingOrchestrator] Error persisting runtime state:", + error, + ); } } function clearRuntimeStateSession(): void { - if (typeof window === 'undefined') return; + if (typeof window === "undefined") return; try { sessionStorage.removeItem(SESSION_TOUR_REQUESTED); @@ -94,9 +109,12 @@ function parseMfaRequired(settings: string | null | undefined): boolean { try { const parsed = JSON.parse(settings) as { mfaRequired?: string }; - return parsed.mfaRequired?.toLowerCase() === 'true'; + return parsed.mfaRequired?.toLowerCase() === "true"; } catch (error) { - console.warn('[useOnboardingOrchestrator] Failed to parse account settings JSON:', error); + console.warn( + "[useOnboardingOrchestrator] Failed to parse account settings JSON:", + error, + ); return false; } } @@ -152,7 +170,7 @@ export interface UseOnboardingOrchestratorOptions { } export function useOnboardingOrchestrator( - options?: UseOnboardingOrchestratorOptions + options?: UseOnboardingOrchestratorOptions, ): UseOnboardingOrchestratorResult { const defaultState = options?.defaultRuntimeState ?? DEFAULT_RUNTIME_STATE; const serverExperience = useServerExperience(); @@ -161,7 +179,7 @@ export function useOnboardingOrchestrator( const bypassOnboarding = useBypassOnboarding(); const [runtimeState, setRuntimeState] = useState(() => - getInitialRuntimeState(defaultState) + getInitialRuntimeState(defaultState), ); const [isPaused, setIsPaused] = useState(false); const [isInitialized, setIsInitialized] = useState(false); @@ -186,10 +204,11 @@ export function useOnboardingOrchestrator( totalUsers: serverExperience.totalUsers, freeTierLimit: serverExperience.freeTierLimit, isOverLimit: serverExperience.overFreeTierLimit ?? false, - requiresLicense: !serverExperience.hasPaidLicense && ( - serverExperience.overFreeTierLimit === true || - (serverExperience.effectiveIsAdmin && serverExperience.userCountResolved) - ), + requiresLicense: + !serverExperience.hasPaidLicense && + (serverExperience.overFreeTierLimit === true || + (serverExperience.effectiveIsAdmin && + serverExperience.userCountResolved)), }, })); }, [ @@ -220,7 +239,10 @@ export function useOnboardingOrchestrator( requiresMfaSetup: parseMfaRequired(accountData.settings), })); } catch (error) { - console.log('[OnboardingOrchestrator] Failed to fetch account data for onboarding runtime state:', error); + console.log( + "[OnboardingOrchestrator] Failed to fetch account data for onboarding runtime state:", + error, + ); // Account endpoint failed - user not logged in or security disabled } }; @@ -230,29 +252,39 @@ export function useOnboardingOrchestrator( } }, [config?.enableLogin, configLoading]); - const isOnAuthRoute = AUTH_ROUTES.some((route) => location.pathname.startsWith(route)); + const isOnAuthRoute = AUTH_ROUTES.some((route) => + location.pathname.startsWith(route), + ); const loginEnabled = config?.enableLogin === true; const isUnauthenticatedWithLoginEnabled = loginEnabled && !hasAuthToken(); const shouldBlockOnboarding = - bypassOnboarding || isOnAuthRoute || configLoading || isUnauthenticatedWithLoginEnabled; + bypassOnboarding || + isOnAuthRoute || + configLoading || + isUnauthenticatedWithLoginEnabled; - const conditionContext = useMemo(() => ({ - ...serverExperience, - ...runtimeState, - effectiveIsAdmin: serverExperience.effectiveIsAdmin || - (!serverExperience.loginEnabled && runtimeState.selectedRole === 'admin'), - }), [serverExperience, runtimeState]); + const conditionContext = useMemo( + () => ({ + ...serverExperience, + ...runtimeState, + effectiveIsAdmin: + serverExperience.effectiveIsAdmin || + (!serverExperience.loginEnabled && + runtimeState.selectedRole === "admin"), + }), + [serverExperience, runtimeState], + ); const activeFlow = useMemo(() => { return ONBOARDING_STEPS.filter((step) => step.condition(conditionContext)); }, [conditionContext]); // Wait for config AND admin status before calculating initial step - const adminStatusResolved = !configLoading && ( - config?.enableLogin === false || - config?.enableLogin === undefined || - config?.isAdmin !== undefined - ); + const adminStatusResolved = + !configLoading && + (config?.enableLogin === false || + config?.enableLogin === undefined || + config?.isAdmin !== undefined); useEffect(() => { if (configLoading || !adminStatusResolved) return; @@ -280,14 +312,27 @@ export function useOnboardingOrchestrator( const totalSteps = activeFlow.length; - const isComplete = isInitialized && - (totalSteps === 0 || currentStepIndex >= totalSteps || isOnboardingCompleted()); - const currentStep = (currentStepIndex >= 0 && currentStepIndex < totalSteps) - ? activeFlow[currentStepIndex] - : null; - const isActive = !shouldBlockOnboarding && !isPaused && !isComplete && isInitialized && currentStep !== null; - const isLoading = configLoading || !adminStatusResolved || !isInitialized || - !initialIndexSet.current || (currentStepIndex === -1 && activeFlow.length > 0); + const isComplete = + isInitialized && + (totalSteps === 0 || + currentStepIndex >= totalSteps || + isOnboardingCompleted()); + const currentStep = + currentStepIndex >= 0 && currentStepIndex < totalSteps + ? activeFlow[currentStepIndex] + : null; + const isActive = + !shouldBlockOnboarding && + !isPaused && + !isComplete && + isInitialized && + currentStep !== null; + const isLoading = + configLoading || + !adminStatusResolved || + !isInitialized || + !initialIndexSet.current || + (currentStepIndex === -1 && activeFlow.length > 0); useEffect(() => { if (!configLoading && !isInitialized) setIsInitialized(true); @@ -325,24 +370,29 @@ export function useOnboardingOrchestrator( setCurrentStepIndex(nextIndex); }, [currentStepIndex, totalSteps]); - - const updateRuntimeState = useCallback((updates: Partial) => { - persistRuntimeState(updates); - setRuntimeState((prev) => ({ ...prev, ...updates })); - }, []); + const updateRuntimeState = useCallback( + (updates: Partial) => { + persistRuntimeState(updates); + setRuntimeState((prev) => ({ ...prev, ...updates })); + }, + [], + ); const refreshFlow = useCallback(() => { initialIndexSet.current = false; setCurrentStepIndex(-1); }, []); - const startStep = useCallback((stepId: OnboardingStepId) => { - const index = activeFlow.findIndex((step) => step.id === stepId); - if (index !== -1) { - setCurrentStepIndex(index); - setIsPaused(false); - } - }, [activeFlow]); + const startStep = useCallback( + (stepId: OnboardingStepId) => { + const index = activeFlow.findIndex((step) => step.id === stepId); + if (index !== -1) { + setCurrentStepIndex(index); + setIsPaused(false); + } + }, + [activeFlow], + ); const pause = useCallback(() => setIsPaused(true), []); const resume = useCallback(() => setIsPaused(false), []); diff --git a/frontend/src/core/components/onboarding/slides/AnalyticsChoiceSlide.tsx b/frontend/src/core/components/onboarding/slides/AnalyticsChoiceSlide.tsx index c04009ca4c..6960b88c27 100644 --- a/frontend/src/core/components/onboarding/slides/AnalyticsChoiceSlide.tsx +++ b/frontend/src/core/components/onboarding/slides/AnalyticsChoiceSlide.tsx @@ -1,20 +1,25 @@ -import React from 'react'; -import { Trans } from 'react-i18next'; -import { Button } from '@mantine/core'; -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import i18n from '@app/i18n'; -import { SlideConfig } from '@app/types/types'; -import { UNIFIED_CIRCLE_CONFIG } from '@app/components/onboarding/slides/unifiedBackgroundConfig'; -import styles from '@app/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css'; +import React from "react"; +import { Trans } from "react-i18next"; +import { Button } from "@mantine/core"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; +import i18n from "@app/i18n"; +import { SlideConfig } from "@app/types/types"; +import { UNIFIED_CIRCLE_CONFIG } from "@app/components/onboarding/slides/unifiedBackgroundConfig"; +import styles from "@app/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css"; interface AnalyticsChoiceSlideProps { analyticsError?: string | null; } -export default function AnalyticsChoiceSlide({ analyticsError }: AnalyticsChoiceSlideProps): SlideConfig { +export default function AnalyticsChoiceSlide({ + analyticsError, +}: AnalyticsChoiceSlideProps): SlideConfig { return { - key: 'analytics-choice', - title: i18n.t('analytics.title', 'Do you want to help make Stirling PDF better?'), + key: "analytics-choice", + title: i18n.t( + "analytics.title", + "Do you want to help make Stirling PDF better?", + ), body: (
}} />
-
+
{analyticsError && ( -
+
{analyticsError}
)}
), background: { - gradientStops: ['#0EA5E9', '#6366F1'], + gradientStops: ["#0EA5E9", "#6366F1"], circles: UNIFIED_CIRCLE_CONFIG, }, }; } - diff --git a/frontend/src/core/components/onboarding/slides/AnimatedSlideBackground.module.css b/frontend/src/core/components/onboarding/slides/AnimatedSlideBackground.module.css index 5e5799ec2d..e278703955 100644 --- a/frontend/src/core/components/onboarding/slides/AnimatedSlideBackground.module.css +++ b/frontend/src/core/components/onboarding/slides/AnimatedSlideBackground.module.css @@ -65,6 +65,10 @@ transform: translate3d(0, 0, 0); } 100% { - transform: translate3d(var(--circle-move-x, 40px), var(--circle-move-y, 24px), 0); + transform: translate3d( + var(--circle-move-x, 40px), + var(--circle-move-y, 24px), + 0 + ); } } diff --git a/frontend/src/core/components/onboarding/slides/AnimatedSlideBackground.tsx b/frontend/src/core/components/onboarding/slides/AnimatedSlideBackground.tsx index 289b07b6c4..56cbb0dd11 100644 --- a/frontend/src/core/components/onboarding/slides/AnimatedSlideBackground.tsx +++ b/frontend/src/core/components/onboarding/slides/AnimatedSlideBackground.tsx @@ -1,12 +1,12 @@ -import React from 'react'; -import styles from '@app/components/onboarding/slides/AnimatedSlideBackground.module.css'; -import { AnimatedSlideBackgroundProps } from '@app/types/types'; +import React from "react"; +import styles from "@app/components/onboarding/slides/AnimatedSlideBackground.module.css"; +import { AnimatedSlideBackgroundProps } from "@app/types/types"; type CircleStyles = React.CSSProperties & { - '--circle-move-x'?: string; - '--circle-move-y'?: string; - '--circle-duration'?: string; - '--circle-delay'?: string; + "--circle-move-x"?: string; + "--circle-move-y"?: string; + "--circle-duration"?: string; + "--circle-delay"?: string; }; interface AnimatedSlideBackgroundComponentProps extends AnimatedSlideBackgroundProps { @@ -19,8 +19,11 @@ export default function AnimatedSlideBackground({ circles, isActive, }: AnimatedSlideBackgroundComponentProps) { - const [prevGradient, setPrevGradient] = React.useState<[string, string] | null>(null); - const [currentGradient, setCurrentGradient] = React.useState<[string, string]>(gradientStops); + const [prevGradient, setPrevGradient] = React.useState< + [string, string] | null + >(null); + const [currentGradient, setCurrentGradient] = + React.useState<[string, string]>(gradientStops); const [isTransitioning, setIsTransitioning] = React.useState(false); const isFirstMount = React.useRef(true); @@ -31,13 +34,16 @@ export default function AnimatedSlideBackground({ setCurrentGradient(gradientStops); return; } - + // Only transition if gradient actually changed - if (currentGradient[0] !== gradientStops[0] || currentGradient[1] !== gradientStops[1]) { + if ( + currentGradient[0] !== gradientStops[0] || + currentGradient[1] !== gradientStops[1] + ) { // Store previous gradient and start transition setPrevGradient(currentGradient); setIsTransitioning(true); - + // Update to new gradient (will fade in) setCurrentGradient(gradientStops); } @@ -59,8 +65,8 @@ export default function AnimatedSlideBackground({ return (
{prevGradientStyle && isTransitioning && ( -
{ setPrevGradient(null); @@ -69,14 +75,24 @@ export default function AnimatedSlideBackground({ /> )}
{circles.map((circle, index) => { - const { position, size, color, opacity, blur, amplitude = 48, duration = 15, delay = 0 } = circle; + const { + position, + size, + color, + opacity, + blur, + amplitude = 48, + duration = 15, + delay = 0, + } = circle; - const moveX = position === 'bottom-left' ? amplitude : -amplitude; - const moveY = position === 'bottom-left' ? -amplitude * 0.6 : amplitude * 0.6; + const moveX = position === "bottom-left" ? amplitude : -amplitude; + const moveY = + position === "bottom-left" ? -amplitude * 0.6 : amplitude * 0.6; const circleStyle: CircleStyles = { width: size, @@ -84,17 +100,17 @@ export default function AnimatedSlideBackground({ background: color, opacity: opacity ?? 0.9, filter: blur ? `blur(${blur}px)` : undefined, - '--circle-move-x': `${moveX}px`, - '--circle-move-y': `${moveY}px`, - '--circle-duration': `${duration}s`, - '--circle-delay': `${delay}s`, + "--circle-move-x": `${moveX}px`, + "--circle-move-y": `${moveY}px`, + "--circle-duration": `${duration}s`, + "--circle-delay": `${delay}s`, }; const defaultOffset = -size / 2; const offsetX = circle.offsetX ?? 0; const offsetY = circle.offsetY ?? 0; - if (position === 'bottom-left') { + if (position === "bottom-left") { circleStyle.left = `${defaultOffset + offsetX}px`; circleStyle.bottom = `${defaultOffset + offsetY}px`; } else { diff --git a/frontend/src/core/components/onboarding/slides/DesktopInstallSlide.tsx b/frontend/src/core/components/onboarding/slides/DesktopInstallSlide.tsx index 9cebfbb9cd..eb3e743527 100644 --- a/frontend/src/core/components/onboarding/slides/DesktopInstallSlide.tsx +++ b/frontend/src/core/components/onboarding/slides/DesktopInstallSlide.tsx @@ -1,8 +1,11 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { SlideConfig } from '@app/types/types'; -import { UNIFIED_CIRCLE_CONFIG } from '@app/components/onboarding/slides/unifiedBackgroundConfig'; -import { DesktopInstallTitle, type OSOption } from '@app/components/onboarding/slides/DesktopInstallTitle'; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { SlideConfig } from "@app/types/types"; +import { UNIFIED_CIRCLE_CONFIG } from "@app/components/onboarding/slides/unifiedBackgroundConfig"; +import { + DesktopInstallTitle, + type OSOption, +} from "@app/components/onboarding/slides/DesktopInstallTitle"; export type { OSOption }; @@ -19,8 +22,8 @@ const DesktopInstallBody = () => { return ( {t( - '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.', + "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.", )} ); @@ -32,11 +35,10 @@ export default function DesktopInstallSlide({ osOptions = [], onDownloadUrlChange, }: DesktopInstallSlideProps): SlideConfig { - return { - key: 'desktop-install', + key: "desktop-install", title: ( - , downloadUrl: osUrl, background: { - gradientStops: ['#2563EB', '#0EA5E9'], + gradientStops: ["#2563EB", "#0EA5E9"], circles: UNIFIED_CIRCLE_CONFIG, }, }; } - diff --git a/frontend/src/core/components/onboarding/slides/DesktopInstallTitle.tsx b/frontend/src/core/components/onboarding/slides/DesktopInstallTitle.tsx index ac42b518b3..7362774017 100644 --- a/frontend/src/core/components/onboarding/slides/DesktopInstallTitle.tsx +++ b/frontend/src/core/components/onboarding/slides/DesktopInstallTitle.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { Menu, ActionIcon } from '@mantine/core'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Menu, ActionIcon } from "@mantine/core"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; export interface OSOption { label: string; @@ -16,11 +16,11 @@ interface DesktopInstallTitleProps { onDownloadUrlChange?: (url: string) => void; } -export const DesktopInstallTitle: React.FC = ({ - osLabel, - osUrl, - osOptions, - onDownloadUrlChange +export const DesktopInstallTitle: React.FC = ({ + osLabel, + osUrl, + osOptions, + onDownloadUrlChange, }) => { const { t } = useTranslation(); const [selectedOsUrl, setSelectedOsUrl] = React.useState(osUrl); @@ -29,37 +29,51 @@ export const DesktopInstallTitle: React.FC = ({ setSelectedOsUrl(osUrl); }, [osUrl]); - const handleOsSelect = React.useCallback((option: OSOption) => { - setSelectedOsUrl(option.url); - onDownloadUrlChange?.(option.url); - }, [onDownloadUrlChange]); + const handleOsSelect = React.useCallback( + (option: OSOption) => { + setSelectedOsUrl(option.url); + onDownloadUrlChange?.(option.url); + }, + [onDownloadUrlChange], + ); - const currentOsOption = osOptions.find(opt => opt.url === selectedOsUrl) || + const currentOsOption = + osOptions.find((opt) => opt.url === selectedOsUrl) || (osOptions.length > 0 ? osOptions[0] : { label: osLabel, url: osUrl }); - + const displayLabel = currentOsOption.label || osLabel; - const title = displayLabel - ? t('onboarding.desktopInstall.titleWithOs', 'Download for {{osLabel}}', { osLabel: displayLabel }) - : t('onboarding.desktopInstall.title', 'Download'); + const title = displayLabel + ? t("onboarding.desktopInstall.titleWithOs", "Download for {{osLabel}}", { + osLabel: displayLabel, + }) + : t("onboarding.desktopInstall.title", "Download"); // If only one option or no options, don't show dropdown if (osOptions.length <= 1) { - return
{title}
; + return
{title}
; } return ( -
- {title} +
+ {title} @@ -74,11 +88,11 @@ export const DesktopInstallTitle: React.FC = ({ onClick={() => handleOsSelect(option)} style={{ backgroundColor: isSelected - ? 'light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))' - : 'transparent', + ? "light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))" + : "transparent", color: isSelected - ? 'light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))' - : 'inherit', + ? "light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))" + : "inherit", }} > {option.label} @@ -90,4 +104,3 @@ export const DesktopInstallTitle: React.FC = ({
); }; - diff --git a/frontend/src/core/components/onboarding/slides/FirstLoginSlide.tsx b/frontend/src/core/components/onboarding/slides/FirstLoginSlide.tsx index 46ec6a0c89..4898c5a825 100644 --- a/frontend/src/core/components/onboarding/slides/FirstLoginSlide.tsx +++ b/frontend/src/core/components/onboarding/slides/FirstLoginSlide.tsx @@ -1,12 +1,12 @@ -import React, { useState } from 'react'; -import { Stack, PasswordInput, Button, Alert, Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { SlideConfig } from '@app/types/types'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { UNIFIED_CIRCLE_CONFIG } from '@app/components/onboarding/slides/unifiedBackgroundConfig'; -import { accountService } from '@app/services/accountService'; -import { alert as showToast } from '@app/components/toast'; -import styles from '@app/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css'; +import React, { useState } from "react"; +import { Stack, PasswordInput, Button, Alert, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { SlideConfig } from "@app/types/types"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { UNIFIED_CIRCLE_CONFIG } from "@app/components/onboarding/slides/unifiedBackgroundConfig"; +import { accountService } from "@app/services/accountService"; +import { alert as showToast } from "@app/components/toast"; +import styles from "@app/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css"; interface FirstLoginSlideProps { username: string; @@ -14,66 +14,98 @@ interface FirstLoginSlideProps { usingDefaultCredentials?: boolean; } -const DEFAULT_PASSWORD = 'stirling'; +const DEFAULT_PASSWORD = "stirling"; -function FirstLoginForm({ username, onPasswordChanged, usingDefaultCredentials = false }: FirstLoginSlideProps) { +function FirstLoginForm({ + username, + onPasswordChanged, + usingDefaultCredentials = false, +}: FirstLoginSlideProps) { const { t } = useTranslation(); // If using default credentials, pre-fill with "stirling" - user won't see this field - const [currentPassword, setCurrentPassword] = useState(usingDefaultCredentials ? DEFAULT_PASSWORD : ''); - const [newPassword, setNewPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); + const [currentPassword, setCurrentPassword] = useState( + usingDefaultCredentials ? DEFAULT_PASSWORD : "", + ); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); + const [error, setError] = useState(""); const handleSubmit = async () => { // Validation - if ((!usingDefaultCredentials && !currentPassword) || !newPassword || !confirmPassword) { - setError(t('firstLogin.allFieldsRequired', 'All fields are required')); + if ( + (!usingDefaultCredentials && !currentPassword) || + !newPassword || + !confirmPassword + ) { + setError(t("firstLogin.allFieldsRequired", "All fields are required")); return; } if (newPassword !== confirmPassword) { - setError(t('firstLogin.passwordsDoNotMatch', 'New passwords do not match')); + setError( + t("firstLogin.passwordsDoNotMatch", "New passwords do not match"), + ); return; } if (newPassword.length < 8) { - setError(t('firstLogin.passwordTooShort', 'Password must be at least 8 characters')); + setError( + t( + "firstLogin.passwordTooShort", + "Password must be at least 8 characters", + ), + ); return; } if (newPassword === currentPassword) { - setError(t('firstLogin.passwordMustBeDifferent', 'New password must be different from current password')); + setError( + t( + "firstLogin.passwordMustBeDifferent", + "New password must be different from current password", + ), + ); return; } try { setLoading(true); - setError(''); + setError(""); - await accountService.changePasswordOnLogin(currentPassword, newPassword, confirmPassword); + await accountService.changePasswordOnLogin( + currentPassword, + newPassword, + confirmPassword, + ); showToast({ - alertType: 'success', - title: t('firstLogin.passwordChangedSuccess', 'Password changed successfully! Please log in again.') + alertType: "success", + title: t( + "firstLogin.passwordChangedSuccess", + "Password changed successfully! Please log in again.", + ), }); // Clear form - setCurrentPassword(''); - setNewPassword(''); - setConfirmPassword(''); + setCurrentPassword(""); + setNewPassword(""); + setConfirmPassword(""); // Wait a moment for the user to see the success message setTimeout(() => { onPasswordChanged(); }, 1500); } catch (err) { - console.error('Failed to change password:', err); + console.error("Failed to change password:", err); // Extract error message from axios response if available const axiosError = err as { response?: { data?: { message?: string } } }; setError( axiosError.response?.data?.message || - t('firstLogin.passwordChangeFailed', 'Failed to change password. Please check your current password.') + t( + "firstLogin.passwordChangeFailed", + "Failed to change password. Please check your current password.", + ), ); } finally { setLoading(false); @@ -85,22 +117,30 @@ function FirstLoginForm({ username, onPasswordChanged, usingDefaultCredentials =
- + {t( - 'firstLogin.welcomeMessage', - 'For security reasons, you must change your password on your first login.' + "firstLogin.welcomeMessage", + "For security reasons, you must change your password on your first login.", )}
- {t('firstLogin.loggedInAs', 'Logged in as')}: {username} + {t("firstLogin.loggedInAs", "Logged in as")}:{" "} + {username} {error && ( } + icon={ + + } color="red" variant="light" > @@ -111,8 +151,11 @@ function FirstLoginForm({ username, onPasswordChanged, usingDefaultCredentials = {/* Only show current password field if not using default credentials */} {!usingDefaultCredentials && ( setCurrentPassword(e.currentTarget.value)} required @@ -123,8 +166,11 @@ function FirstLoginForm({ username, onPasswordChanged, usingDefaultCredentials = )} setNewPassword(e.currentTarget.value)} minLength={8} @@ -135,8 +181,11 @@ function FirstLoginForm({ username, onPasswordChanged, usingDefaultCredentials = /> setConfirmPassword(e.currentTarget.value)} required @@ -150,11 +199,16 @@ function FirstLoginForm({ username, onPasswordChanged, usingDefaultCredentials = fullWidth onClick={handleSubmit} loading={loading} - disabled={!newPassword || !confirmPassword || newPassword.length < 8 || confirmPassword.length < 8} + disabled={ + !newPassword || + !confirmPassword || + newPassword.length < 8 || + confirmPassword.length < 8 + } size="md" mt="xs" > - {t('firstLogin.changePassword', 'Change Password')} + {t("firstLogin.changePassword", "Change Password")}
@@ -168,8 +222,8 @@ export default function FirstLoginSlide({ usingDefaultCredentials = false, }: FirstLoginSlideProps): SlideConfig { return { - key: 'first-login', - title: 'Set Your Password', + key: "first-login", + title: "Set Your Password", body: ( ), background: { - gradientStops: ['#059669', '#0891B2'], // Green to teal - security/trust colors + gradientStops: ["#059669", "#0891B2"], // Green to teal - security/trust colors circles: UNIFIED_CIRCLE_CONFIG, }, }; } - diff --git a/frontend/src/core/components/onboarding/slides/MFASetupSlide.tsx b/frontend/src/core/components/onboarding/slides/MFASetupSlide.tsx index d8def0528a..94bba3010d 100644 --- a/frontend/src/core/components/onboarding/slides/MFASetupSlide.tsx +++ b/frontend/src/core/components/onboarding/slides/MFASetupSlide.tsx @@ -1,10 +1,25 @@ -import { useCallback, useEffect, useRef, useState, type FormEvent } from "react"; -import { Alert, Box, Button, Group, Loader, Stack, Text, TextInput } from "@mantine/core"; +import { + useCallback, + useEffect, + useRef, + useState, + type FormEvent, +} from "react"; +import { + Alert, + Box, + Button, + Group, + Loader, + Stack, + Text, + TextInput, +} from "@mantine/core"; import { QRCodeSVG } from "qrcode.react"; import { SlideConfig } from "@app/types/types"; import { UNIFIED_CIRCLE_CONFIG } from "@app/components/onboarding/slides/unifiedBackgroundConfig"; import { accountService } from "@app/services/accountService"; -import { useAccountLogout } from '@app/extensions/accountLogout'; +import { useAccountLogout } from "@app/extensions/accountLogout"; import { useAuth } from "@app/auth/UseSession"; import LocalIcon from "@app/components/shared/LocalIcon"; import { BASE_PATH } from "@app/constants/app"; @@ -16,7 +31,9 @@ interface MFASetupSlideProps { } function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) { - const [mfaSetupData, setMfaSetupData] = useState(null); + const [mfaSetupData, setMfaSetupData] = useState( + null, + ); const [mfaSetupCode, setMfaSetupCode] = useState(""); const [mfaError, setMfaError] = useState(""); const [mfaLoading, setMfaLoading] = useState(false); @@ -27,7 +44,10 @@ function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) { const accountLogout = useAccountLogout(); const qrLogoSrc = `${BASE_PATH}/modern-logo/StirlingPDFLogoNoTextDark.svg`; - const normalizeMfaCode = useCallback((value: string) => value.replace(/\D/g, "").slice(0, 6), []); + const normalizeMfaCode = useCallback( + (value: string) => value.replace(/\D/g, "").slice(0, 6), + [], + ); const fetchMfaSetup = useCallback(async () => { try { @@ -38,7 +58,10 @@ function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) { setMfaSetupData(data); } catch (err) { const axiosError = err as { response?: { data?: { error?: string } } }; - setMfaError(axiosError.response?.data?.error || "Unable to start two-factor setup. Please try again."); + setMfaError( + axiosError.response?.data?.error || + "Unable to start two-factor setup. Please try again.", + ); } finally { setMfaLoading(false); } @@ -59,10 +82,10 @@ function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) { }, [fetchMfaSetup]); const redirectToLogin = useCallback(() => { - window.location.assign('/login'); + window.location.assign("/login"); }, []); - const onLogout = useCallback(async() => { + const onLogout = useCallback(async () => { await accountLogout({ signOut, redirectToLogin }); }, [accountLogout, redirectToLogin, signOut]); @@ -84,13 +107,14 @@ function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) { } catch (err) { const axiosError = err as { response?: { data?: { error?: string } } }; setMfaError( - axiosError.response?.data?.error || "Unable to enable two-factor authentication. Check the code and try again." + axiosError.response?.data?.error || + "Unable to enable two-factor authentication. Check the code and try again.", ); } finally { setSubmitting(false); } }, - [mfaSetupCode, onMfaSetupComplete] + [mfaSetupCode, onMfaSetupComplete], ); const isReady = Boolean(mfaSetupData); @@ -137,12 +161,17 @@ function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) {
- 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. + 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. {mfaError && ( - } color="red" variant="light"> + } + color="red" + variant="light" + > {mfaError} )} @@ -163,7 +192,9 @@ function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) { label="Authentication code" placeholder="123456" value={mfaSetupCode} - onChange={(event) => setMfaSetupCode(normalizeMfaCode(event.currentTarget.value))} + onChange={(event) => + setMfaSetupCode(normalizeMfaCode(event.currentTarget.value)) + } inputMode="numeric" maxLength={6} minLength={6} @@ -182,15 +213,13 @@ function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) { - @@ -208,7 +237,9 @@ function MFASetupContent({ onMfaSetupComplete }: MFASetupSlideProps) { ); } -export default function MFASetupSlide({ onMfaSetupComplete }: MFASetupSlideProps = {}): SlideConfig { +export default function MFASetupSlide({ + onMfaSetupComplete, +}: MFASetupSlideProps = {}): SlideConfig { return { key: "mfa-setup-slide", title: "Multi-Factor Authentication Setup", diff --git a/frontend/src/core/components/onboarding/slides/PlanOverviewSlide.tsx b/frontend/src/core/components/onboarding/slides/PlanOverviewSlide.tsx index 3b8d4bfb0c..81f7911e7d 100644 --- a/frontend/src/core/components/onboarding/slides/PlanOverviewSlide.tsx +++ b/frontend/src/core/components/onboarding/slides/PlanOverviewSlide.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import { Trans, useTranslation } from 'react-i18next'; -import { SlideConfig, LicenseNotice } from '@app/types/types'; -import { UNIFIED_CIRCLE_CONFIG } from '@app/components/onboarding/slides/unifiedBackgroundConfig'; +import React from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { SlideConfig, LicenseNotice } from "@app/types/types"; +import { UNIFIED_CIRCLE_CONFIG } from "@app/components/onboarding/slides/unifiedBackgroundConfig"; interface PlanOverviewSlideProps { isAdmin: boolean; @@ -16,23 +16,23 @@ const PlanOverviewTitle: React.FC<{ isAdmin: boolean }> = ({ isAdmin }) => { return ( <> {isAdmin - ? t('onboarding.planOverview.adminTitle', 'Admin Overview') - : t('onboarding.planOverview.userTitle', 'Plan Overview')} + ? t("onboarding.planOverview.adminTitle", "Admin Overview") + : t("onboarding.planOverview.userTitle", "Plan Overview")} ); }; -const AdminOverviewBody: React.FC<{ freeTierLimit: number; loginEnabled: boolean }> = ({ - freeTierLimit, - loginEnabled, -}) => { +const AdminOverviewBody: React.FC<{ + freeTierLimit: number; + loginEnabled: boolean; +}> = ({ freeTierLimit, loginEnabled }) => { const adminBodyKey = loginEnabled - ? 'onboarding.planOverview.adminBodyLoginEnabled' - : 'onboarding.planOverview.adminBodyLoginDisabled'; + ? "onboarding.planOverview.adminBodyLoginEnabled" + : "onboarding.planOverview.adminBodyLoginDisabled"; const defaultValue = loginEnabled - ? 'As an admin, you can manage users, configure settings, and monitor server health. The first {{freeTierLimit}} people on your server get to use Stirling free of charge.' - : '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.'; + ? "As an admin, you can manage users, configure settings, and monitor server health. The first {{freeTierLimit}} people on your server get to use Stirling free of charge." + : "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."; return ( { return ( {t( - 'onboarding.planOverview.userBody', + "onboarding.planOverview.userBody", "Invite teammates, assign roles, and keep your documents organized in one secure workspace. Enable login mode whenever you're ready to grow beyond solo use.", )} ); }; -const PlanOverviewBody: React.FC<{ isAdmin: boolean; freeTierLimit: number; loginEnabled: boolean }> = ({ - isAdmin, - freeTierLimit, - loginEnabled, -}) => - isAdmin ? : ; +const PlanOverviewBody: React.FC<{ + isAdmin: boolean; + freeTierLimit: number; + loginEnabled: boolean; +}> = ({ isAdmin, freeTierLimit, loginEnabled }) => + isAdmin ? ( + + ) : ( + + ); export default function PlanOverviewSlide({ isAdmin, @@ -71,13 +78,18 @@ export default function PlanOverviewSlide({ const freeTierLimit = licenseNotice?.freeTierLimit ?? DEFAULT_FREE_TIER_LIMIT; return { - key: isAdmin ? 'admin-overview' : 'plan-overview', + key: isAdmin ? "admin-overview" : "plan-overview", title: , - body: , + body: ( + + ), background: { - gradientStops: isAdmin ? ['#4F46E5', '#0EA5E9'] : ['#F97316', '#EF4444'], + gradientStops: isAdmin ? ["#4F46E5", "#0EA5E9"] : ["#F97316", "#EF4444"], circles: UNIFIED_CIRCLE_CONFIG, }, }; } - diff --git a/frontend/src/core/components/onboarding/slides/SecurityCheckSlide.tsx b/frontend/src/core/components/onboarding/slides/SecurityCheckSlide.tsx index 0efb2f591d..78ed4d6db4 100644 --- a/frontend/src/core/components/onboarding/slides/SecurityCheckSlide.tsx +++ b/frontend/src/core/components/onboarding/slides/SecurityCheckSlide.tsx @@ -1,14 +1,14 @@ -import React from 'react'; -import { Select } from '@mantine/core'; -import { SlideConfig } from '@app/types/types'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { UNIFIED_CIRCLE_CONFIG } from '@app/components/onboarding/slides/unifiedBackgroundConfig'; -import i18n from '@app/i18n'; -import styles from '@app/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css'; +import React from "react"; +import { Select } from "@mantine/core"; +import { SlideConfig } from "@app/types/types"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { UNIFIED_CIRCLE_CONFIG } from "@app/components/onboarding/slides/unifiedBackgroundConfig"; +import i18n from "@app/i18n"; +import styles from "@app/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css"; interface SecurityCheckSlideProps { - selectedRole: 'admin' | 'user' | null; - onRoleSelect: (role: 'admin' | 'user' | null) => void; + selectedRole: "admin" | "user" | null; + onRoleSelect: (role: "admin" | "user" | null) => void; } export default function SecurityCheckSlide({ @@ -16,24 +16,36 @@ export default function SecurityCheckSlide({ onRoleSelect, }: SecurityCheckSlideProps): SlideConfig { return { - key: 'security-check', - title: 'Security Check', + key: "security-check", + title: "Security Check", body: (
- - {i18n.t('onboarding.securityCheck.message', 'The application has undergone significant changes recently. Your server admin\'s attention may be required. Please confirm your role to continue.')} + + + {i18n.t( + "onboarding.securityCheck.message", + "The application has undergone significant changes recently. Your server admin's attention may be required. Please confirm your role to continue.", + )} +
setShareRole((value as typeof shareRole) || 'editor')} - comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_FILE_MANAGER_MODAL + 10 }} + onChange={(value) => + setShareRole((value as typeof shareRole) || "editor") + } + comboboxProps={{ + withinPortal: true, + zIndex: Z_INDEX_OVER_FILE_MANAGER_MODAL + 10, + }} data={[ - { value: 'editor', label: t('storageShare.roleEditor', 'Editor') }, - { value: 'commenter', label: t('storageShare.roleCommenter', 'Commenter') }, - { value: 'viewer', label: t('storageShare.roleViewer', 'Viewer') }, + { + value: "editor", + label: t("storageShare.roleEditor", "Editor"), + }, + { + value: "commenter", + label: t("storageShare.roleCommenter", "Commenter"), + }, + { + value: "viewer", + label: t("storageShare.roleViewer", "Viewer"), + }, ]} /> - {shareRole === 'commenter' && ( + {shareRole === "commenter" && ( - {t('storageShare.commenterHint', 'Commenting is coming soon.')} + {t("storageShare.commenterHint", "Commenting is coming soon.")} )} @@ -245,7 +298,7 @@ const BulkShareModal: React.FC = ({ diff --git a/frontend/src/core/components/shared/BulkUploadToServerModal.tsx b/frontend/src/core/components/shared/BulkUploadToServerModal.tsx index 4e97fe74c7..8dd250851e 100644 --- a/frontend/src/core/components/shared/BulkUploadToServerModal.tsx +++ b/frontend/src/core/components/shared/BulkUploadToServerModal.tsx @@ -1,15 +1,15 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Modal, Stack, Text, Button, Group, Alert } from '@mantine/core'; -import CloudUploadIcon from '@mui/icons-material/CloudUpload'; -import { useTranslation } from 'react-i18next'; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Modal, Stack, Text, Button, Group, Alert } from "@mantine/core"; +import CloudUploadIcon from "@mui/icons-material/CloudUpload"; +import { useTranslation } from "react-i18next"; -import { alert } from '@app/components/toast'; -import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; -import type { StirlingFileStub } from '@app/types/fileContext'; -import { uploadHistoryChains } from '@app/services/serverStorageUpload'; -import { fileStorage } from '@app/services/fileStorage'; -import { useFileActions } from '@app/contexts/FileContext'; -import type { FileId } from '@app/types/file'; +import { alert } from "@app/components/toast"; +import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import { uploadHistoryChains } from "@app/services/serverStorageUpload"; +import { fileStorage } from "@app/services/fileStorage"; +import { useFileActions } from "@app/contexts/FileContext"; +import type { FileId } from "@app/types/file"; interface BulkUploadToServerModalProps { opened: boolean; @@ -45,16 +45,21 @@ const BulkUploadToServerModal: React.FC = ({ try { const rootIds = Array.from( - new Set(files.map((file) => (file.originalFileId || file.id) as FileId)) + new Set( + files.map((file) => (file.originalFileId || file.id) as FileId), + ), ); const remoteIds = Array.from( - new Set(files.map((file) => file.remoteStorageId).filter(Boolean) as number[]) + new Set( + files.map((file) => file.remoteStorageId).filter(Boolean) as number[], + ), ); - const existingRemoteId = remoteIds.length === 1 ? remoteIds[0] : undefined; + const existingRemoteId = + remoteIds.length === 1 ? remoteIds[0] : undefined; const { remoteId, updatedAt, chain } = await uploadHistoryChains( rootIds, - existingRemoteId + existingRemoteId, ); for (const stub of chain) { @@ -73,8 +78,8 @@ const BulkUploadToServerModal: React.FC = ({ } alert({ - alertType: 'success', - title: t('storageUpload.success', 'Uploaded to server'), + alertType: "success", + title: t("storageUpload.success", "Uploaded to server"), expandable: false, durationMs: 3000, }); @@ -83,9 +88,12 @@ const BulkUploadToServerModal: React.FC = ({ } onClose(); } catch (error) { - console.error('Failed to upload files to server:', error); + console.error("Failed to upload files to server:", error); setErrorMessage( - t('storageUpload.failure', 'Upload failed. Please check your login and storage settings.') + t( + "storageUpload.failure", + "Upload failed. Please check your login and storage settings.", + ), ); } finally { setIsUploading(false); @@ -97,48 +105,51 @@ const BulkUploadToServerModal: React.FC = ({ opened={opened} onClose={onClose} centered - title={t('storageUpload.bulkTitle', 'Upload selected files')} + title={t("storageUpload.bulkTitle", "Upload selected files")} zIndex={Z_INDEX_OVER_FILE_MANAGER_MODAL} > {t( - 'storageUpload.bulkDescription', - 'This uploads the selected files to your server storage.' + "storageUpload.bulkDescription", + "This uploads the selected files to your server storage.", )} - {t('storageUpload.fileCount', '{{count}} files selected', { + {t("storageUpload.fileCount", "{{count}} files selected", { count: files.length, })} {displayNames.length > 0 && ( - {displayNames.join(', ')} + {displayNames.join(", ")} {fileNames.length > displayNames.length - ? t('storageUpload.more', ' +{{count}} more', { + ? t("storageUpload.more", " +{{count}} more", { count: fileNames.length - displayNames.length, }) - : ''} + : ""} )} {errorMessage && ( - + {errorMessage} )} diff --git a/frontend/src/core/components/shared/ButtonSelector.test.tsx b/frontend/src/core/components/shared/ButtonSelector.test.tsx index 12a509abd6..ab9fee8ef6 100644 --- a/frontend/src/core/components/shared/ButtonSelector.test.tsx +++ b/frontend/src/core/components/shared/ButtonSelector.test.tsx @@ -1,24 +1,24 @@ -import { describe, expect, test, vi, beforeEach } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/react'; -import { MantineProvider } from '@mantine/core'; -import ButtonSelector from '@app/components/shared/ButtonSelector'; +import { describe, expect, test, vi, beforeEach } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { MantineProvider } from "@mantine/core"; +import ButtonSelector from "@app/components/shared/ButtonSelector"; // Wrapper component to provide Mantine context const TestWrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); -describe('ButtonSelector', () => { +describe("ButtonSelector", () => { const mockOnChange = vi.fn(); beforeEach(() => { vi.clearAllMocks(); }); - test('should render all options as buttons', () => { + test("should render all options as buttons", () => { const options = [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, ]; render( @@ -29,18 +29,18 @@ describe('ButtonSelector', () => { options={options} label="Test Label" /> - + , ); - expect(screen.getByText('Test Label')).toBeInTheDocument(); - expect(screen.getByText('Option 1')).toBeInTheDocument(); - expect(screen.getByText('Option 2')).toBeInTheDocument(); + expect(screen.getByText("Test Label")).toBeInTheDocument(); + expect(screen.getByText("Option 1")).toBeInTheDocument(); + expect(screen.getByText("Option 2")).toBeInTheDocument(); }); - test('should highlight selected button with filled variant', () => { + test("should highlight selected button with filled variant", () => { const options = [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, ]; render( @@ -51,22 +51,22 @@ describe('ButtonSelector', () => { options={options} label="Selection Label" /> - + , ); - const selectedButton = screen.getByRole('button', { name: 'Option 1' }); - const unselectedButton = screen.getByRole('button', { name: 'Option 2' }); + const selectedButton = screen.getByRole("button", { name: "Option 1" }); + const unselectedButton = screen.getByRole("button", { name: "Option 2" }); // Check data-variant attribute for filled/outline - expect(selectedButton).toHaveAttribute('data-variant', 'filled'); - expect(unselectedButton).toHaveAttribute('data-variant', 'outline'); - expect(screen.getByText('Selection Label')).toBeInTheDocument(); + expect(selectedButton).toHaveAttribute("data-variant", "filled"); + expect(unselectedButton).toHaveAttribute("data-variant", "outline"); + expect(screen.getByText("Selection Label")).toBeInTheDocument(); }); - test('should call onChange when button is clicked', () => { + test("should call onChange when button is clicked", () => { const options = [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, ]; render( @@ -76,18 +76,18 @@ describe('ButtonSelector', () => { onChange={mockOnChange} options={options} /> - + , ); - fireEvent.click(screen.getByRole('button', { name: 'Option 2' })); + fireEvent.click(screen.getByRole("button", { name: "Option 2" })); - expect(mockOnChange).toHaveBeenCalledWith('option2'); + expect(mockOnChange).toHaveBeenCalledWith("option2"); }); - test('should handle undefined value (no selection)', () => { + test("should handle undefined value (no selection)", () => { const options = [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, ]; render( @@ -97,37 +97,37 @@ describe('ButtonSelector', () => { onChange={mockOnChange} options={options} /> - + , ); // Both buttons should be outlined when no value is selected - const button1 = screen.getByRole('button', { name: 'Option 1' }); - const button2 = screen.getByRole('button', { name: 'Option 2' }); + const button1 = screen.getByRole("button", { name: "Option 1" }); + const button2 = screen.getByRole("button", { name: "Option 2" }); - expect(button1).toHaveAttribute('data-variant', 'outline'); - expect(button2).toHaveAttribute('data-variant', 'outline'); + expect(button1).toHaveAttribute("data-variant", "outline"); + expect(button2).toHaveAttribute("data-variant", "outline"); }); test.each([ { - description: 'disable buttons when disabled prop is true', + description: "disable buttons when disabled prop is true", options: [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, ], globalDisabled: true, expectedStates: [true, true], }, { - description: 'disable individual options when option.disabled is true', + description: "disable individual options when option.disabled is true", options: [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2', disabled: true }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2", disabled: true }, ], globalDisabled: false, expectedStates: [false, true], }, - ])('should $description', ({ options, globalDisabled, expectedStates }) => { + ])("should $description", ({ options, globalDisabled, expectedStates }) => { render( { options={options} disabled={globalDisabled} /> - + , ); options.forEach((option, index) => { - const button = screen.getByRole('button', { name: option.label }); - expect(button).toHaveProperty('disabled', expectedStates[index]); + const button = screen.getByRole("button", { name: option.label }); + expect(button).toHaveProperty("disabled", expectedStates[index]); }); }); - test('should not call onChange when disabled button is clicked', () => { + test("should not call onChange when disabled button is clicked", () => { const options = [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2', disabled: true }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2", disabled: true }, ]; render( @@ -158,18 +158,18 @@ describe('ButtonSelector', () => { onChange={mockOnChange} options={options} /> - + , ); - fireEvent.click(screen.getByRole('button', { name: 'Option 2' })); + fireEvent.click(screen.getByRole("button", { name: "Option 2" })); expect(mockOnChange).not.toHaveBeenCalled(); }); - test('should not apply fullWidth styling when fullWidth is false', () => { + test("should not apply fullWidth styling when fullWidth is false", () => { const options = [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, ]; render( @@ -181,18 +181,18 @@ describe('ButtonSelector', () => { fullWidth={false} label="Layout Label" /> - + , ); - const button = screen.getByRole('button', { name: 'Option 1' }); - expect(button).not.toHaveStyle({ flex: '1' }); - expect(screen.getByText('Layout Label')).toBeInTheDocument(); + const button = screen.getByRole("button", { name: "Option 1" }); + expect(button).not.toHaveStyle({ flex: "1" }); + expect(screen.getByText("Layout Label")).toBeInTheDocument(); }); - test('should not render label element when not provided', () => { + test("should not render label element when not provided", () => { const options = [ - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' }, + { value: "option1", label: "Option 1" }, + { value: "option2", label: "Option 2" }, ]; const { container } = render( @@ -202,15 +202,17 @@ describe('ButtonSelector', () => { onChange={mockOnChange} options={options} /> - + , ); // Should render buttons - expect(screen.getByText('Option 1')).toBeInTheDocument(); - expect(screen.getByText('Option 2')).toBeInTheDocument(); - + expect(screen.getByText("Option 1")).toBeInTheDocument(); + expect(screen.getByText("Option 2")).toBeInTheDocument(); + // Stack should only contain the Group (buttons), no Text element for label - const stackElement = container.querySelector('[class*="mantine-Stack-root"]'); + const stackElement = container.querySelector( + '[class*="mantine-Stack-root"]', + ); expect(stackElement?.children).toHaveLength(1); // Only the Group, no label Text }); }); diff --git a/frontend/src/core/components/shared/ButtonSelector.tsx b/frontend/src/core/components/shared/ButtonSelector.tsx index 94bd10c6e7..73e18cac8a 100644 --- a/frontend/src/core/components/shared/ButtonSelector.tsx +++ b/frontend/src/core/components/shared/ButtonSelector.tsx @@ -5,7 +5,7 @@ export interface ButtonOption { value: T; label: string; disabled?: boolean; - tooltip?: string; // Tooltip shown on hover (useful for explaining why option is disabled) + tooltip?: string; // Tooltip shown on hover (useful for explaining why option is disabled) } interface ButtonSelectorProps { @@ -30,33 +30,43 @@ const ButtonSelector = ({ textClassName, }: ButtonSelectorProps) => { return ( - + {/* Label (if it exists) */} - {label && {label}} + {label && ( + + {label} + + )} {/* Buttons */} - + {options.map((option) => { const isDisabled = disabled || option.disabled; const button = ( ); diff --git a/frontend/src/core/components/shared/DropdownListWithFooter.tsx b/frontend/src/core/components/shared/DropdownListWithFooter.tsx index b5e5a9f5d7..830a10cd23 100644 --- a/frontend/src/core/components/shared/DropdownListWithFooter.tsx +++ b/frontend/src/core/components/shared/DropdownListWithFooter.tsx @@ -1,8 +1,16 @@ -import React, { ReactNode, useState, useMemo } from 'react'; -import { Stack, Text, Popover, Box, Checkbox, Group, TextInput } from '@mantine/core'; -import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore'; -import SearchIcon from '@mui/icons-material/Search'; -import { Z_INDEX_AUTOMATE_DROPDOWN } from '@app/styles/zIndex'; +import React, { ReactNode, useState, useMemo } from "react"; +import { + Stack, + Text, + Popover, + Box, + Checkbox, + Group, + TextInput, +} from "@mantine/core"; +import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore"; +import SearchIcon from "@mui/icons-material/Search"; +import { Z_INDEX_AUTOMATE_DROPDOWN } from "@app/styles/zIndex"; export interface DropdownItem { value: string; @@ -15,30 +23,30 @@ export interface DropdownListWithFooterProps { // Value and onChange - support both single and multi-select value: string | string[]; onChange: (value: string | string[]) => void; - + // Items and display items: DropdownItem[]; placeholder?: string; disabled?: boolean; - + // Labels and headers label?: string; header?: ReactNode; footer?: ReactNode; - + // Behavior multiSelect?: boolean; searchable?: boolean; maxHeight?: number; - + // Styling className?: string; dropdownClassName?: string; - + // Popover props - position?: 'top' | 'bottom' | 'left' | 'right'; + position?: "top" | "bottom" | "left" | "right"; withArrow?: boolean; - width?: 'target' | number; + width?: "target" | number; withinPortal?: boolean; zIndex?: number; } @@ -47,7 +55,7 @@ const DropdownListWithFooter: React.FC = ({ value, onChange, items, - placeholder = 'Select option', + placeholder = "Select option", disabled = false, label, header, @@ -55,34 +63,33 @@ const DropdownListWithFooter: React.FC = ({ multiSelect = false, searchable = false, maxHeight = 300, - className = '', - dropdownClassName = '', - position = 'bottom', + className = "", + dropdownClassName = "", + position = "bottom", withArrow = false, - width = 'target', + width = "target", withinPortal = true, - zIndex = Z_INDEX_AUTOMATE_DROPDOWN + zIndex = Z_INDEX_AUTOMATE_DROPDOWN, }) => { - - const [searchTerm, setSearchTerm] = useState(''); - + const [searchTerm, setSearchTerm] = useState(""); + const isMultiValue = Array.isArray(value); - const selectedValues = isMultiValue ? value : (value ? [value] : []); + const selectedValues = isMultiValue ? value : value ? [value] : []; // Filter items based on search term const filteredItems = useMemo(() => { if (!searchable || !searchTerm.trim()) { return items; } - return items.filter(item => - item.name.toLowerCase().includes(searchTerm.toLowerCase()) + return items.filter((item) => + item.name.toLowerCase().includes(searchTerm.toLowerCase()), ); }, [items, searchTerm, searchable]); const handleItemClick = (itemValue: string) => { if (multiSelect) { const newSelection = selectedValues.includes(itemValue) - ? selectedValues.filter(v => v !== itemValue) + ? selectedValues.filter((v) => v !== itemValue) : [...selectedValues, itemValue]; onChange(newSelection); } else { @@ -94,7 +101,9 @@ const DropdownListWithFooter: React.FC = ({ if (selectedValues.length === 0) { return placeholder; } else if (selectedValues.length === 1) { - const selectedItem = items.find(item => item.value === selectedValues[0]); + const selectedItem = items.find( + (item) => item.value === selectedValues[0], + ); return selectedItem?.name || selectedValues[0]; } else { return `${selectedValues.length} selected`; @@ -112,125 +121,144 @@ const DropdownListWithFooter: React.FC = ({ {label} )} - - searchable && setSearchTerm('')} + onClose={() => searchable && setSearchTerm("")} withinPortal={withinPortal} zIndex={zIndex} > {getDisplayText()} - + - + {header && ( - + {header} )} - + {searchable && ( - + } + leftSection={} size="sm" - style={{ width: '100%' }} + style={{ width: "100%" }} /> )} - - + + {filteredItems.length === 0 ? ( - + - {searchable && searchTerm ? 'No results found' : 'No items available'} + {searchable && searchTerm + ? "No results found" + : "No items available"} ) : ( filteredItems.map((item) => ( - !item.disabled && handleItemClick(item.value)} - style={{ - padding: '8px 12px', - cursor: item.disabled ? 'not-allowed' : 'pointer', - borderRadius: 'var(--mantine-radius-sm)', - opacity: item.disabled ? 0.5 : 1, - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between' - }} - onMouseEnter={(e) => { - if (!item.disabled) { - e.currentTarget.style.backgroundColor = 'light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5))'; + + !item.disabled && handleItemClick(item.value) } - }} - onMouseLeave={(e) => { - e.currentTarget.style.backgroundColor = 'transparent'; - }} - > - - {item.leftIcon && ( - - {item.leftIcon} - + style={{ + padding: "8px 12px", + cursor: item.disabled ? "not-allowed" : "pointer", + borderRadius: "var(--mantine-radius-sm)", + opacity: item.disabled ? 0.5 : 1, + display: "flex", + alignItems: "center", + justifyContent: "space-between", + }} + onMouseEnter={(e) => { + if (!item.disabled) { + e.currentTarget.style.backgroundColor = + "light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5))"; + } + }} + onMouseLeave={(e) => { + e.currentTarget.style.backgroundColor = "transparent"; + }} + > + + {item.leftIcon && ( + + {item.leftIcon} + + )} + {item.name} + + + {multiSelect && ( + {}} // Handled by parent onClick + size="sm" + disabled={item.disabled} + /> )} - {item.name} - - - {multiSelect && ( - {}} // Handled by parent onClick - size="sm" - disabled={item.disabled} - /> - )} - + )) )} - + {footer && ( - + {footer} )} @@ -241,4 +269,4 @@ const DropdownListWithFooter: React.FC = ({ ); }; -export default DropdownListWithFooter; \ No newline at end of file +export default DropdownListWithFooter; diff --git a/frontend/src/core/components/shared/EditableSecretField.tsx b/frontend/src/core/components/shared/EditableSecretField.tsx index dfd3da6458..2244989269 100644 --- a/frontend/src/core/components/shared/EditableSecretField.tsx +++ b/frontend/src/core/components/shared/EditableSecretField.tsx @@ -1,7 +1,13 @@ -import { useState, useRef, useEffect } from 'react'; -import { PasswordInput, Group, ActionIcon, Tooltip, TextInput } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import LocalIcon from '@app/components/shared/LocalIcon'; +import { useState, useRef, useEffect } from "react"; +import { + PasswordInput, + Group, + ActionIcon, + Tooltip, + TextInput, +} from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import LocalIcon from "@app/components/shared/LocalIcon"; interface EditableSecretFieldProps { label?: string; @@ -26,16 +32,16 @@ export default function EditableSecretField({ description, value, onChange, - placeholder = 'Enter value', + placeholder = "Enter value", disabled = false, error, }: EditableSecretFieldProps) { const { t } = useTranslation(); const [isEditing, setIsEditing] = useState(false); - const [tempValue, setTempValue] = useState(''); + const [tempValue, setTempValue] = useState(""); const inputRef = useRef(null); - const isMasked = value === '********'; + const isMasked = value === "********"; useEffect(() => { if (isEditing && inputRef.current) { @@ -44,38 +50,50 @@ export default function EditableSecretField({ }, [isEditing]); const handleEdit = () => { - setTempValue(''); + setTempValue(""); setIsEditing(true); }; const handleCancel = () => { - setTempValue(''); + setTempValue(""); setIsEditing(false); }; const handleSave = () => { - if (tempValue.trim() !== '') { + if (tempValue.trim() !== "") { onChange(tempValue); } - setTempValue(''); + setTempValue(""); setIsEditing(false); }; return (
- {label && } - {description &&

{description}

} + {label && ( + + )} + {description && ( +

+ {description} +

+ )} {isMasked && !isEditing ? ( // Masked value from backend: show display + Edit button - - + + { - if (e.key === 'Escape') handleCancel(); + if (e.key === "Escape") handleCancel(); }} /> ) : ( diff --git a/frontend/src/core/components/shared/EncryptedPdfUnlockModal.tsx b/frontend/src/core/components/shared/EncryptedPdfUnlockModal.tsx index 5ba2dbf053..f713d1e526 100644 --- a/frontend/src/core/components/shared/EncryptedPdfUnlockModal.tsx +++ b/frontend/src/core/components/shared/EncryptedPdfUnlockModal.tsx @@ -1,7 +1,14 @@ -import { Modal, Stack, Text, Button, PasswordInput, Group } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { type KeyboardEventHandler } from 'react'; -import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex'; +import { + Modal, + Stack, + Text, + Button, + PasswordInput, + Group, +} from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { type KeyboardEventHandler } from "react"; +import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from "@app/styles/zIndex"; interface EncryptedPdfUnlockModalProps { opened: boolean; @@ -9,8 +16,10 @@ interface EncryptedPdfUnlockModalProps { password: string; errorMessage?: string | null; isProcessing: boolean; + remainingCount: number; onPasswordChange: (value: string) => void; onUnlock: () => void; + onUnlockAll: () => void; onSkip: () => void; } @@ -20,14 +29,16 @@ const EncryptedPdfUnlockModal = ({ password, errorMessage, isProcessing, + remainingCount, onPasswordChange, onUnlock, + onUnlockAll, onSkip, }: EncryptedPdfUnlockModalProps) => { const { t } = useTranslation(); const handleKeyDown: KeyboardEventHandler = (event) => { - if (event.key === 'Enter' && !isProcessing && password.trim().length > 0) { + if (event.key === "Enter" && !isProcessing && password.trim().length > 0) { onUnlock(); } }; @@ -36,7 +47,7 @@ const EncryptedPdfUnlockModal = ({ - {fileName} + + {fileName} + {t( - 'encryptedPdfUnlock.description', - 'This PDF is password protected. Enter the password so you can continue working with it.' + "encryptedPdfUnlock.description", + "This PDF is password protected. Enter the password so you can continue working with it.", )} onPasswordChange(event.currentTarget.value)} onKeyDown={handleKeyDown} @@ -70,12 +86,35 @@ const EncryptedPdfUnlockModal = ({ - - + + {remainingCount > 0 && ( + + )} + + diff --git a/frontend/src/core/components/shared/ErrorBoundary.tsx b/frontend/src/core/components/shared/ErrorBoundary.tsx index 0bab94f0a2..aa2f126287 100644 --- a/frontend/src/core/components/shared/ErrorBoundary.tsx +++ b/frontend/src/core/components/shared/ErrorBoundary.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { Text, Button, Stack } from '@mantine/core'; +import React from "react"; +import { Text, Button, Stack } from "@mantine/core"; interface ErrorBoundaryState { hasError: boolean; @@ -8,10 +8,13 @@ interface ErrorBoundaryState { interface ErrorBoundaryProps { children: React.ReactNode; - fallback?: React.ComponentType<{error?: Error; retry: () => void}>; + fallback?: React.ComponentType<{ error?: Error; retry: () => void }>; } -export default class ErrorBoundary extends React.Component { +export default class ErrorBoundary extends React.Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false }; @@ -23,41 +26,43 @@ export default class ErrorBoundary extends React.Component { @@ -72,26 +77,51 @@ export default class ErrorBoundary extends React.Component - Something went wrong - {process.env.NODE_ENV === 'development' && this.state.error && ( + + + Something went wrong + + {process.env.NODE_ENV === "development" && this.state.error && ( <> - + {this.state.error.message} {this.state.error.stack && ( -
- - Show stack trace +
+ + + Show stack trace + -
+                  
                     {this.state.error.stack}
                   
diff --git a/frontend/src/core/components/shared/FileCard.tsx b/frontend/src/core/components/shared/FileCard.tsx index dda4791753..6bb1f8ee84 100644 --- a/frontend/src/core/components/shared/FileCard.tsx +++ b/frontend/src/core/components/shared/FileCard.tsx @@ -1,5 +1,18 @@ import { useState } from "react"; -import { Card, Stack, Text, Group, Badge, Button, Box, Image, ThemeIcon, ActionIcon, Tooltip, Loader } from "@mantine/core"; +import { + Card, + Stack, + Text, + Group, + Badge, + Button, + Box, + Image, + ThemeIcon, + ActionIcon, + Tooltip, + Loader, +} from "@mantine/core"; import { useTranslation } from "react-i18next"; import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf"; import StorageIcon from "@mui/icons-material/Storage"; @@ -22,15 +35,26 @@ interface FileCardProps { isSupported?: boolean; // Whether the file format is supported by the current tool } -const FileCard = ({ file, fileStub, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect, isSupported = true }: FileCardProps) => { +const FileCard = ({ + file, + fileStub, + onRemove, + onDoubleClick, + onView, + onEdit, + isSelected, + onSelect, + isSupported = true, +}: FileCardProps) => { const { t } = useTranslation(); // Use record thumbnail if available, otherwise fall back to IndexedDB lookup - const { thumbnail: indexedDBThumb, isGenerating } = useIndexedDBThumbnail(fileStub); + const { thumbnail: indexedDBThumb, isGenerating } = + useIndexedDBThumbnail(fileStub); const thumb = fileStub?.thumbnailUrl || indexedDBThumb; const [isHovered, setIsHovered] = useState(false); // Show loading state during hydration: PDF file without thumbnail yet - const isPdf = file.type === 'application/pdf'; + const isPdf = file.type === "application/pdf"; const isHydrating = isPdf && !thumb && !isGenerating; return ( @@ -44,11 +68,13 @@ const FileCard = ({ file, fileStub, onRemove, onDoubleClick, onView, onEdit, isS minWidth: 180, maxWidth: 260, cursor: onDoubleClick && isSupported ? "pointer" : undefined, - position: 'relative', - border: isSelected ? '2px solid var(--mantine-color-blue-6)' : undefined, - backgroundColor: isSelected ? 'var(--mantine-color-blue-0)' : undefined, + position: "relative", + border: isSelected + ? "2px solid var(--mantine-color-blue-6)" + : undefined, + backgroundColor: isSelected ? "var(--mantine-color-blue-0)" : undefined, opacity: isSupported ? 1 : 0.5, - filter: isSupported ? 'none' : 'grayscale(50%)' + filter: isSupported ? "none" : "grayscale(50%)", }} onDoubleClick={onDoubleClick} onMouseEnter={() => setIsHovered(true)} @@ -69,22 +95,22 @@ const FileCard = ({ file, fileStub, onRemove, onDoubleClick, onView, onEdit, isS justifyContent: "center", margin: "0 auto", background: "#fafbfc", - position: 'relative' + position: "relative", }} > {/* Hover action buttons */} {isHovered && (onView || onEdit) && (
e.stopPropagation()} > @@ -129,29 +155,39 @@ const FileCard = ({ file, fileStub, onRemove, onDoubleClick, onView, onEdit, isS fit="contain" radius="sm" /> - ) : (isGenerating || isHydrating) ? ( + ) : isGenerating || isHydrating ? ( - Loading... + + Loading... + ) : ( -
+
100 * 1024 * 1024 ? "orange" : "red"} size={60} radius="sm" - style={{ display: "flex", alignItems: "center", justifyContent: "center" }} + style={{ + display: "flex", + alignItems: "center", + justifyContent: "center", + }} > {file.size > 100 * 1024 * 1024 && ( - Large File + + Large File + )}
)} diff --git a/frontend/src/core/components/shared/FileDropdownMenu.tsx b/frontend/src/core/components/shared/FileDropdownMenu.tsx index 3d1f9427af..ce38049af0 100644 --- a/frontend/src/core/components/shared/FileDropdownMenu.tsx +++ b/frontend/src/core/components/shared/FileDropdownMenu.tsx @@ -1,21 +1,12 @@ -import React from 'react'; -import { Menu, Loader, Group, Text, ActionIcon, Tooltip } from '@mantine/core'; -import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import CloseIcon from '@mui/icons-material/Close'; -import FitText from '@app/components/shared/FitText'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; -import { FileId } from '@app/types/file'; - -// Truncate text from the center: "very-long-filename.pdf" -> "very-lo...ame.pdf" -function truncateCenter(text: string, maxLength: number = 25): string { - if (text.length <= maxLength) return text; - const ellipsis = '...'; - const charsToShow = maxLength - ellipsis.length; - const frontChars = Math.ceil(charsToShow / 2); - const backChars = Math.floor(charsToShow / 2); - return text.substring(0, frontChars) + ellipsis + text.substring(text.length - backChars); -} +import React from "react"; +import { Menu, Loader, Group, Text, ActionIcon, Tooltip } from "@mantine/core"; +import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import CloseIcon from "@mui/icons-material/Close"; +import FitText from "@app/components/shared/FitText"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; +import { FileId } from "@app/types/file"; +import { truncateCenter } from "@app/utils/textUtils"; interface FileDropdownMenuProps { displayName: string; @@ -40,7 +31,9 @@ export const FileDropdownMenu: React.FC = ({ return ( -
+
{switchingTo === "viewer" ? ( ) : ( @@ -50,22 +43,24 @@ export const FileDropdownMenu: React.FC = ({
- + {activeFiles.map((file, index) => { - const itemName = file?.name || 'Untitled'; + const itemName = file?.name || "Untitled"; const isActive = index === currentFileIndex; return ( = ({ onFileSelect?.(index); }} className="viewer-file-tab" - {...(isActive && { 'data-active': true })} + {...(isActive && { "data-active": true })} style={{ - justifyContent: 'flex-start', + justifyContent: "flex-start", }} > - -
+ +
diff --git a/frontend/src/core/components/shared/FileGrid.tsx b/frontend/src/core/components/shared/FileGrid.tsx index 5bd31008db..2ade54426f 100644 --- a/frontend/src/core/components/shared/FileGrid.tsx +++ b/frontend/src/core/components/shared/FileGrid.tsx @@ -1,5 +1,13 @@ import { useState } from "react"; -import { Box, Flex, Group, Text, Button, TextInput, Select } from "@mantine/core"; +import { + Box, + Flex, + Group, + Text, + Button, + TextInput, + Select, +} from "@mantine/core"; import { useTranslation } from "react-i18next"; import SearchIcon from "@mui/icons-material/Search"; import SortIcon from "@mui/icons-material/Sort"; @@ -24,7 +32,7 @@ interface FileGridProps { isFileSupported?: (fileName: string) => boolean; // Function to check if file is supported } -type SortOption = 'date' | 'name' | 'size'; +type SortOption = "date" | "name" | "size"; const FileGrid = ({ files, @@ -40,25 +48,25 @@ const FileGrid = ({ onShowAll, showingAll = false, onDeleteAll, - isFileSupported + isFileSupported, }: FileGridProps) => { const { t } = useTranslation(); const [searchTerm, setSearchTerm] = useState(""); - const [sortBy, setSortBy] = useState('date'); + const [sortBy, setSortBy] = useState("date"); // Filter files based on search term - const filteredFiles = files.filter(item => - item.file.name.toLowerCase().includes(searchTerm.toLowerCase()) + const filteredFiles = files.filter((item) => + item.file.name.toLowerCase().includes(searchTerm.toLowerCase()), ); // Sort files const sortedFiles = [...filteredFiles].sort((a, b) => { switch (sortBy) { - case 'date': + case "date": return (b.file.lastModified || 0) - (a.file.lastModified || 0); - case 'name': + case "name": return a.file.name.localeCompare(b.file.name); - case 'size': + case "size": return (b.file.size || 0) - (a.file.size || 0); default: return 0; @@ -66,14 +74,14 @@ const FileGrid = ({ }); // Apply max display limit if specified - const displayFiles = maxDisplay && !showingAll - ? sortedFiles.slice(0, maxDisplay) - : sortedFiles; + const displayFiles = + maxDisplay && !showingAll ? sortedFiles.slice(0, maxDisplay) : sortedFiles; - const hasMoreFiles = maxDisplay && !showingAll && sortedFiles.length > maxDisplay; + const hasMoreFiles = + maxDisplay && !showingAll && sortedFiles.length > maxDisplay; return ( - + {/* Search and Sort Controls */} {(showSearch || showSort || onDeleteAll) && ( @@ -91,9 +99,18 @@ const FileGrid = ({ {showSort && ( + + ); +} diff --git a/frontend/src/core/components/shared/LandingDocumentStack.tsx b/frontend/src/core/components/shared/LandingDocumentStack.tsx new file mode 100644 index 0000000000..32ec182df9 --- /dev/null +++ b/frontend/src/core/components/shared/LandingDocumentStack.tsx @@ -0,0 +1,65 @@ +/** Decorative stack only: window dots + grey bars — no text or i18n (avoids keys showing in the UI). */ +export function LandingDocumentStack() { + const bar = (widthPct: number, heightPx: number, marginBottom: number) => ({ + width: `${widthPct}%`, + height: heightPx, + marginBottom: marginBottom || undefined, + }); + + return ( +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ ); +} diff --git a/frontend/src/core/components/shared/LandingPage.css b/frontend/src/core/components/shared/LandingPage.css new file mode 100644 index 0000000000..e818217600 --- /dev/null +++ b/frontend/src/core/components/shared/LandingPage.css @@ -0,0 +1,152 @@ +/* ============================================================ + Landing Page styles. + All custom properties are defined in theme.css. + ============================================================ */ + +/* ── Hero text ───────────────────────────────────────────── */ +.landing-title { + margin: 0; + margin-top: 1.75rem; + margin-bottom: 0.5rem; + text-align: center; + font-size: 2.125rem; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--text-primary); +} + +.landing-subtitle { + margin: 0; + margin-bottom: 1.5rem; + text-align: center; + font-size: 0.9375rem; + line-height: 1.5; + max-width: 28rem; + color: var(--text-secondary); +} + +/* ── Document stack ──────────────────────────────────────── */ +.landing-stack { + position: relative; + z-index: 1; + width: var(--landing-stack-w); + min-width: var(--landing-stack-w); + height: var(--landing-stack-h); + min-height: var(--landing-stack-h); + margin-left: auto; + margin-right: auto; + flex-shrink: 0; + overflow: visible; +} + +/* Sheets — static white, never change with theme */ +.landing-sheet { + position: absolute; + border-radius: 12px; + background-color: #ffffff; + cursor: default; +} + +.landing-sheet--back { + width: 128px; + height: 160px; + transform-origin: bottom center; + border: 1px solid #e5e7eb; + box-shadow: var(--landing-doc-shadow-back-idle); +} + +.landing-sheet--left { + left: 8px; + top: 12px; + transform: rotate(-8deg); +} + +.landing-sheet--right { + right: 8px; + top: 12px; + transform: rotate(8deg); +} + +.landing-sheet--front { + left: 50%; + top: 0; + z-index: 10; + width: 144px; + height: 176px; + margin-left: -72px; + overflow: hidden; + box-shadow: var(--landing-doc-shadow-front-idle); +} + +.landing-sheet-header { + display: flex; + height: 40px; + align-items: center; + gap: 8px; + padding: 0 12px; + border-radius: 12px 12px 0 0; + background: var(--landing-hero-gradient); +} + +.landing-sheet-dot { + width: 10px; + height: 10px; + border-radius: 9999px; +} + +.landing-sheet-body { + padding: 10px 12px; +} + +.landing-sheet-side-body { + padding: 12px; +} + +/* Bars — static light colours, never change with theme */ +.landing-bar { + border-radius: 9999px; + background-color: #e5e7eb; +} +.landing-bar--strong { + background-color: #d1d5db; +} + +/* ── Action buttons ──────────────────────────────────────── */ +.landing-btn-primary { + background: var(--landing-hero-gradient) !important; + color: #ffffff !important; + border: none !important; + border-radius: 0.75rem !important; + font-weight: 600 !important; +} + +.landing-btn-secondary { + border-radius: 0.75rem !important; + font-weight: 600 !important; + border-color: var(--landing-button-border, var(--border-default)) !important; + background-color: var(--landing-button-bg, var(--bg-surface)) !important; + color: var(--landing-button-color, var(--text-primary)) !important; +} +.landing-btn-secondary:hover { + background-color: var( + --landing-button-hover-bg, + var(--landing-button-bg, var(--bg-surface)) + ) !important; +} + +/* Icon-only variant: accent colour instead of button text colour */ +.landing-btn-icon { + color: var(--accent-interactive) !important; +} + +/* Dropzone accept/reject outlines. Mantine 8 no longer supports nested + * `&[data-accept]` selectors inside the `styles` prop object, so these are + * plain CSS attribute selectors on a class applied to the Dropzone root. */ +.landing-dropzone[data-accept] { + outline: 2px dashed var(--accent-interactive); + outline-offset: 4px; +} +.landing-dropzone[data-reject] { + outline: 2px dashed var(--mantine-color-red-6); + outline-offset: 4px; +} diff --git a/frontend/src/core/components/shared/LandingPage.tsx b/frontend/src/core/components/shared/LandingPage.tsx index a2ad72e2a4..e52eb98b56 100644 --- a/frontend/src/core/components/shared/LandingPage.tsx +++ b/frontend/src/core/components/shared/LandingPage.tsx @@ -1,68 +1,44 @@ -import React, { useEffect } from 'react'; -import { Container, Button, Group, useMantineColorScheme, ActionIcon, Tooltip } from '@mantine/core'; -import { Dropzone } from '@mantine/dropzone'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { useTranslation } from 'react-i18next'; -import { useFileHandler } from '@app/hooks/useFileHandler'; -import { useFilesModalContext } from '@app/contexts/FilesModalContext'; -import { useLogoPath } from '@app/hooks/useLogoPath'; -import { useLogoAssets } from '@app/hooks/useLogoAssets'; -import { useLogoVariant } from '@app/hooks/useLogoVariant'; -import { useFileManager } from '@app/hooks/useFileManager'; -import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; -import { useFileActionIcons } from '@app/hooks/useFileActionIcons'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import { useIsMobile } from '@app/hooks/useIsMobile'; -import MobileUploadModal from '@app/components/shared/MobileUploadModal'; -import { openFilesFromDisk } from '@app/services/openFilesFromDisk'; +import React, { useState } from "react"; +import { Container } from "@mantine/core"; +import { Dropzone } from "@mantine/dropzone"; +import { useTranslation } from "react-i18next"; +import { useFileHandler } from "@app/hooks/useFileHandler"; +import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology"; +import MobileUploadModal from "@app/components/shared/MobileUploadModal"; +import { openFilesFromDisk } from "@app/services/openFilesFromDisk"; +import { LandingDocumentStack } from "@app/components/shared/LandingDocumentStack"; +import { LandingActions } from "@app/components/shared/LandingActions"; +import "@app/components/shared/LandingPage.css"; const LandingPage = () => { - const { addFiles } = useFileHandler(); - const fileInputRef = React.useRef(null); - const { colorScheme } = useMantineColorScheme(); const { t } = useTranslation(); - const { openFilesModal } = useFilesModalContext(); - const [isUploadHover, setIsUploadHover] = React.useState(false); - const logoPath = useLogoPath(); - const logoVariant = useLogoVariant(); - const { wordmark } = useLogoAssets(); - const { loadRecentFiles } = useFileManager(); - const [hasRecents, setHasRecents] = React.useState(false); - const [mobileUploadModalOpen, setMobileUploadModalOpen] = React.useState(false); + const { addFiles } = useFileHandler(); + const fileInputRef = React.useRef(null); const terminology = useFileActionTerminology(); - const icons = useFileActionIcons(); - const { config } = useAppConfig(); - const isMobile = useIsMobile(); + const [mobileUploadModalOpen, setMobileUploadModalOpen] = useState(false); const handleFileDrop = async (files: File[]) => { await addFiles(files); }; - const handleOpenFilesModal = () => { - openFilesModal(); - }; - const handleNativeUploadClick = async () => { const files = await openFilesFromDisk({ multiple: true, - onFallbackOpen: () => fileInputRef.current?.click() + onFallbackOpen: () => fileInputRef.current?.click(), }); if (files.length > 0) { await addFiles(files); } }; - const handleFileSelect = async (event: React.ChangeEvent) => { + const handleFileSelect = async ( + event: React.ChangeEvent, + ) => { const files = Array.from(event.target.files || []); if (files.length > 0) { await addFiles(files); } - // Reset the input so the same file can be selected again - event.target.value = ''; - }; - - const handleMobileUploadClick = () => { - setMobileUploadModalOpen(true); + event.target.value = ""; }; const handleFilesReceivedFromMobile = async (files: File[]) => { @@ -71,249 +47,56 @@ const LandingPage = () => { } }; - // Determine if the user has any recent files (same source as File Manager) - useEffect(() => { - let isMounted = true; - (async () => { - try { - const files = await loadRecentFiles(); - if (isMounted) { - setHasRecents((files?.length || 0) > 0); - } - } catch (_err) { - if (isMounted) setHasRecents(false); - } - })(); - return () => { isMounted = false; }; - }, [loadRecentFiles]); - return ( - - {/* White PDF Page Background */} + - {logoVariant === 'modern' && ( -
- Stirling PDF Logo -
- )} -
- {/* Logo positioned absolutely in top right corner */} + +

+ {t("landing.heroTitle", "Stirling PDF")} +

+

+ {t( + "landing.heroSubtitle", + "Drop in or add an existing PDF to get started.", + )} +

- {/* Centered content container */} -
- {/* Stirling PDF Branding */} - - Stirling PDF - - - {/* Add Files + Native Upload Buttons */} -
setIsUploadHover(false)} - > - {/* Show both buttons only when recents exist; otherwise show a single Upload button */} - {hasRecents && ( - <> - - - {config?.enableMobileScanner && !isMobile && ( - - - - - - )} - - )} - {!hasRecents && ( - <> - - {config?.enableMobileScanner && !isMobile && ( - - - - - - )} - - )} -
- - {/* Hidden file input for native file picker */} - - -
- - {/* Instruction Text */} - - {terminology.dropFilesHere} - -
+ void handleNativeUploadClick()} + onMobileUploadClick={() => setMobileUploadModalOpen(true)} + onFileSelect={handleFileSelect} + />
+ setMobileUploadModalOpen(false)} diff --git a/frontend/src/core/components/shared/LanguageSelector.module.css b/frontend/src/core/components/shared/LanguageSelector.module.css index 431f438069..8f2248687b 100644 --- a/frontend/src/core/components/shared/LanguageSelector.module.css +++ b/frontend/src/core/components/shared/LanguageSelector.module.css @@ -18,11 +18,11 @@ .languageGrid { grid-template-columns: repeat(2, 1fr); } - + .languageItem:nth-child(4n) { border-right: 2px solid var(--mantine-color-gray-3); } - + .languageItem:nth-child(2n) { border-right: none; } @@ -32,11 +32,11 @@ .languageGrid { grid-template-columns: repeat(3, 1fr); } - + .languageItem:nth-child(4n) { border-right: 2px solid var(--mantine-color-gray-3); } - + .languageItem:nth-child(3n) { border-right: none; } @@ -85,4 +85,4 @@ height: 100px; opacity: 0; } -} \ No newline at end of file +} diff --git a/frontend/src/core/components/shared/LanguageSelector.tsx b/frontend/src/core/components/shared/LanguageSelector.tsx index 66aa17c8a8..e0896a21eb 100644 --- a/frontend/src/core/components/shared/LanguageSelector.tsx +++ b/frontend/src/core/components/shared/LanguageSelector.tsx @@ -1,15 +1,15 @@ -import React, { useState, useEffect } from 'react'; -import { Menu, Button, ActionIcon } from '@mantine/core'; -import { Tooltip } from '@app/components/shared/Tooltip'; -import { useTranslation } from 'react-i18next'; -import { supportedLanguages, setUserLanguage } from '@app/i18n'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import styles from '@app/components/shared/LanguageSelector.module.css'; -import { Z_INDEX_CONFIG_MODAL } from '@app/styles/zIndex'; +import React, { useState, useEffect } from "react"; +import { Menu, Button, ActionIcon } from "@mantine/core"; +import { Tooltip } from "@app/components/shared/Tooltip"; +import { useTranslation } from "react-i18next"; +import { supportedLanguages, setUserLanguage } from "@app/i18n"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import styles from "@app/components/shared/LanguageSelector.module.css"; +import { Z_INDEX_CONFIG_MODAL } from "@app/styles/zIndex"; // Types interface LanguageSelectorProps { - position?: React.ComponentProps['position']; + position?: React.ComponentProps["position"]; offset?: number; compact?: boolean; // icon-only trigger tooltip?: string; // tooltip text for compact mode @@ -48,12 +48,12 @@ const LanguageItem: React.FC = ({ rippleEffect, pendingLanguage, compact, - disabled = false + disabled = false, }) => { const { t } = useTranslation(); const labelText = option.label; - const comingSoonText = t('comingSoon', 'Coming soon'); + const comingSoonText = t("comingSoon", "Coming soon"); const label = disabled ? ( @@ -68,7 +68,7 @@ const LanguageItem: React.FC = ({ className={styles.languageItem} style={{ opacity: animationTriggered ? 1 : 0, - transform: animationTriggered ? 'translateY(0px)' : 'translateY(8px)', + transform: animationTriggered ? "translateY(0px)" : "translateY(8px)", transition: `opacity 0.15s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.01}s, transform 0.15s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${index * 0.01}s`, }} > @@ -81,40 +81,42 @@ const LanguageItem: React.FC = ({ disabled={disabled} styles={{ root: { - borderRadius: '4px', - minHeight: '32px', - padding: '4px 8px', - justifyContent: 'flex-start', - position: 'relative', - overflow: 'hidden', + borderRadius: "4px", + minHeight: "32px", + padding: "4px 8px", + justifyContent: "flex-start", + position: "relative", + overflow: "hidden", backgroundColor: isSelected - ? 'light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))' - : 'transparent', + ? "light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))" + : "transparent", color: disabled - ? 'light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3))' + ? "light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3))" : isSelected - ? 'light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))' - : 'light-dark(var(--mantine-color-gray-7), var(--mantine-color-white))', - transition: 'all 0.12s cubic-bezier(0.25, 0.46, 0.45, 0.94)', - cursor: disabled ? 'not-allowed' : 'pointer', - '&:hover': !disabled ? { - backgroundColor: isSelected - ? 'light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-7))' - : 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))', - transform: 'translateY(-1px)', - boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', - } : {} + ? "light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))" + : "light-dark(var(--mantine-color-gray-7), var(--mantine-color-white))", + transition: "all 0.12s cubic-bezier(0.25, 0.46, 0.45, 0.94)", + cursor: disabled ? "not-allowed" : "pointer", + "&:hover": !disabled + ? { + backgroundColor: isSelected + ? "light-dark(var(--mantine-color-blue-2), var(--mantine-color-blue-7))" + : "light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))", + transform: "translateY(-1px)", + boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)", + } + : {}, }, label: { - fontSize: '13px', + fontSize: "13px", fontWeight: isSelected ? 600 : 400, - textAlign: 'left', - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - position: 'relative', + textAlign: "left", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + position: "relative", zIndex: 2, - } + }, }} > {label} @@ -122,16 +124,17 @@ const LanguageItem: React.FC = ({
@@ -155,10 +158,10 @@ const RippleStyles: React.FC = () => ( // Main component const LanguageSelector: React.FC = ({ - position = 'bottom-start', + position = "bottom-start", offset = 8, compact = false, - tooltip + tooltip, }) => { const { i18n, ready } = useTranslation(); const [opened, setOpened] = useState(false); @@ -183,11 +186,15 @@ const LanguageSelector: React.FC = ({ // Get the filtered list of supported languages from i18n // This respects server config (ui.languages) applied by AppConfigLoader - const allowedLanguages = (i18n.options.supportedLngs as string[] || []) - .filter(lang => lang !== 'cimode'); // Exclude i18next debug language + const allowedLanguages = ( + (i18n.options.supportedLngs as string[]) || [] + ).filter((lang) => lang !== "cimode"); // Exclude i18next debug language const languageOptions: LanguageOption[] = Object.entries(supportedLanguages) - .filter(([code]) => allowedLanguages.length === 0 || allowedLanguages.includes(code)) + .filter( + ([code]) => + allowedLanguages.length === 0 || allowedLanguages.includes(code), + ) .sort(([, nameA], [, nameB]) => nameA.localeCompare(nameB)) .map(([code, name]) => ({ value: code, @@ -196,13 +203,11 @@ const LanguageSelector: React.FC = ({ // Calculate dropdown width and grid columns based on number of languages // 2-4: 300px/2 cols, 5-9: 400px/3 cols, 10+: 600px/4 cols - const dropdownWidth = languageOptions.length <= 4 ? 300 - : languageOptions.length <= 9 ? 400 - : 600; + const dropdownWidth = + languageOptions.length <= 4 ? 300 : languageOptions.length <= 9 ? 400 : 600; - const gridColumns = languageOptions.length <= 4 ? 2 - : languageOptions.length <= 9 ? 3 - : 4; + const gridColumns = + languageOptions.length <= 4 ? 2 : languageOptions.length <= 9 ? 3 : 4; const handleLanguageChange = (value: string, event: React.MouseEvent) => { // Create ripple effect at click position (only for button mode) @@ -229,16 +234,17 @@ const LanguageSelector: React.FC = ({ setTimeout(() => setRippleEffect(null), 50); // Force a full reload so RTL/LTR layout and tooltips re-evaluate correctly - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { window.location.reload(); } }, 150); }, 100); }; - const currentLanguage = supportedLanguages[i18n.language as keyof typeof supportedLanguages] || - supportedLanguages['en-GB'] || - 'English'; // Fallback if supportedLanguages lookup fails + const currentLanguage = + supportedLanguages[i18n.language as keyof typeof supportedLanguages] || + supportedLanguages["en-GB"] || + "English"; // Fallback if supportedLanguages lookup fails // Hide the language selector if there's only one language option // (no point showing a selector when there's nothing to select) @@ -258,9 +264,9 @@ const LanguageSelector: React.FC = ({ zIndex={Z_INDEX_CONFIG_MODAL} withinPortal transitionProps={{ - transition: 'scale-y', + transition: "scale-y", duration: 120, - timingFunction: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)' + timingFunction: "cubic-bezier(0.25, 0.46, 0.45, 0.94)", }} > @@ -272,11 +278,12 @@ const LanguageSelector: React.FC = ({ title={!opened && tooltip ? tooltip : undefined} styles={{ root: { - color: 'var(--right-rail-icon)', - '&:hover': { - backgroundColor: 'light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))', - } - } + color: "var(--right-rail-icon)", + "&:hover": { + backgroundColor: + "light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5))", + }, + }, }} > @@ -285,53 +292,58 @@ const LanguageSelector: React.FC = ({ )}
- {languageOptions.map((option, index) => ( - handleLanguageChange(option.value, event)} - rippleEffect={rippleEffect} - pendingLanguage={pendingLanguage} - compact={compact} - disabled={false} - /> - ))} + {languageOptions.map((option, index) => ( + handleLanguageChange(option.value, event)} + rippleEffect={rippleEffect} + pendingLanguage={pendingLanguage} + compact={compact} + disabled={false} + /> + ))}
diff --git a/frontend/src/core/components/shared/LocalIcon.tsx b/frontend/src/core/components/shared/LocalIcon.tsx index ff7ca493af..fc51cb419b 100644 --- a/frontend/src/core/components/shared/LocalIcon.tsx +++ b/frontend/src/core/components/shared/LocalIcon.tsx @@ -1,6 +1,6 @@ -import React from 'react'; -import { addCollection, Icon } from '@iconify/react'; -import iconSet from '../../../assets/material-symbols-icons.json'; // eslint-disable-line no-restricted-imports -- Outside app paths +import React from "react"; +import { addCollection, Icon } from "@iconify/react"; +import iconSet from "../../../assets/material-symbols-icons.json"; // eslint-disable-line no-restricted-imports -- Outside app paths // Load icons synchronously at import time - guaranteed to be ready on first render let iconsLoaded = false; @@ -10,10 +10,12 @@ try { addCollection(iconSet); iconsLoaded = true; const localIconCount = Object.keys(iconSet.icons || {}).length; - console.info(`✅ Local icons loaded: ${localIconCount} icons (${Math.round(JSON.stringify(iconSet).length / 1024)}KB)`); + console.info( + `✅ Local icons loaded: ${localIconCount} icons (${Math.round(JSON.stringify(iconSet).length / 1024)}KB)`, + ); } } catch { - console.info('â„¹ï¸ Local icons not available - using CDN fallback'); + console.info("â„¹ï¸ Local icons not available - using CDN fallback"); } interface LocalIconProps { @@ -28,19 +30,25 @@ interface LocalIconProps { * LocalIcon component that uses our locally bundled Material Symbols icons * instead of loading from CDN */ -export const LocalIcon: React.FC = ({ icon, width, height, style, ...props }) => { +export const LocalIcon: React.FC = ({ + icon, + width, + height, + style, + ...props +}) => { // Convert our icon naming convention to the local collection format - const iconName = icon.startsWith('material-symbols:') + const iconName = icon.startsWith("material-symbols:") ? icon : `material-symbols:${icon}`; // Development logging (only in dev mode) - if (process.env.NODE_ENV === 'development') { + if (process.env.NODE_ENV === "development") { const logKey = `icon-${iconName}`; if (!sessionStorage.getItem(logKey)) { - const source = iconsLoaded ? 'local' : 'CDN'; + const source = iconsLoaded ? "local" : "CDN"; console.debug(`🎯 Icon: ${iconName} (${source})`); - sessionStorage.setItem(logKey, 'logged'); + sessionStorage.setItem(logKey, "logged"); } } @@ -48,10 +56,10 @@ export const LocalIcon: React.FC = ({ icon, width, height, style // Use width if provided, otherwise fall back to height const size = width || height; - if (size && typeof size === 'string') { + if (size && typeof size === "string") { // If it's a CSS unit string (like '1.5rem'), use it as fontSize iconStyle.fontSize = size; - } else if (typeof size === 'number') { + } else if (typeof size === "number") { // If it's a number, treat it as pixels iconStyle.fontSize = `${size}px`; } diff --git a/frontend/src/core/components/shared/LogoIcon.tsx b/frontend/src/core/components/shared/LogoIcon.tsx new file mode 100644 index 0000000000..6723fc3c32 --- /dev/null +++ b/frontend/src/core/components/shared/LogoIcon.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import { useMantineColorScheme } from "@mantine/core"; +import { useLogoPath } from "@app/hooks/useLogoPath"; + +interface LogoIconProps extends React.ImgHTMLAttributes { + alt?: string; +} + +export function LogoIcon({ alt = "", ...props }: LogoIconProps) { + const { colorScheme } = useMantineColorScheme(); + const logoPaths = useLogoPath(); + const src = colorScheme === "dark" ? logoPaths.dark : logoPaths.light; + return {alt}; +} diff --git a/frontend/src/core/components/shared/MobileUploadModal.tsx b/frontend/src/core/components/shared/MobileUploadModal.tsx index 275b2cb0d9..0232ef93f4 100644 --- a/frontend/src/core/components/shared/MobileUploadModal.tsx +++ b/frontend/src/core/components/shared/MobileUploadModal.tsx @@ -1,16 +1,16 @@ -import { useEffect, useCallback, useState, useRef } from 'react'; -import { Modal, Stack, Text, Badge, Box, Alert } from '@mantine/core'; -import { QRCodeSVG } from 'qrcode.react'; -import { useTranslation } from 'react-i18next'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import InfoRoundedIcon from '@mui/icons-material/InfoRounded'; -import ErrorRoundedIcon from '@mui/icons-material/ErrorRounded'; -import CheckRoundedIcon from '@mui/icons-material/CheckRounded'; -import WarningRoundedIcon from '@mui/icons-material/WarningRounded'; -import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; -import { withBasePath } from '@app/constants/app'; -import { convertImageToPdf, isImageFile } from '@app/utils/imageToPdfUtils'; -import apiClient from '@app/services/apiClient'; +import { useEffect, useCallback, useState, useRef } from "react"; +import { Modal, Stack, Text, Badge, Box, Alert } from "@mantine/core"; +import { QRCodeSVG } from "qrcode.react"; +import { useTranslation } from "react-i18next"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import InfoRoundedIcon from "@mui/icons-material/InfoRounded"; +import ErrorRoundedIcon from "@mui/icons-material/ErrorRounded"; +import CheckRoundedIcon from "@mui/icons-material/CheckRounded"; +import WarningRoundedIcon from "@mui/icons-material/WarningRounded"; +import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; +import { withBasePath } from "@app/constants/app"; +import { convertImageToPdf, isImageFile } from "@app/utils/imageToPdfUtils"; +import apiClient from "@app/services/apiClient"; interface MobileUploadModalProps { opened: boolean; @@ -21,9 +21,10 @@ interface MobileUploadModalProps { // Generate a cryptographically secure UUID v4-like session ID function generateSessionId(): string { // Use Web Crypto API for cryptographically secure random values - const cryptoObj = typeof crypto !== 'undefined' ? crypto : (window as any).crypto; + const cryptoObj = + typeof crypto !== "undefined" ? crypto : (window as any).crypto; - if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') { + if (cryptoObj && typeof cryptoObj.getRandomValues === "function") { const bytes = new Uint8Array(16); cryptoObj.getRandomValues(bytes); @@ -32,19 +33,23 @@ function generateSessionId(): string { bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10 // Convert bytes to hex string in UUID format - const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')); + const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")); return [ - hex.slice(0, 4).join(''), - hex.slice(4, 6).join(''), - hex.slice(6, 8).join(''), - hex.slice(8, 10).join(''), - hex.slice(10, 16).join(''), - ].join('-'); + hex.slice(0, 4).join(""), + hex.slice(4, 6).join(""), + hex.slice(6, 8).join(""), + hex.slice(8, 10).join(""), + hex.slice(10, 16).join(""), + ].join("-"); } // If Web Crypto is not available, fail fast rather than using insecure randomness - console.error('Web Crypto API not available. Cannot generate secure session ID.'); - throw new Error('Web Crypto API not available. Cannot generate secure session ID.'); + console.error( + "Web Crypto API not available. Cannot generate secure session ID.", + ); + throw new Error( + "Web Crypto API not available. Cannot generate secure session ID.", + ); } interface SessionInfo { @@ -60,7 +65,11 @@ interface SessionInfo { * Displays a QR code that mobile devices can scan to upload files via backend server. * Files are temporarily stored on server and retrieved by desktop. */ -export default function MobileUploadModal({ opened, onClose, onFilesReceived }: MobileUploadModalProps) { +export default function MobileUploadModal({ + opened, + onClose, + onFilesReceived, +}: MobileUploadModalProps) { const { t } = useTranslation(); const { config } = useAppConfig(); @@ -76,30 +85,39 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: // Use configured frontendUrl if set, otherwise use current origin // Combine with base path and mobile-scanner route - const baseUrl = localStorage.getItem('server_url') || ''; + const baseUrl = localStorage.getItem("server_url") || ""; const frontendUrl = baseUrl || config?.frontendUrl || window.location.origin; - const mobileUrl = `${frontendUrl}${withBasePath('/mobile-scanner')}?session=${sessionId}`; + const mobileUrl = `${frontendUrl}${withBasePath("/mobile-scanner")}?session=${sessionId}`; // Create session on backend - const createSession = useCallback(async (newSessionId: string) => { - try { - const response = await apiClient.post(`/api/v1/mobile-scanner/create-session/${newSessionId}`, undefined, { - responseType: 'json', - }); + const createSession = useCallback( + async (newSessionId: string) => { + try { + const response = await apiClient.post( + `/api/v1/mobile-scanner/create-session/${newSessionId}`, + undefined, + { + responseType: "json", + }, + ); - if (!response.status || response.status !== 200) { - throw new Error('Failed to create session'); + if (!response.status || response.status !== 200) { + throw new Error("Failed to create session"); + } + + const data = response.data; + setSessionInfo(data); + setError(null); + console.log("[MobileUploadModal] Session created:", data); + } catch (err) { + console.error("[MobileUploadModal] Failed to create session:", err); + setError( + t("mobileUpload.sessionCreateError", "Failed to create session"), + ); } - - const data = response.data; - setSessionInfo(data); - setError(null); - console.log('[MobileUploadModal] Session created:', data); - } catch (err) { - console.error('[MobileUploadModal] Failed to create session:', err); - setError(t('mobileUpload.sessionCreateError', 'Failed to create session')); - } - }, [t]); + }, + [t], + ); // Regenerate session (when expired or warned) const regenerateSession = useCallback(() => { @@ -115,43 +133,64 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: if (!opened) return; try { - const response = await apiClient.get(`/api/v1/mobile-scanner/files/${sessionId}`); + const response = await apiClient.get( + `/api/v1/mobile-scanner/files/${sessionId}`, + ); if (!response.status || response.status !== 200) { - throw new Error('Failed to check for files'); + throw new Error("Failed to check for files"); } const data = response.data; const files = data.files || []; // Download only files we haven't processed yet - const newFiles = files.filter((f: any) => !processedFiles.current.has(f.filename)); + const newFiles = files.filter( + (f: any) => !processedFiles.current.has(f.filename), + ); if (newFiles.length > 0) { for (const fileMetadata of newFiles) { try { const downloadResponse = await apiClient.get( - `/api/v1/mobile-scanner/download/${sessionId}/${fileMetadata.filename}`, { - responseType: 'blob', - } + `/api/v1/mobile-scanner/download/${sessionId}/${fileMetadata.filename}`, + { + responseType: "blob", + }, ); if (downloadResponse.status === 200) { const blob = downloadResponse.data; let file = new File([blob], fileMetadata.filename, { - type: fileMetadata.contentType || 'image/jpeg' + type: fileMetadata.contentType || "image/jpeg", }); // Convert images to PDF if enabled - if (isImageFile(file) && config?.mobileScannerConvertToPdf !== false) { + if ( + isImageFile(file) && + config?.mobileScannerConvertToPdf !== false + ) { try { file = await convertImageToPdf(file, { - imageResolution: config?.mobileScannerImageResolution as 'full' | 'reduced' | undefined, - pageFormat: config?.mobileScannerPageFormat as 'keep' | 'A4' | 'letter' | undefined, + imageResolution: config?.mobileScannerImageResolution as + | "full" + | "reduced" + | undefined, + pageFormat: config?.mobileScannerPageFormat as + | "keep" + | "A4" + | "letter" + | undefined, stretchToFit: config?.mobileScannerStretchToFit, }); - console.log('[MobileUploadModal] Converted image to PDF:', file.name); + console.log( + "[MobileUploadModal] Converted image to PDF:", + file.name, + ); } catch (convertError) { - console.warn('[MobileUploadModal] Failed to convert image to PDF, using original file:', convertError); + console.warn( + "[MobileUploadModal] Failed to convert image to PDF, using original file:", + convertError, + ); // Continue with original image file if conversion fails } } @@ -161,7 +200,11 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: onFilesReceived([file]); } } catch (err) { - console.error('[MobileUploadModal] Failed to download file:', fileMetadata.filename, err); + console.error( + "[MobileUploadModal] Failed to download file:", + fileMetadata.filename, + err, + ); } } @@ -169,14 +212,19 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: // This ensures files are only on server for ~1 second try { await apiClient.delete(`/api/v1/mobile-scanner/session/${sessionId}`); - console.log('[MobileUploadModal] Session cleaned up after file download'); + console.log( + "[MobileUploadModal] Session cleaned up after file download", + ); } catch (cleanupErr) { - console.warn('[MobileUploadModal] Failed to cleanup session after download:', cleanupErr); + console.warn( + "[MobileUploadModal] Failed to cleanup session after download:", + cleanupErr, + ); } } } catch (err) { - console.error('[MobileUploadModal] Error polling for files:', err); - setError(t('mobileUpload.pollingError', 'Error checking for files')); + console.error("[MobileUploadModal] Error polling for files:", err); + setError(t("mobileUpload.pollingError", "Error checking for files")); } }, [opened, sessionId, onFilesReceived, t]); @@ -201,9 +249,12 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: processedFiles.current.clear(); return () => { - console.log('Cleaning up session on unmount/close:', sessionId); - apiClient.delete(`/api/v1/mobile-scanner/session/${sessionId}`) - .catch(err => console.warn('[MobileUploadModal] Cleanup failed:', err)); + console.log("Cleaning up session on unmount/close:", sessionId); + apiClient + .delete(`/api/v1/mobile-scanner/session/${sessionId}`) + .catch((err) => + console.warn("[MobileUploadModal] Cleanup failed:", err), + ); }; }, [opened, sessionId, createSession]); @@ -267,7 +318,7 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: } + icon={} color="blue" variant="light" > {config?.mobileScannerConvertToPdf !== false ? t( - 'mobileUpload.description', - 'Scan this QR code with your mobile device to upload photos. Images will be automatically converted to PDF.' + "mobileUpload.description", + "Scan this QR code with your mobile device to upload photos. Images will be automatically converted to PDF.", ) : t( - 'mobileUpload.descriptionNoConvert', - 'Scan this QR code with your mobile device to upload photos.' + "mobileUpload.descriptionNoConvert", + "Scan this QR code with your mobile device to upload photos.", )} {showExpiryWarning && timeRemaining !== null && ( } - title={t('mobileUpload.expiryWarning', 'Session Expiring Soon')} + icon={} + title={t("mobileUpload.expiryWarning", "Session Expiring Soon")} color="orange" > {t( - 'mobileUpload.expiryWarningMessage', - 'This QR code will expire in {{seconds}} seconds. A new code will be generated automatically.', - { seconds: Math.ceil(timeRemaining / 1000) } + "mobileUpload.expiryWarningMessage", + "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically.", + { seconds: Math.ceil(timeRemaining / 1000) }, )} @@ -316,41 +367,55 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: {error && ( } - title={t('mobileUpload.error', 'Connection Error')} + icon={} + title={t("mobileUpload.error", "Connection Error")} color="red" > {error} )} - + {filesReceived > 0 && ( - }> - {t('mobileUpload.filesReceived', '{{count}} file(s) received', { count: filesReceived })} + } + > + {t("mobileUpload.filesReceived", "{{count}} file(s) received", { + count: filesReceived, + })} )} - + {config?.mobileScannerConvertToPdf !== false ? t( - 'mobileUpload.instructions', - 'Open the camera app on your phone and scan this code. Images will be automatically converted to PDF.' + "mobileUpload.instructions", + "Open the camera app on your phone and scan this code. Images will be automatically converted to PDF.", ) : t( - 'mobileUpload.instructionsNoConvert', - 'Open the camera app on your phone and scan this code. Files will be uploaded through the server.' + "mobileUpload.instructionsNoConvert", + "Open the camera app on your phone and scan this code. Files will be uploaded through the server.", )} @@ -358,9 +423,9 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }: size="xs" c="dimmed" style={{ - wordBreak: 'break-all', - textAlign: 'center', - fontFamily: 'monospace', + wordBreak: "break-all", + textAlign: "center", + fontFamily: "monospace", }} > {mobileUrl} diff --git a/frontend/src/core/components/shared/MultiSelectControls.tsx b/frontend/src/core/components/shared/MultiSelectControls.tsx index b6e0b24b9f..7a1cb80d10 100644 --- a/frontend/src/core/components/shared/MultiSelectControls.tsx +++ b/frontend/src/core/components/shared/MultiSelectControls.tsx @@ -16,33 +16,32 @@ const MultiSelectControls = ({ onOpenInFileEditor, onOpenInPageEditor, onAddToUpload, - onDeleteAll + onDeleteAll, }: MultiSelectControlsProps) => { const { t } = useTranslation(); if (selectedCount === 0) return null; return ( - + {selectedCount} {t("fileManager.filesSelected", "files selected")} - {onAddToUpload && ( - )} @@ -70,11 +69,7 @@ const MultiSelectControls = ({ )} {onDeleteAll && ( - )} diff --git a/frontend/src/core/components/shared/NavigationWarningModal.tsx b/frontend/src/core/components/shared/NavigationWarningModal.tsx index 6e143ccd18..d35f2a470c 100644 --- a/frontend/src/core/components/shared/NavigationWarningModal.tsx +++ b/frontend/src/core/components/shared/NavigationWarningModal.tsx @@ -1,3 +1,4 @@ +import { useRef, useEffect } from "react"; import { Modal, Text, Button, Group, Stack } from "@mantine/core"; import { useNavigationGuard } from "@app/contexts/NavigationContext"; import { useTranslation } from "react-i18next"; @@ -6,51 +7,69 @@ import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; import { Z_INDEX_TOAST } from "@app/styles/zIndex"; -interface NavigationWarningModalProps { - onApplyAndContinue?: () => Promise; - onExportAndContinue?: () => Promise; - /** Called when discarding - allows saving applied changes while discarding pending ones */ - onDiscardAndContinue?: () => Promise; -} - -const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue, onDiscardAndContinue }: NavigationWarningModalProps) => { +const NavigationWarningModal = () => { const { t } = useTranslation(); - const { showNavigationWarning, hasUnsavedChanges, pendingNavigation, cancelNavigation, confirmNavigation, setHasUnsavedChanges } = - useNavigationGuard(); + const { + showNavigationWarning, + hasUnsavedChanges, + pendingNavigation, + cancelNavigation, + setHasUnsavedChanges, + navigationWarningHandlersRef, + } = useNavigationGuard(); + + // Store pendingNavigation in a ref so async handlers always have the latest, + // not a stale closure captured before an await. + const pendingNavigationRef = useRef(pendingNavigation); + useEffect(() => { + pendingNavigationRef.current = pendingNavigation; + }, [pendingNavigation]); const handleKeepWorking = () => { cancelNavigation(); }; - const handleDiscardChanges = async () => { - // If a discard handler is provided, call it to save any already-applied changes, then discard the unsaved changes - if (onDiscardAndContinue) { - await onDiscardAndContinue(); - } + const finishAndNavigate = () => { + const nav = pendingNavigationRef.current; setHasUnsavedChanges(false); - confirmNavigation(); + cancelNavigation(); + if (nav) { + nav(); + } + }; + + const handleDiscardChanges = async () => { + const handlers = navigationWarningHandlersRef.current; + if (handlers?.onDiscardAndContinue) { + await handlers.onDiscardAndContinue(); + } + finishAndNavigate(); }; const handleApplyAndContinue = async () => { - if (onApplyAndContinue) { - await onApplyAndContinue(); + const handlers = navigationWarningHandlersRef.current; + if (handlers?.onApplyAndContinue) { + await handlers.onApplyAndContinue(); } - setHasUnsavedChanges(false); - confirmNavigation(); + finishAndNavigate(); }; const handleExportAndContinue = async () => { - if (onExportAndContinue) { - await onExportAndContinue(); + const handlers = navigationWarningHandlersRef.current; + if (handlers?.onExportAndContinue) { + await handlers.onExportAndContinue(); } - setHasUnsavedChanges(false); - confirmNavigation(); + finishAndNavigate(); }; + // Read handler availability at render time for button visibility + const handlers = navigationWarningHandlersRef.current; + const hasApply = !!handlers?.onApplyAndContinue; + const hasExport = !!handlers?.onExportAndContinue; + const BUTTON_WIDTH = "12rem"; // Only show modal if there are unsaved changes AND there's an actual pending navigation - // This prevents the modal from showing due to spurious state updates if (!hasUnsavedChanges || !pendingNavigation) { return null; } @@ -67,33 +86,55 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue, onDis zIndex={Z_INDEX_TOAST} > - - - {t("unsavedChanges", "You have unsaved changes to your PDF.")} - - - {t("areYouSure", "Are you sure you want to leave?")} - + + + {t("unsavedChanges", "You have unsaved changes to your PDF.")} + + + {t("areYouSure", "Are you sure you want to leave?")} + {/* Desktop layout: 2 groups side by side */} - - - {onApplyAndContinue && ( - )} - {onExportAndContinue && ( - )} @@ -102,19 +143,41 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue, onDis {/* Mobile layout: centered stack of 4 buttons */} - - - {onApplyAndContinue && ( - )} - {onExportAndContinue && ( - )} diff --git a/frontend/src/core/components/shared/ObscuredOverlay.tsx b/frontend/src/core/components/shared/ObscuredOverlay.tsx index 2329d624dc..ba8140d303 100644 --- a/frontend/src/core/components/shared/ObscuredOverlay.tsx +++ b/frontend/src/core/components/shared/ObscuredOverlay.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import styles from '@app/components/shared/ObscuredOverlay/ObscuredOverlay.module.css'; +import React from "react"; +import styles from "@app/components/shared/ObscuredOverlay/ObscuredOverlay.module.css"; type ObscuredOverlayProps = { obscured: boolean; @@ -31,12 +31,14 @@ export default function ObscuredOverlay({ >
{overlayMessage && ( -
- {overlayMessage} -
+
{overlayMessage}
)} {buttonText && onButtonClick && ( - )} @@ -46,5 +48,3 @@ export default function ObscuredOverlay({
); } - - diff --git a/frontend/src/core/components/shared/PageEditorFileDropdown.tsx b/frontend/src/core/components/shared/PageEditorFileDropdown.tsx index 11f9742c2d..4cbd84b157 100644 --- a/frontend/src/core/components/shared/PageEditorFileDropdown.tsx +++ b/frontend/src/core/components/shared/PageEditorFileDropdown.tsx @@ -1,16 +1,16 @@ -import React from 'react'; -import { Menu, Loader, Group, Text, Checkbox } from '@mantine/core'; -import { LocalIcon } from '@app/components/shared/LocalIcon'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; -import AddIcon from '@mui/icons-material/Add'; -import FitText from '@app/components/shared/FitText'; -import { getFileColorWithOpacity } from '@app/components/pageEditor/fileColors'; -import { useFilesModalContext } from '@app/contexts/FilesModalContext'; -import { PrivateContent } from '@app/components/shared/PrivateContent'; -import { useFileItemDragDrop } from '@app/components/shared/pageEditor/useFileItemDragDrop'; +import React from "react"; +import { Menu, Loader, Group, Text, Checkbox } from "@mantine/core"; +import { LocalIcon } from "@app/components/shared/LocalIcon"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import AddIcon from "@mui/icons-material/Add"; +import FitText from "@app/components/shared/FitText"; +import { getFileColorWithOpacity } from "@app/components/pageEditor/fileColors"; +import { useFilesModalContext } from "@app/contexts/FilesModalContext"; +import { PrivateContent } from "@app/components/shared/PrivateContent"; +import { useFileItemDragDrop } from "@app/components/shared/pageEditor/useFileItemDragDrop"; -import { FileId } from '@app/types/file'; +import { FileId } from "@app/types/file"; // Local interface for PageEditor file display interface PageEditorFile { @@ -50,28 +50,30 @@ const FileMenuItem: React.FC = ({ onReorder, }); - const itemName = file?.name || 'Untitled'; + const itemName = file?.name || "Untitled"; const fileColorBorder = getFileColorWithOpacity(colorIndex, 1); const fileColorBorderHover = getFileColorWithOpacity(colorIndex, 1.0); return (
{/* Drop indicator line */} {isDragOver && (
@@ -87,34 +89,40 @@ const FileMenuItem: React.FC = ({ onToggleSelection(file.fileId); }} style={{ - padding: '0.75rem 0.75rem', - cursor: isDragging ? 'grabbing' : 'grab', - backgroundColor: file.isSelected ? 'rgba(0, 0, 0, 0.05)' : 'transparent', + padding: "0.75rem 0.75rem", + cursor: isDragging ? "grabbing" : "grab", + backgroundColor: file.isSelected + ? "rgba(0, 0, 0, 0.05)" + : "transparent", borderLeft: `6px solid ${fileColorBorder}`, opacity: isDragging ? 0.5 : 1, - transition: 'opacity 0.2s ease-in-out, background-color 0.15s ease', - userSelect: 'none', + transition: "opacity 0.2s ease-in-out, background-color 0.15s ease", + userSelect: "none", }} onMouseEnter={(e) => { if (!isDragging) { - (e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(0, 0, 0, 0.05)'; - (e.currentTarget as HTMLDivElement).style.borderLeftColor = fileColorBorderHover; + (e.currentTarget as HTMLDivElement).style.backgroundColor = + "rgba(0, 0, 0, 0.05)"; + (e.currentTarget as HTMLDivElement).style.borderLeftColor = + fileColorBorderHover; } }} onMouseLeave={(e) => { if (!isDragging) { - (e.currentTarget as HTMLDivElement).style.backgroundColor = file.isSelected ? 'rgba(0, 0, 0, 0.05)' : 'transparent'; - (e.currentTarget as HTMLDivElement).style.borderLeftColor = fileColorBorder; + (e.currentTarget as HTMLDivElement).style.backgroundColor = + file.isSelected ? "rgba(0, 0, 0, 0.05)" : "transparent"; + (e.currentTarget as HTMLDivElement).style.borderLeftColor = + fileColorBorder; } }} > - +
@@ -125,7 +133,7 @@ const FileMenuItem: React.FC = ({ onClick={(e) => e.stopPropagation()} size="sm" /> -
+
@@ -167,24 +175,36 @@ export const PageEditorFileDropdown: React.FC = ({ return ( -
+
{switchingTo === "pageEditor" ? ( ) : ( - + )} - {selectedCount}/{totalCount} files selected + + {selectedCount}/{totalCount} files selected +
- + {files.map((file, index) => { const colorIndex = fileColorMap.get(file.fileId as string) ?? 0; @@ -207,23 +227,33 @@ export const PageEditorFileDropdown: React.FC = ({ openFilesModal(); }} style={{ - padding: '0.75rem 0.75rem', - marginTop: '0.5rem', - cursor: 'pointer', - backgroundColor: 'transparent', - borderTop: '1px solid var(--border-subtle)', - transition: 'background-color 0.15s ease', + padding: "0.75rem 0.75rem", + marginTop: "0.5rem", + cursor: "pointer", + backgroundColor: "transparent", + borderTop: "1px solid var(--border-subtle)", + transition: "background-color 0.15s ease", }} onMouseEnter={(e) => { - (e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(59, 130, 246, 0.25)'; + (e.currentTarget as HTMLDivElement).style.backgroundColor = + "rgba(59, 130, 246, 0.25)"; }} onMouseLeave={(e) => { - (e.currentTarget as HTMLDivElement).style.backgroundColor = 'transparent'; + (e.currentTarget as HTMLDivElement).style.backgroundColor = + "transparent"; }} > - - - + + + Add File diff --git a/frontend/src/core/components/shared/PageSelectionSyntaxHint.tsx b/frontend/src/core/components/shared/PageSelectionSyntaxHint.tsx index bf7e642066..47cda328d8 100644 --- a/frontend/src/core/components/shared/PageSelectionSyntaxHint.tsx +++ b/frontend/src/core/components/shared/PageSelectionSyntaxHint.tsx @@ -1,47 +1,75 @@ -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Text } from '@mantine/core'; -import classes from '@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css'; -import { parseSelectionWithDiagnostics } from '@app/utils/bulkselection/parseSelection'; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Text } from "@mantine/core"; +import classes from "@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css"; +import { parseSelectionWithDiagnostics } from "@app/utils/bulkselection/parseSelection"; interface PageSelectionSyntaxHintProps { input: string; /** Optional known page count; if not provided, a large max is used for syntax-only checks */ maxPages?: number; /** panel = full bulk panel style, compact = inline tool style */ - variant?: 'panel' | 'compact'; + variant?: "panel" | "compact"; } const FALLBACK_MAX_PAGES = 100000; // large upper bound for syntax validation without a document -const PageSelectionSyntaxHint = ({ input, maxPages, variant = 'panel' }: PageSelectionSyntaxHintProps) => { +const PageSelectionSyntaxHint = ({ + input, + maxPages, + variant = "panel", +}: PageSelectionSyntaxHintProps) => { const [syntaxError, setSyntaxError] = useState(null); const { t } = useTranslation(); useEffect(() => { - const text = (input || '').trim(); + const text = (input || "").trim(); if (!text) { setSyntaxError(null); return; } try { - const { warning } = parseSelectionWithDiagnostics(text, maxPages && maxPages > 0 ? maxPages : FALLBACK_MAX_PAGES); - setSyntaxError(warning ? t('bulkSelection.syntaxError', 'There is a syntax issue. See Page Selection tips for help.') : null); + const { warning } = parseSelectionWithDiagnostics( + text, + maxPages && maxPages > 0 ? maxPages : FALLBACK_MAX_PAGES, + ); + setSyntaxError( + warning + ? t( + "bulkSelection.syntaxError", + "There is a syntax issue. See Page Selection tips for help.", + ) + : null, + ); } catch { - setSyntaxError(t('bulkSelection.syntaxError', 'There is a syntax issue. See Page Selection tips for help.')); + setSyntaxError( + t( + "bulkSelection.syntaxError", + "There is a syntax issue. See Page Selection tips for help.", + ), + ); } }, [input, maxPages]); if (!syntaxError) return null; return ( -
- {syntaxError} +
+ + {syntaxError} +
); }; export default PageSelectionSyntaxHint; - - diff --git a/frontend/src/core/components/shared/PrivateContent.tsx b/frontend/src/core/components/shared/PrivateContent.tsx index 3ed11bfc6d..270fe544ad 100644 --- a/frontend/src/core/components/shared/PrivateContent.tsx +++ b/frontend/src/core/components/shared/PrivateContent.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React from "react"; interface PrivateContentProps extends React.HTMLAttributes { children: React.ReactNode; @@ -25,12 +25,12 @@ interface PrivateContentProps extends React.HTMLAttributes { */ export const PrivateContent: React.FC = ({ children, - className = '', + className = "", style, ...props }) => { - const combinedClassName = `ph-no-capture${className ? ` ${className}` : ''}`; - const combinedStyle = { display: 'contents' as const, ...style }; + const combinedClassName = `ph-no-capture${className ? ` ${className}` : ""}`; + const combinedStyle = { display: "contents" as const, ...style }; return ( diff --git a/frontend/src/core/components/shared/QuickAccessBar.tsx b/frontend/src/core/components/shared/QuickAccessBar.tsx index 726ed19c43..16d0ff0964 100644 --- a/frontend/src/core/components/shared/QuickAccessBar.tsx +++ b/frontend/src/core/components/shared/QuickAccessBar.tsx @@ -1,49 +1,62 @@ -import React, { useState, useRef, forwardRef, useEffect, useMemo, useCallback } from "react"; -import { createPortal } from 'react-dom'; +import React, { + useState, + useRef, + forwardRef, + useEffect, + useMemo, + useCallback, +} from "react"; +import { createPortal } from "react-dom"; import { Stack, Divider, Menu, Indicator } from "@mantine/core"; -import { useTranslation } from 'react-i18next'; -import { useNavigate, useLocation } from 'react-router-dom'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import SignPopout, { SIGN_REQUEST_WORKBENCH_TYPE, SESSION_DETAIL_WORKBENCH_TYPE } from '@app/components/shared/signing/SignPopout'; +import { useTranslation } from "react-i18next"; +import { useNavigate, useLocation } from "react-router-dom"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import SignPopout, { + SIGN_REQUEST_WORKBENCH_TYPE, + SESSION_DETAIL_WORKBENCH_TYPE, +} from "@app/components/shared/signing/SignPopout"; import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider"; -import { useFilesModalContext } from '@app/contexts/FilesModalContext'; -import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; -import { useFileSelection, useFileState } from '@app/contexts/file/fileHooks'; -import { useNavigationState, useNavigationActions } from '@app/contexts/NavigationContext'; -import { useSidebarNavigation } from '@app/hooks/useSidebarNavigation'; -import { handleUnlessSpecialClick } from '@app/utils/clickHandlers'; -import { ButtonConfig } from '@app/types/sidebar'; -import '@app/components/shared/quickAccessBar/QuickAccessBar.css'; -import { Tooltip } from '@app/components/shared/Tooltip'; -import AllToolsNavButton from '@app/components/shared/AllToolsNavButton'; +import { useFilesModalContext } from "@app/contexts/FilesModalContext"; +import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext"; +import { useFileSelection, useFileState } from "@app/contexts/file/fileHooks"; +import { + useNavigationState, + useNavigationActions, +} from "@app/contexts/NavigationContext"; +import { useSidebarNavigation } from "@app/hooks/useSidebarNavigation"; +import { handleUnlessSpecialClick } from "@app/utils/clickHandlers"; +import { ButtonConfig } from "@app/types/sidebar"; +import "@app/components/shared/quickAccessBar/QuickAccessBar.css"; +import { Tooltip } from "@app/components/shared/Tooltip"; +import AllToolsNavButton from "@app/components/shared/AllToolsNavButton"; import ActiveToolButton from "@app/components/shared/quickAccessBar/ActiveToolButton"; -import AppConfigModal from '@app/components/shared/AppConfigModal'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import { useGroupSigningEnabled } from '@app/hooks/useGroupSigningEnabled'; -import { useSharingEnabled } from '@app/hooks/useSharingEnabled'; +import AppConfigModal from "@app/components/shared/AppConfigModal"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { useGroupSigningEnabled } from "@app/hooks/useGroupSigningEnabled"; +import { useSharingEnabled } from "@app/hooks/useSharingEnabled"; import { useLicenseAlert } from "@app/hooks/useLicenseAlert"; -import { requestStartTour } from '@app/constants/events'; -import QuickAccessButton from '@app/components/shared/quickAccessBar/QuickAccessButton'; -import { useToursTooltip } from '@app/components/shared/quickAccessBar/useToursTooltip'; -import ShareManagementModal from '@app/components/shared/ShareManagementModal'; -import apiClient from '@app/services/apiClient'; -import { absoluteWithBasePath } from '@app/constants/app'; -import { alert } from '@app/components/toast'; -import { uploadHistoryChain } from '@app/services/serverStorageUpload'; -import { fileStorage } from '@app/services/fileStorage'; -import { useFileActions } from '@app/contexts/FileContext'; -import type { FileId } from '@app/types/file'; -import type { StirlingFileStub } from '@app/types/fileContext'; -import type { SignRequestSummary } from '@app/types/signingSession'; +import { requestStartTour } from "@app/constants/events"; +import QuickAccessButton from "@app/components/shared/quickAccessBar/QuickAccessButton"; +import { useToursTooltip } from "@app/components/shared/quickAccessBar/useToursTooltip"; +import ShareManagementModal from "@app/components/shared/ShareManagementModal"; +import apiClient from "@app/services/apiClient"; +import { absoluteWithBasePath } from "@app/constants/app"; +import { alert } from "@app/components/toast"; +import { uploadHistoryChain } from "@app/services/serverStorageUpload"; +import { fileStorage } from "@app/services/fileStorage"; +import { useFileActions } from "@app/contexts/FileContext"; +import type { FileId } from "@app/types/file"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import type { SignRequestSummary } from "@app/types/signingSession"; import { isNavButtonActive, getNavButtonStyle, getActiveNavButton, -} from '@app/components/shared/quickAccessBar/QuickAccessBar'; -import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex'; -import { QuickAccessBarFooterExtensions } from '@app/components/quickAccessBar/QuickAccessBarFooterExtensions'; -import { useConfigButtonIcon } from '@app/hooks/useConfigButtonIcon'; +} from "@app/components/shared/quickAccessBar/QuickAccessBar"; +import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from "@app/styles/zIndex"; +import { QuickAccessBarFooterExtensions } from "@app/components/quickAccessBar/QuickAccessBarFooterExtensions"; +import { useConfigButtonIcon } from "@app/hooks/useConfigButtonIcon"; const QuickAccessBar = forwardRef((_, ref) => { const { t } = useTranslation(); @@ -59,34 +72,45 @@ const QuickAccessBar = forwardRef((_, ref) => { toolRegistry, readerMode, resetTool, - toolAvailability + toolAvailability, } = useToolWorkflow(); const { selectedFiles, selectedFileIds } = useFileSelection(); const { state, selectors } = useFileState(); const { actions } = useFileActions(); - const { hasUnsavedChanges, workbench: currentWorkbench } = useNavigationState(); + const { hasUnsavedChanges, workbench: currentWorkbench } = + useNavigationState(); const { actions: navigationActions } = useNavigationActions(); const { getToolNavigation } = useSidebarNavigation(); const { config } = useAppConfig(); const licenseAlert = useLicenseAlert(); const [configModalOpen, setConfigModalOpen] = useState(false); - const [activeButton, setActiveButton] = useState('tools'); + const [activeButton, setActiveButton] = useState("tools"); const [accessMenuOpen, setAccessMenuOpen] = useState(false); const [accessInviteOpen, setAccessInviteOpen] = useState(false); - const [selectedAccessFileId, setSelectedAccessFileId] = useState(null); + const [selectedAccessFileId, setSelectedAccessFileId] = useState< + string | null + >(null); const [shareManageOpen, setShareManageOpen] = useState(false); const scrollableRef = useRef(null); const accessButtonRef = useRef(null); const accessPopoverRef = useRef(null); - const [accessPopoverPosition, setAccessPopoverPosition] = useState({ top: 160, left: 84 }); + const [accessPopoverPosition, setAccessPopoverPosition] = useState({ + top: 160, + left: 84, + }); const { sharingEnabled, shareLinksEnabled } = useSharingEnabled(); const groupSigningEnabled = useGroupSigningEnabled(); const isSignWorkbenchActive = currentWorkbench === SIGN_REQUEST_WORKBENCH_TYPE || currentWorkbench === SESSION_DETAIL_WORKBENCH_TYPE; - const [inviteRows, setInviteRows] = useState>([ - { id: Date.now(), email: '', role: 'editor' }, - ]); + const [inviteRows, setInviteRows] = useState< + Array<{ + id: number; + email: string; + role: "editor" | "commenter" | "viewer"; + error?: string; + }> + >([{ id: Date.now(), email: "", role: "editor" }]); const [isInviting, setIsInviting] = useState(false); // Sign button state @@ -99,12 +123,16 @@ const QuickAccessBar = forwardRef((_, ref) => { if (!groupSigningEnabled) return; const fetchCount = async () => { try { - const response = await apiClient.get('/api/v1/security/cert-sign/sign-requests'); + const response = await apiClient.get( + "/api/v1/security/cert-sign/sign-requests", + ); const pending = response.data.filter( - r => r.myStatus !== 'SIGNED' && r.myStatus !== 'DECLINED' + (r) => r.myStatus !== "SIGNED" && r.myStatus !== "DECLINED", ).length; setPendingSignCount(pending); - } catch { /* silent — avoid noisy background error toasts */ } + } catch { + /* silent — avoid noisy background error toasts */ + } }; fetchCount(); const interval = setInterval(fetchCount, 60000); @@ -116,12 +144,16 @@ const QuickAccessBar = forwardRef((_, ref) => { if (!signMenuOpen && groupSigningEnabled) { const timeout = setTimeout(async () => { try { - const response = await apiClient.get('/api/v1/security/cert-sign/sign-requests'); + const response = await apiClient.get( + "/api/v1/security/cert-sign/sign-requests", + ); const pending = response.data.filter( - r => r.myStatus !== 'SIGNED' && r.myStatus !== 'DECLINED' + (r) => r.myStatus !== "SIGNED" && r.myStatus !== "DECLINED", ).length; setPendingSignCount(pending); - } catch { /* silent */ } + } catch { + /* silent */ + } }, 500); return () => clearTimeout(timeout); } @@ -138,14 +170,19 @@ const QuickAccessBar = forwardRef((_, ref) => { handleTooltipOpenChange, } = useToursTooltip(); - const isRTL = typeof document !== 'undefined' && document.documentElement.dir === 'rtl'; + const isRTL = + typeof document !== "undefined" && document.documentElement.dir === "rtl"; const hasSelectedFiles = selectedFiles.length > 0; const selectedFileStubs = useMemo( - () => selectedFileIds.map((id) => selectors.getStirlingFileStub(id)).filter((x): x is StirlingFileStub => Boolean(x)), - [selectedFileIds, selectors, state.files.byId] + () => + selectedFileIds + .map((id) => selectors.getStirlingFileStub(id)) + .filter((x): x is StirlingFileStub => Boolean(x)), + [selectedFileIds, selectors, state.files.byId], ); const selectedAccessFileStub = - selectedFileStubs.find((file) => file.id === selectedAccessFileId) || selectedFileStubs[0]; + selectedFileStubs.find((file) => file.id === selectedAccessFileId) || + selectedFileStubs[0]; useEffect(() => { if (!hasSelectedFiles) { setAccessMenuOpen(false); @@ -153,13 +190,16 @@ const QuickAccessBar = forwardRef((_, ref) => { setAccessInviteOpen(false); return; } - if (!selectedAccessFileId || !selectedFiles.some((file) => file.fileId === selectedAccessFileId)) { + if ( + !selectedAccessFileId || + !selectedFiles.some((file) => file.fileId === selectedAccessFileId) + ) { setSelectedAccessFileId(selectedFiles[0]?.fileId ?? null); } }, [hasSelectedFiles, selectedAccessFileId, selectedFiles]); const resetInviteRows = useCallback(() => { - setInviteRows([{ id: Date.now(), email: '', role: 'editor' }]); + setInviteRows([{ id: Date.now(), email: "", role: "editor" }]); }, []); useEffect(() => { @@ -176,11 +216,11 @@ const QuickAccessBar = forwardRef((_, ref) => { setAccessPopoverPosition({ top, left }); }; updatePosition(); - window.addEventListener('resize', updatePosition); - window.addEventListener('scroll', updatePosition, true); + window.addEventListener("resize", updatePosition); + window.addEventListener("scroll", updatePosition, true); return () => { - window.removeEventListener('resize', updatePosition); - window.removeEventListener('scroll', updatePosition, true); + window.removeEventListener("resize", updatePosition); + window.removeEventListener("scroll", updatePosition, true); }; }, [accessMenuOpen, isRTL, resetInviteRows]); @@ -192,77 +232,85 @@ const QuickAccessBar = forwardRef((_, ref) => { if (accessButtonRef.current?.contains(target)) return; // Check if click is inside a Mantine dropdown - const mantineDropdown = (target as Element).closest?.('.mantine-Combobox-dropdown, .mantine-Popover-dropdown'); + const mantineDropdown = (target as Element).closest?.( + ".mantine-Combobox-dropdown, .mantine-Popover-dropdown", + ); if (mantineDropdown) return; setAccessMenuOpen(false); }; const handleEscape = (event: KeyboardEvent) => { - if (event.key === 'Escape') { + if (event.key === "Escape") { setAccessMenuOpen(false); } }; - document.addEventListener('mousedown', handleOutside); - document.addEventListener('keydown', handleEscape); + document.addEventListener("mousedown", handleOutside); + document.addEventListener("keydown", handleEscape); return () => { - document.removeEventListener('mousedown', handleOutside); - document.removeEventListener('keydown', handleEscape); + document.removeEventListener("mousedown", handleOutside); + document.removeEventListener("keydown", handleEscape); }; }, [accessMenuOpen]); const shareBaseUrl = useMemo(() => { - const frontendUrl = (config?.frontendUrl || '').trim(); + const frontendUrl = (config?.frontendUrl || "").trim(); if (frontendUrl) { try { const parsed = new URL(frontendUrl); - if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { - const normalized = frontendUrl.endsWith('/') ? frontendUrl.slice(0, -1) : frontendUrl; + if (parsed.protocol === "http:" || parsed.protocol === "https:") { + const normalized = frontendUrl.endsWith("/") + ? frontendUrl.slice(0, -1) + : frontendUrl; return `${normalized}/share/`; } } catch { // invalid URL — fall through to default } } - return absoluteWithBasePath('/share/'); + return absoluteWithBasePath("/share/"); }, [config?.frontendUrl]); - const ensureStoredFile = useCallback(async (fileStub: StirlingFileStub): Promise => { - const localUpdatedAt = fileStub.createdAt ?? fileStub.lastModified ?? 0; - const isUpToDate = - Boolean(fileStub.remoteStorageId) && - Boolean(fileStub.remoteStorageUpdatedAt) && - (fileStub.remoteStorageUpdatedAt as number) >= localUpdatedAt; - if (isUpToDate && fileStub.remoteStorageId) { - return fileStub.remoteStorageId as number; - } - const originalFileId = (fileStub.originalFileId || fileStub.id) as FileId; - const remoteId = fileStub.remoteStorageId as number | undefined; - const { remoteId: storedId, updatedAt, chain } = await uploadHistoryChain( - originalFileId, - remoteId - ); - for (const stub of chain) { - actions.updateStirlingFileStub(stub.id, { - remoteStorageId: storedId, - remoteStorageUpdatedAt: updatedAt, - remoteOwnedByCurrentUser: true, - remoteSharedViaLink: false, - }); - await fileStorage.updateFileMetadata(stub.id, { - remoteStorageId: storedId, - remoteStorageUpdatedAt: updatedAt, - remoteOwnedByCurrentUser: true, - remoteSharedViaLink: false, - }); - } - return storedId; - }, [actions]); + const ensureStoredFile = useCallback( + async (fileStub: StirlingFileStub): Promise => { + const localUpdatedAt = fileStub.createdAt ?? fileStub.lastModified ?? 0; + const isUpToDate = + Boolean(fileStub.remoteStorageId) && + Boolean(fileStub.remoteStorageUpdatedAt) && + (fileStub.remoteStorageUpdatedAt as number) >= localUpdatedAt; + if (isUpToDate && fileStub.remoteStorageId) { + return fileStub.remoteStorageId as number; + } + const originalFileId = (fileStub.originalFileId || fileStub.id) as FileId; + const remoteId = fileStub.remoteStorageId as number | undefined; + const { + remoteId: storedId, + updatedAt, + chain, + } = await uploadHistoryChain(originalFileId, remoteId); + for (const stub of chain) { + actions.updateStirlingFileStub(stub.id, { + remoteStorageId: storedId, + remoteStorageUpdatedAt: updatedAt, + remoteOwnedByCurrentUser: true, + remoteSharedViaLink: false, + }); + await fileStorage.updateFileMetadata(stub.id, { + remoteStorageId: storedId, + remoteStorageUpdatedAt: updatedAt, + remoteOwnedByCurrentUser: true, + remoteSharedViaLink: false, + }); + } + return storedId; + }, + [actions], + ); const openShareManage = useCallback(async () => { if (!sharingEnabled) { alert({ - alertType: 'warning', - title: t('storageShare.sharingDisabled', 'Sharing is disabled.'), + alertType: "warning", + title: t("storageShare.sharingDisabled", "Sharing is disabled."), expandable: false, durationMs: 2500, }); @@ -270,8 +318,11 @@ const QuickAccessBar = forwardRef((_, ref) => { } if (selectedFileStubs.length > 1) { alert({ - alertType: 'warning', - title: t('storageShare.selectSingleFile', 'Select a single file to manage sharing.'), + alertType: "warning", + title: t( + "storageShare.selectSingleFile", + "Select a single file to manage sharing.", + ), expandable: false, durationMs: 2500, }); @@ -279,8 +330,11 @@ const QuickAccessBar = forwardRef((_, ref) => { } if (selectedAccessFileStub?.remoteOwnedByCurrentUser === false) { alert({ - alertType: 'warning', - title: t('storageShare.ownerOnly', 'Only the owner can manage sharing.'), + alertType: "warning", + title: t( + "storageShare.ownerOnly", + "Only the owner can manage sharing.", + ), expandable: false, durationMs: 2500, }); @@ -293,45 +347,72 @@ const QuickAccessBar = forwardRef((_, ref) => { setAccessMenuOpen(false); setShareManageOpen(true); } catch (error) { - console.error('Failed to upload file for sharing:', error); + console.error("Failed to upload file for sharing:", error); alert({ - alertType: 'warning', - title: t('storageUpload.failure', 'Upload failed. Please check your login and storage settings.'), + alertType: "warning", + title: t( + "storageUpload.failure", + "Upload failed. Please check your login and storage settings.", + ), expandable: false, durationMs: 3000, }); } - }, [ensureStoredFile, selectedAccessFileStub, selectedFileStubs.length, sharingEnabled, t]); + }, [ + ensureStoredFile, + selectedAccessFileStub, + selectedFileStubs.length, + sharingEnabled, + t, + ]); const handleInviteRowChange = useCallback( - (id: number, updates: Partial<{ email: string; role: 'editor' | 'commenter' | 'viewer'; error?: string }>) => { + ( + id: number, + updates: Partial<{ + email: string; + role: "editor" | "commenter" | "viewer"; + error?: string; + }>, + ) => { setInviteRows((prev) => prev.map((row) => { if (row.id !== id) return row; - const nextError = Object.prototype.hasOwnProperty.call(updates, 'error') + const nextError = Object.prototype.hasOwnProperty.call( + updates, + "error", + ) ? updates.error : row.error; return { ...row, ...updates, error: nextError }; - }) + }), ); }, - [] + [], ); const handleAddInviteRow = useCallback(() => { - setInviteRows((prev) => [...prev, { id: Date.now(), email: '', role: 'editor' }]); + setInviteRows((prev) => [ + ...prev, + { id: Date.now(), email: "", role: "editor" }, + ]); }, []); const handleRemoveInviteRow = useCallback((id: number) => { - setInviteRows((prev) => (prev.length > 1 ? prev.filter((row) => row.id !== id) : prev)); + setInviteRows((prev) => + prev.length > 1 ? prev.filter((row) => row.id !== id) : prev, + ); }, []); const handleSendInvites = useCallback(async () => { if (!selectedAccessFileStub) return; if (selectedAccessFileStub.remoteOwnedByCurrentUser === false) { alert({ - alertType: 'warning', - title: t('storageShare.ownerOnly', 'Only the owner can manage sharing.'), + alertType: "warning", + title: t( + "storageShare.ownerOnly", + "Only the owner can manage sharing.", + ), expandable: false, durationMs: 2500, }); @@ -341,7 +422,10 @@ const QuickAccessBar = forwardRef((_, ref) => { const trimmed = row.email.trim(); let error: string | undefined; if (!trimmed) { - error = t('storageShare.invalidUsername', 'Enter a valid username or email address.'); + error = t( + "storageShare.invalidUsername", + "Enter a valid username or email address.", + ); } return { ...row, email: trimmed, error }; }); @@ -359,32 +443,41 @@ const QuickAccessBar = forwardRef((_, ref) => { }); } alert({ - alertType: 'success', - title: t('storageShare.userAdded', 'User added to shared list.'), + alertType: "success", + title: t("storageShare.userAdded", "User added to shared list."), expandable: false, durationMs: 2500, }); setAccessInviteOpen(false); resetInviteRows(); } catch (error) { - console.error('Failed to send invite:', error); + console.error("Failed to send invite:", error); alert({ - alertType: 'warning', - title: t('storageShare.userAddFailed', 'Unable to share with that user.'), + alertType: "warning", + title: t( + "storageShare.userAddFailed", + "Unable to share with that user.", + ), expandable: false, durationMs: 3000, }); } finally { setIsInviting(false); } - }, [ensureStoredFile, inviteRows, resetInviteRows, selectedAccessFileStub, t]); + }, [ + ensureStoredFile, + inviteRows, + resetInviteRows, + selectedAccessFileStub, + t, + ]); const handleCopyShareLink = async () => { if (!selectedAccessFileStub) return; if (!shareLinksEnabled) { alert({ - alertType: 'warning', - title: t('storageShare.linksDisabled', 'Share links are disabled.'), + alertType: "warning", + title: t("storageShare.linksDisabled", "Share links are disabled."), expandable: false, durationMs: 2500, }); @@ -392,8 +485,11 @@ const QuickAccessBar = forwardRef((_, ref) => { } if (selectedFileStubs.length > 1) { alert({ - alertType: 'warning', - title: t('storageShare.selectSingleFile', 'Select a single file to copy a link.'), + alertType: "warning", + title: t( + "storageShare.selectSingleFile", + "Select a single file to copy a link.", + ), expandable: false, durationMs: 2500, }); @@ -401,8 +497,11 @@ const QuickAccessBar = forwardRef((_, ref) => { } if (selectedAccessFileStub?.remoteOwnedByCurrentUser === false) { alert({ - alertType: 'warning', - title: t('storageShare.ownerOnly', 'Only the owner can manage sharing.'), + alertType: "warning", + title: t( + "storageShare.ownerOnly", + "Only the owner can manage sharing.", + ), expandable: false, durationMs: 2500, }); @@ -412,10 +511,13 @@ const QuickAccessBar = forwardRef((_, ref) => { try { await ensureStoredFile(selectedAccessFileStub); } catch (error) { - console.error('Failed to upload file for sharing:', error); + console.error("Failed to upload file for sharing:", error); alert({ - alertType: 'warning', - title: t('storageUpload.failure', 'Upload failed. Please check your login and storage settings.'), + alertType: "warning", + title: t( + "storageUpload.failure", + "Upload failed. Please check your login and storage settings.", + ), expandable: false, durationMs: 3000, }); @@ -424,26 +526,37 @@ const QuickAccessBar = forwardRef((_, ref) => { } try { const storedId = await ensureStoredFile(selectedAccessFileStub); - const response = await apiClient.get<{ shareLinks?: Array<{ token?: string }> }>( - `/api/v1/storage/files/${storedId}`, - { suppressErrorToast: true } - ); + const response = await apiClient.get<{ + shareLinks?: Array<{ token?: string }>; + }>(`/api/v1/storage/files/${storedId}`, { + suppressErrorToast: true, + }); const links = response.data?.shareLinks ?? []; let token = links[links.length - 1]?.token; if (!token) { - const shareResponse = await apiClient.post(`/api/v1/storage/files/${storedId}/shares/links`, { - accessRole: 'editor', - }); + const shareResponse = await apiClient.post( + `/api/v1/storage/files/${storedId}/shares/links`, + { + accessRole: "editor", + }, + ); token = shareResponse.data?.token; if (token) { - actions.updateStirlingFileStub(selectedAccessFileStub.id, { remoteHasShareLinks: true }); - await fileStorage.updateFileMetadata(selectedAccessFileStub.id, { remoteHasShareLinks: true }); + actions.updateStirlingFileStub(selectedAccessFileStub.id, { + remoteHasShareLinks: true, + }); + await fileStorage.updateFileMetadata(selectedAccessFileStub.id, { + remoteHasShareLinks: true, + }); } } if (!token) { alert({ - alertType: 'warning', - title: t('storageShare.failure', 'Unable to generate a share link. Please try again.'), + alertType: "warning", + title: t( + "storageShare.failure", + "Unable to generate a share link. Please try again.", + ), expandable: false, durationMs: 2500, }); @@ -451,26 +564,25 @@ const QuickAccessBar = forwardRef((_, ref) => { } await navigator.clipboard.writeText(`${shareBaseUrl}${token}`); alert({ - alertType: 'success', - title: t('storageShare.copied', 'Link copied to clipboard'), + alertType: "success", + title: t("storageShare.copied", "Link copied to clipboard"), expandable: false, durationMs: 2000, }); } catch (error) { - console.error('Failed to copy share link:', error); + console.error("Failed to copy share link:", error); alert({ - alertType: 'warning', - title: t('storageShare.copyFailed', 'Copy failed'), + alertType: "warning", + title: t("storageShare.copyFailed", "Copy failed"), expandable: false, durationMs: 2500, }); } }; - // Open modal if URL is at /settings/* useEffect(() => { - const isSettings = location.pathname.startsWith('/settings'); + const isSettings = location.pathname.startsWith("/settings"); setConfigModalOpen(isSettings); }, [location.pathname]); @@ -484,13 +596,28 @@ const QuickAccessBar = forwardRef((_, ref) => { }; // Helper function to render navigation buttons with URL support - const renderNavButton = (config: ButtonConfig, index: number, shouldGuardNavigation = false) => { - const isActive = !isSignWorkbenchActive && isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView); + const renderNavButton = ( + config: ButtonConfig, + index: number, + shouldGuardNavigation = false, + ) => { + const isActive = + !isSignWorkbenchActive && + isNavButtonActive( + config, + activeButton, + isFilesModalOpen, + configModalOpen, + selectedToolKey, + leftPanelView, + ); // Check if this button has URL navigation support - const navProps = config.type === 'navigation' && (config.id === 'read' || config.id === 'automate') - ? getToolNavigation(config.id) - : null; + const navProps = + config.type === "navigation" && + (config.id === "read" || config.id === "automate") + ? getToolNavigation(config.id) + : null; const handleClick = (e?: React.MouseEvent) => { // If there are unsaved changes and this button should guard navigation, show warning modal @@ -509,14 +636,26 @@ const QuickAccessBar = forwardRef((_, ref) => { }; const buttonStyle = isSignWorkbenchActive - ? { backgroundColor: 'var(--icon-inactive-bg)', color: 'var(--icon-inactive-color)', border: 'none', borderRadius: '0.5rem' } - : getNavButtonStyle(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView); + ? { + backgroundColor: "var(--icon-inactive-bg)", + color: "var(--icon-inactive-color)", + border: "none", + borderRadius: "0.5rem", + } + : getNavButtonStyle( + config, + activeButton, + isFilesModalOpen, + configModalOpen, + selectedToolKey, + leftPanelView, + ); // Render navigation button with conditional URL support return (
((_, ref) => { ariaLabel={config.name} backgroundColor={buttonStyle.backgroundColor} color={buttonStyle.color} - component={navProps ? 'a' : 'button'} + component={navProps ? "a" : "button"} dataTestId={`${config.id}-button`} dataTour={`${config.id}-button`} /> @@ -535,54 +674,81 @@ const QuickAccessBar = forwardRef((_, ref) => { ); }; - const mainButtons: ButtonConfig[] = useMemo(() => [ - { - id: 'read', - name: t("quickAccess.reader", "Reader"), - icon: , - size: 'md' as const, - isRound: false, - type: 'navigation' as const, - onClick: () => { - setActiveButton('read'); - handleReaderToggle(); - } - }, - { - id: 'automate', - name: t("quickAccess.automate", "Automate"), - icon: , - size: 'md' as const, - isRound: false, - type: 'navigation' as const, - onClick: () => { - setActiveButton('automate'); - // If already on automate tool, reset it directly - if (selectedToolKey === 'automate') { - resetTool('automate'); - } else { - handleToolSelect('automate'); - } - } - }, - ].filter(button => { - // Filter out buttons for disabled tools - // 'read' is always available (viewer mode) - if (button.id === 'read') return true; - // Check if tool is actually available (not just present in registry) - const availability = toolAvailability[button.id as keyof typeof toolAvailability]; - return availability?.available !== false; - }), [t, setActiveButton, handleReaderToggle, selectedToolKey, resetTool, handleToolSelect, toolAvailability]); + const mainButtons: ButtonConfig[] = useMemo( + () => + [ + { + id: "read", + name: t("quickAccess.reader", "Reader"), + icon: ( + + ), + size: "md" as const, + isRound: false, + type: "navigation" as const, + onClick: () => { + setActiveButton("read"); + handleReaderToggle(); + }, + }, + { + id: "automate", + name: t("quickAccess.automate", "Automate"), + icon: ( + + ), + size: "md" as const, + isRound: false, + type: "navigation" as const, + onClick: () => { + setActiveButton("automate"); + // If already on automate tool, reset it directly + if (selectedToolKey === "automate") { + resetTool("automate"); + } else { + handleToolSelect("automate"); + } + }, + }, + ].filter((button) => { + // Filter out buttons for disabled tools + // 'read' is always available (viewer mode) + if (button.id === "read") return true; + // Check if tool is actually available (not just present in registry) + const availability = + toolAvailability[button.id as keyof typeof toolAvailability]; + return availability?.available !== false; + }), + [ + t, + setActiveButton, + handleReaderToggle, + selectedToolKey, + resetTool, + handleToolSelect, + toolAvailability, + ], + ); const middleButtons: ButtonConfig[] = [ { - id: 'files', + id: "files", name: t("quickAccess.files", "Files"), - icon: , + icon: ( + + ), isRound: true, - size: 'md', - type: 'modal', - onClick: handleFilesButtonClick + size: "md", + type: "modal", + onClick: handleFilesButtonClick, }, ]; //TODO: Activity @@ -599,50 +765,64 @@ const QuickAccessBar = forwardRef((_, ref) => { // Determine if settings button should be hidden // Hide when login is disabled AND showSettingsWhenNoLogin is false const shouldHideSettingsButton = - config?.enableLogin === false && - config?.showSettingsWhenNoLogin === false; + config?.enableLogin === false && config?.showSettingsWhenNoLogin === false; const bottomButtons: ButtonConfig[] = [ { - id: 'help', + id: "help", name: t("quickAccess.tours", "Tours"), - icon: , + icon: ( + + ), isRound: true, - size: 'md', - type: 'action', + size: "md", + type: "action", onClick: () => { // This will be overridden by the wrapper logic }, }, - ...(shouldHideSettingsButton ? [] : [{ - id: 'config', - name: t("quickAccess.settings", "Settings"), - icon: configButtonIcon ?? , - size: 'md' as const, - type: 'modal' as const, - onClick: () => { - navigate('/settings/overview'); - setConfigModalOpen(true); - } - } as ButtonConfig]) + ...(shouldHideSettingsButton + ? [] + : [ + { + id: "config", + name: t("quickAccess.settings", "Settings"), + icon: configButtonIcon ?? ( + + ), + size: "md" as const, + type: "modal" as const, + onClick: () => { + navigate("/settings/overview"); + setConfigModalOpen(true); + }, + } as ButtonConfig, + ]), ]; - return (
{/* Fixed header outside scrollable area */}
- - - + +
- {/* Scrollable content area */}
((_, ref) => { {mainButtons.map((config, index) => ( - {renderNavButton(config, index, config.id === 'read' || config.id === 'automate')} + {renderNavButton( + config, + index, + config.id === "read" || config.id === "automate", + )} ))} @@ -665,10 +849,7 @@ const QuickAccessBar = forwardRef((_, ref) => { {/* Middle section */} {middleButtons.length > 0 && ( <> - + {middleButtons.map((config, index) => ( @@ -678,19 +859,28 @@ const QuickAccessBar = forwardRef((_, ref) => { {hasSelectedFiles && sharingEnabled && (
} - label={t('quickAccess.access', 'Access')} + icon={ + + } + label={t("quickAccess.access", "Access")} isActive={!isSignWorkbenchActive && accessMenuOpen} onClick={() => { setAccessMenuOpen((prev) => !prev); }} - ariaLabel={t('quickAccess.access', 'Access')} + ariaLabel={t("quickAccess.access", "Access")} dataTestId="access-button" />
)} {groupSigningEnabled && ( -
+
{pendingSignCount > 0 ? ( ((_, ref) => { offset={4} > } - label={t('quickAccess.sign', 'Sign')} + icon={ + + } + label={t("quickAccess.sign", "Sign")} isActive={signMenuOpen || isSignWorkbenchActive} onClick={() => setSignMenuOpen((prev) => !prev)} - ariaLabel={t('quickAccess.sign', 'Sign')} + ariaLabel={t("quickAccess.sign", "Sign")} dataTestId="sign-button" /> ) : ( } - label={t('quickAccess.sign', 'Sign')} + icon={ + + } + label={t("quickAccess.sign", "Sign")} isActive={signMenuOpen || isSignWorkbenchActive} onClick={() => setSignMenuOpen((prev) => !prev)} - ariaLabel={t('quickAccess.sign', 'Sign')} + ariaLabel={t("quickAccess.sign", "Sign")} dataTestId="sign-button" /> )} @@ -734,39 +936,82 @@ const QuickAccessBar = forwardRef((_, ref) => { {bottomButtons.map((buttonConfig, index) => { // Handle help button with menu or direct action - if (buttonConfig.id === 'help') { + if (buttonConfig.id === "help") { const isAdmin = config?.isAdmin === true; const toursTooltipContent = isAdmin - ? t('quickAccess.toursTooltip.admin', 'Watch walkthroughs here: Tools tour, New V2 layout tour, and the Admin tour.') - : t('quickAccess.toursTooltip.user', 'Watch walkthroughs here: Tools tour and the New V2 layout tour.'); + ? t( + "quickAccess.toursTooltip.admin", + "Watch walkthroughs here: Tools tour, New V2 layout tour, and the Admin tour.", + ) + : t( + "quickAccess.toursTooltip.user", + "Watch walkthroughs here: Tools tour and the New V2 layout tour.", + ); const tourItems = [ { - key: 'whatsnew', - icon: , - title: t("quickAccess.helpMenu.whatsNewTour", "See what's new in V2"), - description: t("quickAccess.helpMenu.whatsNewTourDesc", "Tour the updated layout"), - onClick: () => requestStartTour('whatsnew'), + key: "whatsnew", + icon: ( + + ), + title: t( + "quickAccess.helpMenu.whatsNewTour", + "See what's new in V2", + ), + description: t( + "quickAccess.helpMenu.whatsNewTourDesc", + "Tour the updated layout", + ), + onClick: () => requestStartTour("whatsnew"), }, { - key: 'tools', - icon: , + key: "tools", + icon: ( + + ), title: t("quickAccess.helpMenu.toolsTour", "Tools Tour"), - description: t("quickAccess.helpMenu.toolsTourDesc", "Learn what the tools can do"), - onClick: () => requestStartTour('tools'), + description: t( + "quickAccess.helpMenu.toolsTourDesc", + "Learn what the tools can do", + ), + onClick: () => requestStartTour("tools"), }, - ...(isAdmin ? [{ - key: 'admin', - icon: , - title: t("quickAccess.helpMenu.adminTour", "Admin Tour"), - description: t("quickAccess.helpMenu.adminTourDesc", "Explore admin settings & features"), - onClick: () => requestStartTour('admin'), - }] : []), + ...(isAdmin + ? [ + { + key: "admin", + icon: ( + + ), + title: t( + "quickAccess.helpMenu.adminTour", + "Admin Tour", + ), + description: t( + "quickAccess.helpMenu.adminTourDesc", + "Explore admin settings & features", + ), + onClick: () => requestStartTour("admin"), + }, + ] + : []), ]; const helpButtonNode = (
((_, ref) => {
{item.title}
-
+
{item.description}
@@ -819,9 +1066,9 @@ const QuickAccessBar = forwardRef((_, ref) => { const buttonNode = renderNavButton(buttonConfig, index); const shouldShowSettingsBadge = - buttonConfig.id === 'config' && + buttonConfig.id === "config" && licenseAlert.active && - licenseAlert.audience === 'admin'; + licenseAlert.audience === "admin"; return ( @@ -857,239 +1104,310 @@ const QuickAccessBar = forwardRef((_, ref) => { file={selectedAccessFileStub} /> )} - {hasSelectedFiles && typeof document !== 'undefined' && createPortal( -
-
-
- -
- {accessInviteOpen - ? t('quickAccess.accessInviteTitle', 'Invite People') - : t('quickAccess.accessTitle', 'Document Access')} -
-
- {!accessInviteOpen && ( + {hasSelectedFiles && + typeof document !== "undefined" && + createPortal( +
+
+
+ +
+ {accessInviteOpen + ? t("quickAccess.accessInviteTitle", "Invite People") + : t("quickAccess.accessTitle", "Document Access")} +
+
+ {!accessInviteOpen && ( + + )} - )} - -
-
- -
-
-
-
- {t('quickAccess.accessFileLabel', 'File')} -
- -
- -
- -
-
- {t('quickAccess.accessGeneral', 'General Access')} -
-
-
- -
-
-
- {t('quickAccess.accessRestricted', 'Restricted')} -
-
- {t('quickAccess.accessRestrictedHint', 'Only people with access can open')} -
-
-
-
- -
- -
-
- {t('quickAccess.accessPeople', 'People with access')} -
-
-
- {(selectedAccessFileStub?.remoteOwnerUsername || 'You').slice(0, 2).toUpperCase()} -
-
-
- {selectedAccessFileStub?.remoteOwnerUsername || t('quickAccess.accessYou', 'You')} -
-
- {selectedAccessFileStub?.name ?? t('quickAccess.accessSelectedFile', 'Selected file')} -
-
- - {t('quickAccess.accessOwner', 'Owner')} - -
-
-
-
- {t('quickAccess.accessInviteTitle', 'Invite People')} -
-
- {inviteRows.map((row) => ( -
-
- - - handleInviteRowChange(row.id, { email: event.target.value, error: undefined }) - } - /> - {row.error && ( -
{row.error}
- )} +
+
+
+
+ {t("quickAccess.accessFileLabel", "File")}
-
- - -
-
- ))} - -
-
+
-
- {accessInviteOpen ? ( - <> +
+
+ {t("quickAccess.accessGeneral", "General Access")} +
+
+
+ +
+
+
+ {t("quickAccess.accessRestricted", "Restricted")} +
+
+ {t( + "quickAccess.accessRestrictedHint", + "Only people with access can open", + )} +
+
+
+
+ +
+ +
+
+ {t("quickAccess.accessPeople", "People with access")} +
+
+
+ {(selectedAccessFileStub?.remoteOwnerUsername || "You") + .slice(0, 2) + .toUpperCase()} +
+
+
+ {selectedAccessFileStub?.remoteOwnerUsername || + t("quickAccess.accessYou", "You")} +
+
+ {selectedAccessFileStub?.name ?? + t( + "quickAccess.accessSelectedFile", + "Selected file", + )} +
+
+ + {t("quickAccess.accessOwner", "Owner")} + +
+
+
+ +
+
+
+ {t("quickAccess.accessInviteTitle", "Invite People")} +
+
+ {inviteRows.map((row) => ( +
+
+ + + handleInviteRowChange(row.id, { + email: event.target.value, + error: undefined, + }) + } + /> + {row.error && ( +
+ {row.error} +
+ )} +
+
+ + +
+ +
+ ))} - {shareLinksEnabled && ( - - )} - - ) : ( - <> - {sharingEnabled && ( +
+
+ +
+ {accessInviteOpen ? ( + <> - )} - {shareLinksEnabled && ( - - )} - - )} + {shareLinksEnabled && ( + + )} + + ) : ( + <> + {sharingEnabled && ( + + )} + {shareLinksEnabled && ( + + )} + + )} +
-
-
, - document.body - )} +
, + document.body, + )} {/* Sign Popover */} ((_, ref) => { ); }); -QuickAccessBar.displayName = 'QuickAccessBar'; +QuickAccessBar.displayName = "QuickAccessBar"; export default QuickAccessBar; diff --git a/frontend/src/core/components/shared/RainbowThemeProvider.tsx b/frontend/src/core/components/shared/RainbowThemeProvider.tsx index 992aa79b5c..25ddffaa72 100644 --- a/frontend/src/core/components/shared/RainbowThemeProvider.tsx +++ b/frontend/src/core/components/shared/RainbowThemeProvider.tsx @@ -1,12 +1,12 @@ -import { createContext, useContext, ReactNode } from 'react'; -import { MantineProvider } from '@mantine/core'; -import { useRainbowTheme } from '@app/hooks/useRainbowTheme'; -import { mantineTheme } from '@app/theme/mantineTheme'; -import rainbowStyles from '@app/styles/rainbow.module.css'; -import { ToastProvider } from '@app/components/toast'; -import ToastRenderer from '@app/components/toast/ToastRenderer'; -import { ToastPortalBinder } from '@app/components/toast'; -import type { ThemeMode } from '@app/constants/theme'; +import { createContext, useContext, ReactNode } from "react"; +import { MantineProvider } from "@mantine/core"; +import { useRainbowTheme } from "@app/hooks/useRainbowTheme"; +import { mantineTheme } from "@app/theme/mantineTheme"; +import rainbowStyles from "@app/styles/rainbow.module.css"; +import { ToastProvider } from "@app/components/toast"; +import ToastRenderer from "@app/components/toast/ToastRenderer"; +import { ToastPortalBinder } from "@app/components/toast"; +import type { ThemeMode } from "@app/constants/theme"; interface RainbowThemeContextType { themeMode: ThemeMode; @@ -22,7 +22,9 @@ const RainbowThemeContext = createContext(null); export function useRainbowThemeContext() { const context = useContext(RainbowThemeContext); if (!context) { - throw new Error('useRainbowThemeContext must be used within RainbowThemeProvider'); + throw new Error( + "useRainbowThemeContext must be used within RainbowThemeProvider", + ); } return context; } @@ -35,7 +37,8 @@ export function RainbowThemeProvider({ children }: RainbowThemeProviderProps) { const rainbowTheme = useRainbowTheme(); // Determine the Mantine color scheme - const mantineColorScheme = rainbowTheme.themeMode === 'rainbow' ? 'dark' : rainbowTheme.themeMode; + const mantineColorScheme = + rainbowTheme.themeMode === "rainbow" ? "dark" : rainbowTheme.themeMode; return ( @@ -45,8 +48,10 @@ export function RainbowThemeProvider({ children }: RainbowThemeProviderProps) { forceColorScheme={mantineColorScheme} >
diff --git a/frontend/src/core/components/shared/RightRail.tsx b/frontend/src/core/components/shared/RightRail.tsx index 8001b17f73..95d4694a76 100644 --- a/frontend/src/core/components/shared/RightRail.tsx +++ b/frontend/src/core/components/shared/RightRail.tsx @@ -1,43 +1,58 @@ -import React, { useCallback, useMemo } from 'react'; -import { ActionIcon, Divider } from '@mantine/core'; -import '@app/components/shared/rightRail/RightRail.css'; -import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; -import { useRightRail } from '@app/contexts/RightRailContext'; -import { useFileState, useFileSelection, useFileActions } from '@app/contexts/FileContext'; -import { isStirlingFile } from '@app/types/fileContext'; -import { useNavigationState } from '@app/contexts/NavigationContext'; -import { useTranslation } from 'react-i18next'; -import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology'; -import { useFileActionIcons } from '@app/hooks/useFileActionIcons'; +import React, { useCallback, useMemo } from "react"; +import { ActionIcon, Divider } from "@mantine/core"; +import "@app/components/shared/rightRail/RightRail.css"; +import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext"; +import { useRightRail } from "@app/contexts/RightRailContext"; +import { + useFileState, + useFileSelection, + useFileActions, +} from "@app/contexts/FileContext"; +import { isStirlingFile } from "@app/types/fileContext"; +import { useNavigationState } from "@app/contexts/NavigationContext"; +import { useTranslation } from "react-i18next"; +import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology"; +import { useFileActionIcons } from "@app/hooks/useFileActionIcons"; -import LanguageSelector from '@app/components/shared/LanguageSelector'; -import { useRainbowThemeContext } from '@app/components/shared/RainbowThemeProvider'; -import { Tooltip } from '@app/components/shared/Tooltip'; -import { ViewerContext } from '@app/contexts/ViewerContext'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { RightRailFooterExtensions } from '@app/components/rightRail/RightRailFooterExtensions'; -import DarkModeIcon from '@mui/icons-material/DarkMode'; -import LightModeIcon from '@mui/icons-material/LightMode'; +import LanguageSelector from "@app/components/shared/LanguageSelector"; +import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider"; +import { Tooltip } from "@app/components/shared/Tooltip"; +import { ViewerContext } from "@app/contexts/ViewerContext"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { RightRailFooterExtensions } from "@app/components/rightRail/RightRailFooterExtensions"; +import DarkModeIcon from "@mui/icons-material/DarkMode"; +import LightModeIcon from "@mui/icons-material/LightMode"; -import { useSidebarContext } from '@app/contexts/SidebarContext'; -import { RightRailButtonConfig, RightRailRenderContext, RightRailSection } from '@app/types/rightRail'; -import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide'; -import { downloadFile } from '@app/services/downloadService'; +import { useSidebarContext } from "@app/contexts/SidebarContext"; +import { + RightRailButtonConfig, + RightRailRenderContext, + RightRailSection, +} from "@app/types/rightRail"; +import { useRightRailTooltipSide } from "@app/hooks/useRightRailTooltipSide"; +import { downloadFile } from "@app/services/downloadService"; -const SECTION_ORDER: RightRailSection[] = ['top', 'middle', 'bottom']; +const SECTION_ORDER: RightRailSection[] = ["top", "middle", "bottom"]; function renderWithTooltip( node: React.ReactNode, tooltip: React.ReactNode | undefined, - position: 'left' | 'right', - offset: number + position: "left" | "right", + offset: number, ) { if (!tooltip) return node; - const portalTarget = typeof document !== 'undefined' ? document.body : undefined; + const portalTarget = + typeof document !== "undefined" ? document.body : undefined; return ( - +
{node}
); @@ -45,7 +60,8 @@ function renderWithTooltip( export default function RightRail() { const { sidebarRefs } = useSidebarContext(); - const { position: tooltipPosition, offset: tooltipOffset } = useRightRailTooltipSide(sidebarRefs); + const { position: tooltipPosition, offset: tooltipOffset } = + useRightRailTooltipSide(sidebarRefs); const { t } = useTranslation(); const terminology = useFileActionTerminology(); const icons = useFileActionIcons(); @@ -53,8 +69,10 @@ export default function RightRail() { const { toggleTheme, themeMode } = useRainbowThemeContext(); const { buttons, actions, allButtonsDisabled } = useRightRail(); - const { pageEditorFunctions, toolPanelMode, leftPanelView } = useToolWorkflow(); - const disableForFullscreen = toolPanelMode === 'fullscreen' && leftPanelView === 'toolPicker'; + const { pageEditorFunctions, toolPanelMode, leftPanelView } = + useToolWorkflow(); + const disableForFullscreen = + toolPanelMode === "fullscreen" && leftPanelView === "toolPicker"; const { workbench: currentView } = useNavigationState(); @@ -63,33 +81,36 @@ export default function RightRail() { const { actions: fileActions } = useFileActions(); const activeFiles = selectors.getFiles(); const pageEditorTotalPages = pageEditorFunctions?.totalPages ?? 0; - const pageEditorSelectedCount = pageEditorFunctions?.selectedPageIds?.length ?? 0; + const pageEditorSelectedCount = + pageEditorFunctions?.selectedPageIds?.length ?? 0; const totalItems = useMemo(() => { - if (currentView === 'pageEditor') return pageEditorTotalPages; + if (currentView === "pageEditor") return pageEditorTotalPages; return activeFiles.length; }, [currentView, pageEditorTotalPages, activeFiles.length]); const selectedCount = useMemo(() => { - if (currentView === 'pageEditor') { + if (currentView === "pageEditor") { return pageEditorSelectedCount; } return selectedFileIds.length; }, [currentView, pageEditorSelectedCount, selectedFileIds.length]); const sectionsWithButtons = useMemo(() => { - return SECTION_ORDER - .map(section => { - const sectionButtons = buttons.filter(btn => (btn.section ?? 'top') === section && (btn.visible ?? true)); - return { section, buttons: sectionButtons }; - }) - .filter(entry => entry.buttons.length > 0); + return SECTION_ORDER.map((section) => { + const sectionButtons = buttons.filter( + (btn) => (btn.section ?? "top") === section && (btn.visible ?? true), + ); + return { section, buttons: sectionButtons }; + }).filter((entry) => entry.buttons.length > 0); }, [buttons]); const renderButton = useCallback( (btn: RightRailButtonConfig) => { const action = actions[btn.id]; - const disabled = Boolean(btn.disabled || allButtonsDisabled || disableForFullscreen); + const disabled = Boolean( + btn.disabled || allButtonsDisabled || disableForFullscreen, + ); const isActive = Boolean(btn.active); const triggerAction = () => { @@ -111,40 +132,57 @@ export default function RightRail() { if (!btn.icon) return null; const ariaLabel = - btn.ariaLabel || (typeof btn.tooltip === 'string' ? (btn.tooltip as string) : undefined); - const className = ['right-rail-icon', btn.className].filter(Boolean).join(' '); + btn.ariaLabel || + (typeof btn.tooltip === "string" ? (btn.tooltip as string) : undefined); + const className = ["right-rail-icon", btn.className] + .filter(Boolean) + .join(" "); const buttonNode = ( {btn.icon} ); - return renderWithTooltip(buttonNode, btn.tooltip, tooltipPosition, tooltipOffset); + return renderWithTooltip( + buttonNode, + btn.tooltip, + tooltipPosition, + tooltipOffset, + ); }, - [actions, allButtonsDisabled, disableForFullscreen, tooltipPosition, tooltipOffset] + [ + actions, + allButtonsDisabled, + disableForFullscreen, + tooltipPosition, + tooltipOffset, + ], ); const handleExportAll = useCallback( async (forceNewFile = false) => { - if (currentView === 'viewer') { + if (currentView === "viewer") { const buffer = await viewerContext?.exportActions?.saveAsCopy?.(); if (!buffer) return; - const fileToExport = selectedFiles.length > 0 ? selectedFiles[0] : activeFiles[0]; + const fileToExport = + selectedFiles.length > 0 ? selectedFiles[0] : activeFiles[0]; if (!fileToExport) return; - const stub = isStirlingFile(fileToExport) ? selectors.getStirlingFileStub(fileToExport.fileId) : undefined; + const stub = isStirlingFile(fileToExport) + ? selectors.getStirlingFileStub(fileToExport.fileId) + : undefined; try { const result = await downloadFile({ - data: new Blob([buffer], { type: 'application/pdf' }), + data: new Blob([buffer], { type: "application/pdf" }), filename: fileToExport.name, localPath: forceNewFile ? undefined : stub?.localFilePath, }); @@ -155,21 +193,24 @@ export default function RightRail() { }); } } catch (error) { - console.error('[RightRail] Failed to export viewer file:', error); + console.error("[RightRail] Failed to export viewer file:", error); } return; } - if (currentView === 'pageEditor') { + if (currentView === "pageEditor") { pageEditorFunctions?.onExportAll?.(); return; } - const filesToExport = selectedFiles.length > 0 ? selectedFiles : activeFiles; + const filesToExport = + selectedFiles.length > 0 ? selectedFiles : activeFiles; if (filesToExport.length > 0) { for (const file of filesToExport) { - const stub = isStirlingFile(file) ? selectors.getStirlingFileStub(file.fileId) : undefined; + const stub = isStirlingFile(file) + ? selectors.getStirlingFileStub(file.fileId) + : undefined; try { const result = await downloadFile({ data: file, @@ -184,7 +225,11 @@ export default function RightRail() { }); } } catch (error) { - console.error('[RightRail] Failed to export file:', file.name, error); + console.error( + "[RightRail] Failed to export file:", + file.name, + error, + ); } } } @@ -197,14 +242,14 @@ export default function RightRail() { viewerContext, selectors, fileActions, - ] + ], ); const downloadTooltip = useMemo(() => { - if (currentView === 'pageEditor') { - return t('rightRail.exportAll', 'Export PDF'); + if (currentView === "pageEditor") { + return t("rightRail.exportAll", "Export PDF"); } - if (currentView === 'viewer') { + if (currentView === "viewer") { return terminology.download; } if (selectedCount > 0) { @@ -214,7 +259,11 @@ export default function RightRail() { }, [currentView, selectedCount, t, terminology]); return ( -
+
{sectionsWithButtons.map(({ section, buttons: sectionButtons }) => ( @@ -236,7 +285,15 @@ export default function RightRail() { ))} -
+
{renderWithTooltip( - {themeMode === 'dark' ? ( - + {themeMode === "dark" ? ( + ) : ( - + )} , - t('rightRail.toggleTheme', 'Toggle Theme'), + t("rightRail.toggleTheme", "Toggle Theme"), tooltipPosition, - tooltipOffset + tooltipOffset, )} {renderWithTooltip( @@ -270,14 +327,19 @@ export default function RightRail() { onClick={() => handleExportAll()} disabled={ disableForFullscreen || - (currentView !== 'viewer' && (totalItems === 0 || allButtonsDisabled)) + (currentView !== "viewer" && + (totalItems === 0 || allButtonsDisabled)) } > - + , downloadTooltip, tooltipPosition, - tooltipOffset + tooltipOffset, )} {icons.saveAsIconName && renderWithTooltip( @@ -288,14 +350,19 @@ export default function RightRail() { onClick={() => handleExportAll(true)} disabled={ disableForFullscreen || - (currentView !== 'viewer' && (totalItems === 0 || allButtonsDisabled)) + (currentView !== "viewer" && + (totalItems === 0 || allButtonsDisabled)) } > - + , - t('rightRail.saveAs', 'Save As'), + t("rightRail.saveAs", "Save As"), tooltipPosition, - tooltipOffset + tooltipOffset, )}
diff --git a/frontend/src/core/components/shared/ShareFileModal.tsx b/frontend/src/core/components/shared/ShareFileModal.tsx index 3cdcf99245..2f205326e3 100644 --- a/frontend/src/core/components/shared/ShareFileModal.tsx +++ b/frontend/src/core/components/shared/ShareFileModal.tsx @@ -1,19 +1,29 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Modal, Stack, Text, Button, Group, Alert, TextInput, Paper, Select } from '@mantine/core'; -import LinkIcon from '@mui/icons-material/Link'; -import ContentCopyRoundedIcon from '@mui/icons-material/ContentCopyRounded'; -import { useTranslation } from 'react-i18next'; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { + Modal, + Stack, + Text, + Button, + Group, + Alert, + TextInput, + Paper, + Select, +} from "@mantine/core"; +import LinkIcon from "@mui/icons-material/Link"; +import ContentCopyRoundedIcon from "@mui/icons-material/ContentCopyRounded"; +import { useTranslation } from "react-i18next"; -import apiClient from '@app/services/apiClient'; -import { absoluteWithBasePath } from '@app/constants/app'; -import { alert } from '@app/components/toast'; -import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import type { StirlingFileStub } from '@app/types/fileContext'; -import { uploadHistoryChain } from '@app/services/serverStorageUpload'; -import { fileStorage } from '@app/services/fileStorage'; -import { useFileActions } from '@app/contexts/FileContext'; -import type { FileId } from '@app/types/file'; +import apiClient from "@app/services/apiClient"; +import { absoluteWithBasePath } from "@app/constants/app"; +import { alert } from "@app/components/toast"; +import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import { uploadHistoryChain } from "@app/services/serverStorageUpload"; +import { fileStorage } from "@app/services/fileStorage"; +import { useFileActions } from "@app/contexts/FileContext"; +import type { FileId } from "@app/types/file"; interface ShareFileModalProps { opened: boolean; @@ -35,7 +45,9 @@ const ShareFileModal: React.FC = ({ const [isWorking, setIsWorking] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const [shareToken, setShareToken] = useState(null); - const [shareRole, setShareRole] = useState<'editor' | 'commenter' | 'viewer'>('editor'); + const [shareRole, setShareRole] = useState<"editor" | "commenter" | "viewer">( + "editor", + ); useEffect(() => { if (!opened) { @@ -47,18 +59,20 @@ const ShareFileModal: React.FC = ({ useEffect(() => { if (opened) { - setShareRole('editor'); + setShareRole("editor"); } }, [opened]); const shareUrl = useMemo(() => { - if (!shareToken) return ''; - const frontendUrl = (config?.frontendUrl || '').trim(); + if (!shareToken) return ""; + const frontendUrl = (config?.frontendUrl || "").trim(); if (frontendUrl) { try { const parsed = new URL(frontendUrl); - if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { - const normalized = frontendUrl.endsWith('/') ? frontendUrl.slice(0, -1) : frontendUrl; + if (parsed.protocol === "http:" || parsed.protocol === "https:") { + const normalized = frontendUrl.endsWith("/") + ? frontendUrl.slice(0, -1) + : frontendUrl; return `${normalized}/share/${shareToken}`; } } catch { @@ -68,18 +82,24 @@ const ShareFileModal: React.FC = ({ return absoluteWithBasePath(`/share/${shareToken}`); }, [config?.frontendUrl, shareToken]); - const createShareLink = useCallback(async (storedFileId: number) => { - const response = await apiClient.post(`/api/v1/storage/files/${storedFileId}/shares/links`, { - accessRole: shareRole, - }); - return response.data as { token?: string }; - }, [shareRole]); + const createShareLink = useCallback( + async (storedFileId: number) => { + const response = await apiClient.post( + `/api/v1/storage/files/${storedFileId}/shares/links`, + { + accessRole: shareRole, + }, + ); + return response.data as { token?: string }; + }, + [shareRole], + ); const handleGenerateLink = useCallback(async () => { if (!shareLinksEnabled) { alert({ - alertType: 'warning', - title: t('storageShare.linksDisabled', 'Share links are disabled.'), + alertType: "warning", + title: t("storageShare.linksDisabled", "Share links are disabled."), expandable: false, durationMs: 2500, }); @@ -101,10 +121,11 @@ const ShareFileModal: React.FC = ({ if (!isUpToDate) { const originalFileId = (file.originalFileId || file.id) as FileId; const remoteId = file.remoteStorageId; - const { remoteId: newStoredId, updatedAt, chain } = await uploadHistoryChain( - originalFileId, - remoteId - ); + const { + remoteId: newStoredId, + updatedAt, + chain, + } = await uploadHistoryChain(originalFileId, remoteId); storedId = newStoredId; for (const stub of chain) { @@ -124,28 +145,33 @@ const ShareFileModal: React.FC = ({ } if (!storedId) { - throw new Error('Missing stored file ID for sharing.'); + throw new Error("Missing stored file ID for sharing."); } const shareResponse = await createShareLink(storedId); setShareToken(shareResponse.token ?? null); alert({ - alertType: 'success', - title: t('storageShare.generated', 'Share link generated'), + alertType: "success", + title: t("storageShare.generated", "Share link generated"), expandable: false, durationMs: 3000, }); if (storedId) { actions.updateStirlingFileStub(file.id, { remoteHasShareLinks: true }); - await fileStorage.updateFileMetadata(file.id, { remoteHasShareLinks: true }); + await fileStorage.updateFileMetadata(file.id, { + remoteHasShareLinks: true, + }); } if (onUploaded) { await onUploaded(); } } catch (error: any) { - console.error('Failed to generate share link:', error); + console.error("Failed to generate share link:", error); setErrorMessage( - t('storageShare.failure', 'Unable to generate a share link. Please try again.') + t( + "storageShare.failure", + "Unable to generate a share link. Please try again.", + ), ); } finally { setIsWorking(false); @@ -157,16 +183,16 @@ const ShareFileModal: React.FC = ({ try { await navigator.clipboard.writeText(shareUrl); alert({ - alertType: 'success', - title: t('storageShare.copied', 'Link copied to clipboard'), + alertType: "success", + title: t("storageShare.copied", "Link copied to clipboard"), expandable: false, durationMs: 2000, }); } catch (error) { - console.error('Failed to copy share link:', error); + console.error("Failed to copy share link:", error); alert({ - alertType: 'warning', - title: t('storageShare.copyFailed', 'Copy failed'), + alertType: "warning", + title: t("storageShare.copyFailed", "Copy failed"), expandable: false, durationMs: 2500, }); @@ -178,7 +204,7 @@ const ShareFileModal: React.FC = ({ opened={opened} onClose={onClose} centered - title={t('storageShare.title', 'Share File')} + title={t("storageShare.title", "Share File")} zIndex={Z_INDEX_OVER_FILE_MANAGER_MODAL} size="lg" overlayProps={{ blur: 6 }} @@ -188,24 +214,33 @@ const ShareFileModal: React.FC = ({ {t( - 'storageShare.description', - 'Create a share link for this file. Signed-in users with the link can access it.' + "storageShare.description", + "Create a share link for this file. Signed-in users with the link can access it.", )} - {t('storageShare.fileLabel', 'File')}: {file.name} + {t("storageShare.fileLabel", "File")}: {file.name} {errorMessage && ( - + {errorMessage} )} {!shareLinksEnabled && ( - - {t('storageShare.linksDisabledBody', 'Share links are disabled by your server settings.')} + + {t( + "storageShare.linksDisabledBody", + "Share links are disabled by your server settings.", + )} )} @@ -215,15 +250,17 @@ const ShareFileModal: React.FC = ({ } + leftSection={ + + } onClick={handleCopyLink} > - {t('storageShare.copy', 'Copy')} + {t("storageShare.copy", "Copy")} } /> @@ -234,22 +271,36 @@ const ShareFileModal: React.FC = ({ - {t('storageShare.linkAccessTitle', 'Share link access')} + {t("storageShare.linkAccessTitle", "Share link access")} setShareRole((value as typeof shareRole) || 'editor')} - comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_FILE_MANAGER_MODAL + 10 }} + onChange={(value) => + setShareRole((value as typeof shareRole) || "editor") + } + comboboxProps={{ + withinPortal: true, + zIndex: Z_INDEX_OVER_FILE_MANAGER_MODAL + 10, + }} data={[ - { value: 'editor', label: t('storageShare.roleEditor', 'Editor') }, - { value: 'commenter', label: t('storageShare.roleCommenter', 'Commenter') }, - { value: 'viewer', label: t('storageShare.roleViewer', 'Viewer') }, + { + value: "editor", + label: t("storageShare.roleEditor", "Editor"), + }, + { + value: "commenter", + label: t("storageShare.roleCommenter", "Commenter"), + }, + { + value: "viewer", + label: t("storageShare.roleViewer", "Viewer"), + }, ]} /> - {shareRole === 'commenter' && ( + {shareRole === "commenter" && ( - {t('storageShare.commenterHint', 'Commenting is coming soon.')} + {t( + "storageShare.commenterHint", + "Commenting is coming soon.", + )} )} @@ -454,7 +562,7 @@ const ShareManagementModal: React.FC = ({ onClick={() => createShareLink()} loading={isLoading} > - {t('storageShare.generate', 'Generate Link')} + {t("storageShare.generate", "Generate Link")} @@ -464,19 +572,22 @@ const ShareManagementModal: React.FC = ({ - {t('storageShare.sharedUsersTitle', 'Shared users')} + {t("storageShare.sharedUsersTitle", "Shared users")} { setShareUsername(event.currentTarget.value); setShowEmailWarning(false); }} onKeyDown={(event) => { - if (event.key === 'Enter') { + if (event.key === "Enter") { event.preventDefault(); void handleAddUser(); } @@ -486,18 +597,27 @@ const ShareManagementModal: React.FC = ({ /> {showEmailWarning && ( - + {t( - 'storageShare.emailWarningBody', - 'This looks like an email address. If this person is not already a Stirling PDF user, they will not be able to access the file.' + "storageShare.emailWarningBody", + "This looks like an email address. If this person is not already a Stirling PDF user, they will not be able to access the file.", )} @@ -506,13 +626,16 @@ const ShareManagementModal: React.FC = ({ onClick={() => setShowEmailWarning(false)} disabled={isLoading} > - {t('cancel', 'Cancel')} + {t("cancel", "Cancel")} @@ -520,32 +643,62 @@ const ShareManagementModal: React.FC = ({ )} {sharedUsers.length === 0 ? ( - {t('storageShare.noSharedUsers', 'No users have access yet.')} + {t( + "storageShare.noSharedUsers", + "No users have access yet.", + )} ) : ( {sharedUsers.map((user) => ( - + {user.username} - {user.accessRole === 'commenter' && ( + {user.accessRole === "commenter" && ( - {t('storageShare.commenterHint', 'Commenting is coming soon.')} + {t( + "storageShare.commenterHint", + "Commenting is coming soon.", + )} )} onChange(e.currentTarget.value)} + autoComplete={autoComplete} + className={styles.input} + disabled={disabled} + readOnly={readOnly} + aria-label={ariaLabel} + onFocus={onFocus} + style={{ + backgroundColor: "var(--input-bg)", + color: "var(--search-text-and-icon-color)", + paddingRight: shouldShowClearButton ? "40px" : "12px", + paddingLeft: icon ? "40px" : "12px", + }} + {...props} + /> + {shouldShowClearButton && ( + + )} +
+ ); + }, +); - return ( -
- {icon && ( - - {icon} - - )} - onChange(e.currentTarget.value)} - autoComplete={autoComplete} - className={styles.input} - disabled={disabled} - readOnly={readOnly} - aria-label={ariaLabel} - onFocus={onFocus} - style={{ - backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF', - color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382', - paddingRight: shouldShowClearButton ? '40px' : '12px', - paddingLeft: icon ? '40px' : '12px', - }} - {...props} - /> - {shouldShowClearButton && ( - - )} -
- ); -}); - -TextInput.displayName = 'TextInput'; +TextInput.displayName = "TextInput"; diff --git a/frontend/src/core/components/shared/ToolChain.tsx b/frontend/src/core/components/shared/ToolChain.tsx index c67d894273..31f05eef01 100644 --- a/frontend/src/core/components/shared/ToolChain.tsx +++ b/frontend/src/core/components/shared/ToolChain.tsx @@ -3,63 +3,66 @@ * Used across FileListItem, FileDetails, and FileThumbnail for consistent display */ -import React from 'react'; -import { Text, Tooltip, Badge, Group } from '@mantine/core'; -import { ToolOperation } from '@app/types/file'; -import { useTranslation } from 'react-i18next'; -import { ToolId } from '@app/types/toolId'; +import React from "react"; +import { Text, Tooltip, Badge, Group } from "@mantine/core"; +import { ToolOperation } from "@app/types/file"; +import { useTranslation } from "react-i18next"; +import { ToolId } from "@app/types/toolId"; interface ToolChainProps { toolChain: ToolOperation[]; maxWidth?: string; - displayStyle?: 'text' | 'badges' | 'compact'; - size?: 'xs' | 'sm' | 'md'; + displayStyle?: "text" | "badges" | "compact"; + size?: "xs" | "sm" | "md"; color?: string; } const ToolChain: React.FC = ({ toolChain, - maxWidth = '100%', - displayStyle = 'text', - size = 'xs', - color = 'var(--mantine-color-blue-7)' + maxWidth = "100%", + displayStyle = "text", + size = "xs", + color = "var(--mantine-color-blue-7)", }) => { const { t } = useTranslation(); if (!toolChain || toolChain.length === 0) return null; - const toolIds = toolChain.map(tool => tool.toolId); + const toolIds = toolChain.map((tool) => tool.toolId); const getToolName = (toolId: ToolId) => { return t(`home.${toolId}.title`, toolId); }; // Create full tool chain for tooltip - const fullChainDisplay = displayStyle === 'badges' ? ( - - {toolChain.map((tool, index) => ( - - - {getToolName(tool.toolId)} - - {index < toolChain.length - 1 && ( - → - )} - - ))} - - ) : ( - {toolIds.map(getToolName).join(' → ')} - ); + const fullChainDisplay = + displayStyle === "badges" ? ( + + {toolChain.map((tool, index) => ( + + + {getToolName(tool.toolId)} + + {index < toolChain.length - 1 && ( + + → + + )} + + ))} + + ) : ( + {toolIds.map(getToolName).join(" → ")} + ); // Create truncated display based on available space const getTruncatedDisplay = () => { if (toolIds.length <= 2) { // Show all tools if 2 or fewer - return { text: toolIds.map(getToolName).join(' → '), isTruncated: false }; + return { text: toolIds.map(getToolName).join(" → "), isTruncated: false }; } else { // Show first tool ... last tool for longer chains return { - text: `${getToolName(toolIds[0])} → +${toolIds.length-2} → ${getToolName(toolIds[toolIds.length - 1])}`, + text: `${getToolName(toolIds[0])} → +${toolIds.length - 2} → ${getToolName(toolIds[toolIds.length - 1])}`, isTruncated: true, }; } @@ -68,8 +71,11 @@ const ToolChain: React.FC = ({ const { text: truncatedText, isTruncated } = getTruncatedDisplay(); // Compact style for very small spaces - if (displayStyle === 'compact') { - const compactText = toolIds.length === 1 ? getToolName(toolIds[0]) : `${toolIds.length} tools`; + if (displayStyle === "compact") { + const compactText = + toolIds.length === 1 + ? getToolName(toolIds[0]) + : `${toolIds.length} tools`; const isCompactTruncated = toolIds.length > 1; const compactElement = ( @@ -78,11 +84,11 @@ const ToolChain: React.FC = ({ style={{ color, fontWeight: 500, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", maxWidth: `${maxWidth}`, - cursor: isCompactTruncated ? 'help' : 'default' + cursor: isCompactTruncated ? "help" : "default", }} > {compactText} @@ -93,15 +99,17 @@ const ToolChain: React.FC = ({ {compactElement} - ) : compactElement; + ) : ( + compactElement + ); } // Badge style for file details - if (displayStyle === 'badges') { + if (displayStyle === "badges") { const isBadgesTruncated = toolChain.length > 3; const badgesElement = ( -
+
{toolChain.slice(0, 3).map((tool, index) => ( @@ -109,13 +117,17 @@ const ToolChain: React.FC = ({ {getToolName(tool.toolId)} {index < Math.min(toolChain.length - 1, 2) && ( - → + + → + )} ))} {toolChain.length > 3 && ( <> - ... + + ... + {getToolName(toolChain[toolChain.length - 1].toolId)} @@ -126,10 +138,12 @@ const ToolChain: React.FC = ({ ); return isBadgesTruncated ? ( - + {badgesElement} - ) : badgesElement; + ) : ( + badgesElement + ); } // Text style (default) for file list items @@ -139,11 +153,11 @@ const ToolChain: React.FC = ({ style={{ color, fontWeight: 500, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", maxWidth: `${maxWidth}`, - cursor: isTruncated ? 'help' : 'default' + cursor: isTruncated ? "help" : "default", }} > {truncatedText} @@ -154,7 +168,9 @@ const ToolChain: React.FC = ({ {textElement} - ) : textElement; + ) : ( + textElement + ); }; export default ToolChain; diff --git a/frontend/src/core/components/shared/ToolIcon.tsx b/frontend/src/core/components/shared/ToolIcon.tsx index 75ab249ba7..d0a1c82b9b 100644 --- a/frontend/src/core/components/shared/ToolIcon.tsx +++ b/frontend/src/core/components/shared/ToolIcon.tsx @@ -15,7 +15,7 @@ export const ToolIcon: React.FC = ({ icon, opacity = 1, color = "var(--tools-text-and-icon-color)", - marginRight = "0.5rem" + marginRight = "0.5rem", }) => { return (
= ({ marginRight, transform: "scale(0.8)", transformOrigin: "center", - opacity + opacity, }} > {icon} diff --git a/frontend/src/core/components/shared/Tooltip.tsx b/frontend/src/core/components/shared/Tooltip.tsx index 2580b3530a..b6dadb9f75 100644 --- a/frontend/src/core/components/shared/Tooltip.tsx +++ b/frontend/src/core/components/shared/Tooltip.tsx @@ -1,18 +1,24 @@ -import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react'; -import { createPortal } from 'react-dom'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { addEventListenerWithCleanup } from '@app/utils/genericUtils'; -import { useTooltipPosition } from '@app/hooks/useTooltipPosition'; -import { TooltipTip } from '@app/types/tips'; -import { TooltipContent } from '@app/components/shared/tooltip/TooltipContent'; -import { useSidebarContext } from '@app/contexts/SidebarContext'; -import { useLogoAssets } from '@app/hooks/useLogoAssets'; -import styles from '@app/components/shared/tooltip/Tooltip.module.css'; -import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex'; +import React, { + useState, + useRef, + useEffect, + useMemo, + useCallback, +} from "react"; +import { createPortal } from "react-dom"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { addEventListenerWithCleanup } from "@app/utils/genericUtils"; +import { useTooltipPosition } from "@app/hooks/useTooltipPosition"; +import { TooltipTip } from "@app/types/tips"; +import { TooltipContent } from "@app/components/shared/tooltip/TooltipContent"; +import { useSidebarContext } from "@app/contexts/SidebarContext"; +import { useLogoAssets } from "@app/hooks/useLogoAssets"; +import styles from "@app/components/shared/tooltip/Tooltip.module.css"; +import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from "@app/styles/zIndex"; export interface TooltipProps { sidebarTooltip?: boolean; - position?: 'right' | 'left' | 'top' | 'bottom'; + position?: "right" | "left" | "top" | "bottom"; content?: React.ReactNode; tips?: TooltipTip[]; children: React.ReactElement; @@ -74,7 +80,7 @@ export const Tooltip: React.FC = ({ // Runtime guard: some browsers may surface non-Node EventTargets for relatedTarget/target const isDomNode = (value: unknown): value is Node => - typeof Node !== 'undefined' && value instanceof Node; + typeof Node !== "undefined" && value instanceof Node; const clearTimers = useCallback(() => { if (openTimeoutRef.current) { @@ -92,15 +98,17 @@ export const Tooltip: React.FC = ({ const open = (isControlled ? !!controlledOpen : internalOpen) && !disabled; const allowAutoClose = !manualCloseOnly; - const resolvedPosition: NonNullable = useMemo(() => { - const htmlDir = typeof document !== 'undefined' ? document.documentElement.dir : 'ltr'; - const isRTL = htmlDir === 'rtl'; - const base = position ?? 'right'; - if (!isRTL) return base as NonNullable; - if (base === 'left') return 'right'; - if (base === 'right') return 'left'; - return base as NonNullable; - }, [position]); + const resolvedPosition: NonNullable = + useMemo(() => { + const htmlDir = + typeof document !== "undefined" ? document.documentElement.dir : "ltr"; + const isRTL = htmlDir === "rtl"; + const base = position ?? "right"; + if (!isRTL) return base as NonNullable; + if (base === "left") return "right"; + if (base === "right") return "left"; + return base as NonNullable; + }, [position]); const setOpen = useCallback( (newOpen: boolean) => { @@ -109,7 +117,7 @@ export const Tooltip: React.FC = ({ else setInternalOpen(newOpen); if (!newOpen) setIsPinned(false); }, - [isControlled, onOpenChange, open] + [isControlled, onOpenChange, open], ); const { coords, positionReady } = useTooltipPosition({ @@ -129,8 +137,12 @@ export const Tooltip: React.FC = ({ const tEl = tooltipRef.current; const trg = triggerRef.current; const target = e.target as unknown; - const insideTooltip = Boolean(tEl && isDomNode(target) && tEl.contains(target)); - const insideTrigger = Boolean(trg && isDomNode(target) && trg.contains(target)); + const insideTooltip = Boolean( + tEl && isDomNode(target) && tEl.contains(target), + ); + const insideTrigger = Boolean( + trg && isDomNode(target) && trg.contains(target), + ); // If pinned: only close when clicking outside BOTH tooltip & trigger if (isPinned) { @@ -142,17 +154,26 @@ export const Tooltip: React.FC = ({ } // Not pinned and configured to close on outside - if (allowAutoClose && closeOnOutside && !insideTooltip && !insideTrigger) { + if ( + allowAutoClose && + closeOnOutside && + !insideTooltip && + !insideTrigger + ) { setOpen(false); } }, - [isPinned, closeOnOutside, setOpen, allowAutoClose] + [isPinned, closeOnOutside, setOpen, allowAutoClose], ); useEffect(() => { // Attach global click when open (so hover tooltips can also close on outside if desired) if (open || isPinned) { - return addEventListenerWithCleanup(document, 'click', handleDocumentClick as EventListener); + return addEventListenerWithCleanup( + document, + "click", + handleDocumentClick as EventListener, + ); } }, [open, isPinned, handleDocumentClick]); @@ -160,11 +181,11 @@ export const Tooltip: React.FC = ({ const arrowClass = useMemo(() => { if (sidebarTooltip) return null; - const map: Record, string> = { - top: 'tooltip-arrow-bottom', - bottom: 'tooltip-arrow-top', - left: 'tooltip-arrow-left', - right: 'tooltip-arrow-right', + const map: Record, string> = { + top: "tooltip-arrow-bottom", + bottom: "tooltip-arrow-top", + left: "tooltip-arrow-left", + right: "tooltip-arrow-right", }; return map[resolvedPosition] || map.right; }, [resolvedPosition, sidebarTooltip]); @@ -172,16 +193,23 @@ export const Tooltip: React.FC = ({ const getArrowStyleClass = useCallback( (key: string) => styles[key as keyof typeof styles] || - styles[key.replace(/-([a-z])/g, (_, l) => l.toUpperCase()) as keyof typeof styles] || - '', - [] + styles[ + key.replace(/-([a-z])/g, (_, l) => + l.toUpperCase(), + ) as keyof typeof styles + ] || + "", + [], ); // === Trigger handlers === const openWithDelay = useCallback(() => { clearTimers(); if (disabled) return; - openTimeoutRef.current = setTimeout(() => setOpen(true), Math.max(0, delay || 0)); + openTimeoutRef.current = setTimeout( + () => setOpen(true), + Math.max(0, delay || 0), + ); }, [clearTimers, setOpen, delay, disabled]); const handlePointerEnter = useCallback( @@ -189,7 +217,7 @@ export const Tooltip: React.FC = ({ if (!isPinned && !disabled) openWithDelay(); (children.props as any)?.onPointerEnter?.(e); }, - [isPinned, openWithDelay, children.props, disabled] + [isPinned, openWithDelay, children.props, disabled], ); const handlePointerLeave = useCallback( @@ -197,8 +225,11 @@ export const Tooltip: React.FC = ({ const related = e.relatedTarget as Node | null; // Moving into the tooltip → keep open - if (isDomNode(related) && tooltipRef.current && tooltipRef.current.contains(related)) { - + if ( + isDomNode(related) && + tooltipRef.current && + tooltipRef.current.contains(related) + ) { (children.props as any)?.onPointerLeave?.(e); return; } @@ -213,7 +244,7 @@ export const Tooltip: React.FC = ({ if (allowAutoClose && !isPinned) setOpen(false); (children.props as any)?.onPointerLeave?.(e); }, - [clearTimers, isPinned, setOpen, children.props, allowAutoClose] + [clearTimers, isPinned, setOpen, children.props, allowAutoClose], ); const handleMouseDown = useCallback( @@ -221,7 +252,7 @@ export const Tooltip: React.FC = ({ clickPendingRef.current = true; (children.props as any)?.onMouseDown?.(e); }, - [children.props] + [children.props], ); const handleMouseUp = useCallback( @@ -230,7 +261,7 @@ export const Tooltip: React.FC = ({ queueMicrotask(() => (clickPendingRef.current = false)); (children.props as any)?.onMouseUp?.(e); }, - [children.props] + [children.props], ); const handleClick = useCallback( @@ -247,7 +278,7 @@ export const Tooltip: React.FC = ({ clickPendingRef.current = false; (children.props as any)?.onClick?.(e); }, - [clearTimers, pinOnClick, open, setOpen, children.props] + [clearTimers, pinOnClick, open, setOpen, children.props], ); // Keyboard / focus accessibility @@ -256,13 +287,17 @@ export const Tooltip: React.FC = ({ if (!isPinned && !disabled && openOnFocus) openWithDelay(); (children.props as any)?.onFocus?.(e); }, - [isPinned, openWithDelay, children.props, disabled, openOnFocus] + [isPinned, openWithDelay, children.props, disabled, openOnFocus], ); const handleBlur = useCallback( (e: React.FocusEvent) => { const related = e.relatedTarget as Node | null; - if (isDomNode(related) && tooltipRef.current && tooltipRef.current.contains(related)) { + if ( + isDomNode(related) && + tooltipRef.current && + tooltipRef.current.contains(related) + ) { (children.props as any)?.onBlur?.(e); return; } @@ -270,13 +305,16 @@ export const Tooltip: React.FC = ({ if (allowAutoClose && !isPinned) setOpen(false); (children.props as any)?.onBlur?.(e); }, - [isPinned, setOpen, children.props, allowAutoClose, clearTimers] + [isPinned, setOpen, children.props, allowAutoClose, clearTimers], ); - const handleKeyDown = useCallback((e: React.KeyboardEvent) => { - if (manualCloseOnly) return; - if (e.key === 'Escape') setOpen(false); - }, [setOpen, manualCloseOnly]); + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (manualCloseOnly) return; + if (e.key === "Escape") setOpen(false); + }, + [setOpen, manualCloseOnly], + ); // Keep open while pointer is over the tooltip; close when leaving it (if not pinned) const handleTooltipPointerEnter = useCallback(() => { @@ -286,10 +324,15 @@ export const Tooltip: React.FC = ({ const handleTooltipPointerLeave = useCallback( (e: React.PointerEvent) => { const related = e.relatedTarget as Node | null; - if (isDomNode(related) && triggerRef.current && triggerRef.current.contains(related)) return; + if ( + isDomNode(related) && + triggerRef.current && + triggerRef.current.contains(related) + ) + return; if (allowAutoClose && !isPinned) setOpen(false); }, - [isPinned, setOpen, allowAutoClose] + [isPinned, setOpen, allowAutoClose], ); // Enhance child with handlers and ref @@ -297,10 +340,11 @@ export const Tooltip: React.FC = ({ ref: (node: HTMLElement | null) => { triggerRef.current = node || null; const originalRef = (children as any).ref; - if (typeof originalRef === 'function') originalRef(node); - else if (originalRef && typeof originalRef === 'object') (originalRef as any).current = node; + if (typeof originalRef === "function") originalRef(node); + else if (originalRef && typeof originalRef === "object") + (originalRef as any).current = node; }, - 'aria-describedby': open ? tooltipIdRef.current : undefined, + "aria-describedby": open ? tooltipIdRef.current : undefined, onPointerEnter: handlePointerEnter, onPointerLeave: handlePointerLeave, onMouseDown: handleMouseDown, @@ -323,23 +367,35 @@ export const Tooltip: React.FC = ({ onPointerEnter={handleTooltipPointerEnter} onPointerLeave={handleTooltipPointerLeave} style={{ - position: 'fixed', + position: "fixed", top: coords.top, left: coords.left, - width: maxWidth !== undefined ? maxWidth : (sidebarTooltip ? '25rem' as const : undefined), + width: + maxWidth !== undefined + ? maxWidth + : sidebarTooltip + ? ("25rem" as const) + : undefined, minWidth, zIndex: Z_INDEX_OVER_FULLSCREEN_SURFACE, - visibility: positionReady ? 'visible' : 'hidden', + visibility: positionReady ? "visible" : "hidden", opacity: positionReady ? 1 : 0, - color: 'var(--text-primary)', + color: "var(--text-primary)", ...containerStyle, }} - className={`${styles['tooltip-container']} ${isPinned ? styles.pinned : ''}`} - onClick={pinOnClick ? (e) => { e.stopPropagation(); setIsPinned(true); } : undefined} + className={`${styles["tooltip-container"]} ${isPinned ? styles.pinned : ""}`} + onClick={ + pinOnClick + ? (e) => { + e.stopPropagation(); + setIsPinned(true); + } + : undefined + } > {shouldShowCloseButton && ( - - - ))} - - - )} + {updateSummary.migration_guides && + updateSummary.migration_guides.length > 0 && ( + <> + + + + {t("update.migrationGuides", "Migration Guides")} + + {updateSummary.migration_guides.map((guide, idx) => ( + + + + + {t("update.version", "Version")} {guide.version} + + + {guide.notes} + + + + + + ))} + + + )} {/* Version details */} @@ -254,18 +298,26 @@ const UpdateModal: React.FC = ({ - {t('update.loadingDetailedInfo', 'Loading detailed information...')} + {t( + "update.loadingDetailedInfo", + "Loading detailed information...", + )}
- ) : fullUpdateInfo && fullUpdateInfo.new_versions && fullUpdateInfo.new_versions.length > 0 ? ( + ) : fullUpdateInfo && + fullUpdateInfo.new_versions && + fullUpdateInfo.new_versions.length > 0 ? ( - {t('update.availableUpdates', 'Available Updates')} + {t("update.availableUpdates", "Available Updates")} - {fullUpdateInfo.new_versions.length} {fullUpdateInfo.new_versions.length === 1 ? 'version' : 'versions'} + {fullUpdateInfo.new_versions.length}{" "} + {fullUpdateInfo.new_versions.length === 1 + ? "version" + : "versions"} @@ -275,9 +327,9 @@ const UpdateModal: React.FC = ({ = ({ align="center" p="md" style={{ - cursor: 'pointer', - background: isExpanded ? 'var(--mantine-color-gray-0)' : 'transparent', - transition: 'background 0.15s ease', + cursor: "pointer", + background: isExpanded + ? "var(--mantine-color-gray-0)" + : "transparent", + transition: "background 0.15s ease", }} onClick={() => toggleVersion(index)} > - {t('update.version', 'Version')} + {t("update.version", "Version")} {version.version} - + {getPriorityLabel(version.priority)} @@ -312,26 +369,48 @@ const UpdateModal: React.FC = ({ variant="light" size="xs" onClick={(e) => e.stopPropagation()} - rightSection={} + rightSection={ + + } > - {t('update.releaseNotes', 'Release Notes')} + {t("update.releaseNotes", "Release Notes")} {isExpanded ? ( - + ) : ( - + )} - + {version.announcement.title} - + {version.announcement.message} @@ -339,33 +418,58 @@ const UpdateModal: React.FC = ({ {version.compatibility.breaking_changes && ( - - + + - {t('update.breakingChanges', 'Breaking Changes')} + {t( + "update.breakingChanges", + "Breaking Changes", + )} {version.compatibility.breaking_description || - t('update.breakingChangesDefault', 'This version contains breaking changes.')} + t( + "update.breakingChangesDefault", + "This version contains breaking changes.", + )} {version.compatibility.migration_guide_url && ( )} @@ -384,7 +488,7 @@ const UpdateModal: React.FC = ({ {downloadUrl && ( )} diff --git a/frontend/src/core/components/shared/UploadToServerModal.tsx b/frontend/src/core/components/shared/UploadToServerModal.tsx index ecf424e60b..10cfc57dd8 100644 --- a/frontend/src/core/components/shared/UploadToServerModal.tsx +++ b/frontend/src/core/components/shared/UploadToServerModal.tsx @@ -1,15 +1,15 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { Modal, Stack, Text, Button, Group, Alert } from '@mantine/core'; -import CloudUploadIcon from '@mui/icons-material/CloudUpload'; -import { useTranslation } from 'react-i18next'; +import React, { useCallback, useEffect, useState } from "react"; +import { Modal, Stack, Text, Button, Group, Alert } from "@mantine/core"; +import CloudUploadIcon from "@mui/icons-material/CloudUpload"; +import { useTranslation } from "react-i18next"; -import { alert } from '@app/components/toast'; -import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; -import type { StirlingFileStub } from '@app/types/fileContext'; -import { uploadHistoryChain } from '@app/services/serverStorageUpload'; -import { fileStorage } from '@app/services/fileStorage'; -import { useFileActions } from '@app/contexts/FileContext'; -import type { FileId } from '@app/types/file'; +import { alert } from "@app/components/toast"; +import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; +import type { StirlingFileStub } from "@app/types/fileContext"; +import { uploadHistoryChain } from "@app/services/serverStorageUpload"; +import { fileStorage } from "@app/services/fileStorage"; +import { useFileActions } from "@app/contexts/FileContext"; +import type { FileId } from "@app/types/file"; interface UploadToServerModalProps { opened: boolean; @@ -43,10 +43,11 @@ const UploadToServerModal: React.FC = ({ try { const originalFileId = (file.originalFileId || file.id) as FileId; const remoteId = file.remoteStorageId; - const { remoteId: storedId, updatedAt, chain } = await uploadHistoryChain( - originalFileId, - remoteId - ); + const { + remoteId: storedId, + updatedAt, + chain, + } = await uploadHistoryChain(originalFileId, remoteId); for (const stub of chain) { actions.updateStirlingFileStub(stub.id, { @@ -62,8 +63,8 @@ const UploadToServerModal: React.FC = ({ } alert({ - alertType: 'success', - title: t('storageUpload.success', 'Uploaded to server'), + alertType: "success", + title: t("storageUpload.success", "Uploaded to server"), expandable: false, durationMs: 3000, }); @@ -72,9 +73,12 @@ const UploadToServerModal: React.FC = ({ } onClose(); } catch (error) { - console.error('Failed to upload file to server:', error); + console.error("Failed to upload file to server:", error); setErrorMessage( - t('storageUpload.failure', 'Upload failed. Please check your login and storage settings.') + t( + "storageUpload.failure", + "Upload failed. Please check your login and storage settings.", + ), ); } finally { setIsUploading(false); @@ -86,35 +90,38 @@ const UploadToServerModal: React.FC = ({ opened={opened} onClose={onClose} centered - title={t('storageUpload.title', 'Upload to Server')} + title={t("storageUpload.title", "Upload to Server")} zIndex={Z_INDEX_OVER_FILE_MANAGER_MODAL} > {t( - 'storageUpload.description', - 'This uploads the current file to server storage for your own access.' + "storageUpload.description", + "This uploads the current file to server storage for your own access.", )} - {t('storageUpload.fileLabel', 'File')}: {file.name} + {t("storageUpload.fileLabel", "File")}: {file.name} {t( - 'storageUpload.hint', - 'Public links and access modes are controlled by your server settings.' + "storageUpload.hint", + "Public links and access modes are controlled by your server settings.", )} {errorMessage && ( - + {errorMessage} )} diff --git a/frontend/src/core/components/shared/UserSelector.tsx b/frontend/src/core/components/shared/UserSelector.tsx index 22c99ec61d..956da0cff5 100644 --- a/frontend/src/core/components/shared/UserSelector.tsx +++ b/frontend/src/core/components/shared/UserSelector.tsx @@ -1,25 +1,31 @@ -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { MultiSelect, Loader, Text, Button, Stack } from '@mantine/core'; -import { useNavigate } from 'react-router-dom'; -import { alert } from '@app/components/toast'; -import { UserSummary } from '@app/types/signingSession'; -import apiClient from '@app/services/apiClient'; -import { useAuth } from '@app/auth/UseSession'; -import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex'; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { MultiSelect, Loader, Text, Button, Stack } from "@mantine/core"; +import { useNavigate } from "react-router-dom"; +import { alert } from "@app/components/toast"; +import { UserSummary } from "@app/types/signingSession"; +import apiClient from "@app/services/apiClient"; +import { useAuth } from "@app/auth/UseSession"; +import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex"; interface UserSelectorProps { value: number[]; onChange: (userIds: number[]) => void; placeholder?: string; - size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + size?: "xs" | "sm" | "md" | "lg" | "xl"; disabled?: boolean; } type SelectItem = { value: string; label: string }; type GroupedData = { group: string; items: SelectItem[] }; -const UserSelector = ({ value, onChange, placeholder, size = 'sm', disabled = false }: UserSelectorProps) => { +const UserSelector = ({ + value, + onChange, + placeholder, + size = "sm", + disabled = false, +}: UserSelectorProps) => { const { t } = useTranslation(); const { user } = useAuth(); const navigate = useNavigate(); @@ -30,8 +36,8 @@ const UserSelector = ({ value, onChange, placeholder, size = 'sm', disabled = fa useEffect(() => { const fetchUsers = async () => { try { - const response = await apiClient.get('/api/v1/user/users'); - console.log('Users API response:', response.data); + const response = await apiClient.get("/api/v1/user/users"); + console.log("Users API response:", response.data); const fetchedUsers = response.data || []; // Process selectData inside useEffect - group by team @@ -41,16 +47,20 @@ const UserSelector = ({ value, onChange, placeholder, size = 'sm', disabled = fa fetchedUsers .filter((u: UserSummary) => u && u.userId && u.username) .filter((u: UserSummary) => u.userId !== currentUserId) // Exclude current user - .filter((u: UserSummary) => u.teamName?.toLowerCase() !== 'internal') // Exclude internal users + .filter((u: UserSummary) => u.teamName?.toLowerCase() !== "internal") // Exclude internal users .forEach((user: UserSummary) => { - const teamName = user.teamName || t('certSign.collab.userSelector.noTeam', 'No Team'); + const teamName = + user.teamName || + t("certSign.collab.userSelector.noTeam", "No Team"); if (!usersByTeam[teamName]) { usersByTeam[teamName] = []; } - const displayName = user.displayName || user.username || 'Unknown'; - const username = user.username || 'unknown'; + const displayName = user.displayName || user.username || "Unknown"; + const username = user.username || "unknown"; const label = - displayName !== username ? `${displayName} (@${username})` : displayName; + displayName !== username + ? `${displayName} (@${username})` + : displayName; usersByTeam[teamName].push({ value: String(user.userId), label, @@ -58,19 +68,24 @@ const UserSelector = ({ value, onChange, placeholder, size = 'sm', disabled = fa }); // Convert to Mantine's grouped format - const processed: GroupedData[] = Object.entries(usersByTeam).map(([teamName, items]) => ({ - group: teamName, - items: items.sort((a, b) => a.label.localeCompare(b.label)), - })); + const processed: GroupedData[] = Object.entries(usersByTeam).map( + ([teamName, items]) => ({ + group: teamName, + items: items.sort((a, b) => a.label.localeCompare(b.label)), + }), + ); - console.log('Processed selectData:', processed); + console.log("Processed selectData:", processed); setSelectData(processed); } catch (error) { - console.error('Failed to load users:', error); + console.error("Failed to load users:", error); alert({ - alertType: 'error', - title: t('common.error'), - body: t('certSign.collab.userSelector.loadError', 'Failed to load users'), + alertType: "error", + title: t("common.error"), + body: t( + "certSign.collab.userSelector.loadError", + "Failed to load users", + ), }); } finally { setLoading(false); @@ -83,8 +98,10 @@ const UserSelector = ({ value, onChange, placeholder, size = 'sm', disabled = fa // Process stringValue when value prop changes useEffect(() => { const safeValue = Array.isArray(value) ? value : []; - const result = safeValue.map((id) => (id != null ? id.toString() : '')).filter(Boolean); - console.log('stringValue for MultiSelect:', result); + const result = safeValue + .map((id) => (id != null ? id.toString() : "")) + .filter(Boolean); + console.log("stringValue for MultiSelect:", result); setStringValue(result); }, [value]); @@ -97,10 +114,14 @@ const UserSelector = ({ value, onChange, placeholder, size = 'sm', disabled = fa return ( - {t('certSign.collab.userSelector.noUsers', 'No other users found.')} + {t("certSign.collab.userSelector.noUsers", "No other users found.")} - ); @@ -116,13 +137,19 @@ const UserSelector = ({ value, onChange, placeholder, size = 'sm', disabled = fa .filter((id) => !isNaN(id)); onChange(parsedIds); }} - placeholder={placeholder || t('certSign.collab.userSelector.placeholder', 'Select users...')} + placeholder={ + placeholder || + t("certSign.collab.userSelector.placeholder", "Select users...") + } searchable clearable size={size} disabled={disabled} maxDropdownHeight={300} - comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_FILE_MANAGER_MODAL + 10 }} + comboboxProps={{ + withinPortal: true, + zIndex: Z_INDEX_OVER_FILE_MANAGER_MODAL + 10, + }} /> ); }; diff --git a/frontend/src/core/components/shared/Wordmark.tsx b/frontend/src/core/components/shared/Wordmark.tsx new file mode 100644 index 0000000000..62185420c9 --- /dev/null +++ b/frontend/src/core/components/shared/Wordmark.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { useMantineColorScheme } from "@mantine/core"; +import { useLogoAssets } from "@app/hooks/useLogoAssets"; + +interface WordmarkProps extends React.ImgHTMLAttributes { + alt?: string; + muted?: boolean; +} + +export function Wordmark({ alt = "", muted = false, ...props }: WordmarkProps) { + const { colorScheme } = useMantineColorScheme(); + const isDark = colorScheme === "dark"; + const { wordmark } = useLogoAssets(); + + // light: black text (standard) or grey text (muted) + // dark: white text for both variants + const src = isDark ? wordmark.white : muted ? wordmark.grey : wordmark.black; + + return {alt}; +} diff --git a/frontend/src/core/components/shared/ZipWarningModal.tsx b/frontend/src/core/components/shared/ZipWarningModal.tsx index 909cf1b31b..024cb63976 100644 --- a/frontend/src/core/components/shared/ZipWarningModal.tsx +++ b/frontend/src/core/components/shared/ZipWarningModal.tsx @@ -15,12 +15,18 @@ interface ZipWarningModalProps { const WARNING_ICON_STYLE: CSSProperties = { fontSize: 36, - display: 'block', - margin: '0 auto 8px', - color: 'var(--mantine-color-blue-6)' + display: "block", + margin: "0 auto 8px", + color: "var(--mantine-color-blue-6)", }; -const ZipWarningModal = ({ opened, onConfirm, onCancel, fileCount, zipFileName }: ZipWarningModalProps) => { +const ZipWarningModal = ({ + opened, + onConfirm, + onCancel, + fileCount, + zipFileName, +}: ZipWarningModalProps) => { const { t } = useTranslation(); return ( @@ -41,7 +47,7 @@ const ZipWarningModal = ({ opened, onConfirm, onCancel, fileCount, zipFileName } {t("zipWarning.message", { count: fileCount, - defaultValue: "This ZIP contains {{count}} files. Extract anyway?" + defaultValue: "This ZIP contains {{count}} files. Extract anyway?", })} diff --git a/frontend/src/core/components/shared/config/LoginRequiredBanner.tsx b/frontend/src/core/components/shared/config/LoginRequiredBanner.tsx index f16e38f4ea..04f6db4322 100644 --- a/frontend/src/core/components/shared/config/LoginRequiredBanner.tsx +++ b/frontend/src/core/components/shared/config/LoginRequiredBanner.tsx @@ -1,6 +1,6 @@ -import { Alert, Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import LocalIcon from '@app/components/shared/LocalIcon'; +import { Alert, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import LocalIcon from "@app/components/shared/LocalIcon"; interface LoginRequiredBannerProps { show: boolean; @@ -10,7 +10,9 @@ interface LoginRequiredBannerProps { * Banner component that displays when login mode is required but not enabled * Shows prominent warning that settings are read-only */ -export default function LoginRequiredBanner({ show }: LoginRequiredBannerProps) { +export default function LoginRequiredBanner({ + show, +}: LoginRequiredBannerProps) { const { t } = useTranslation(); if (!show) return null; @@ -18,20 +20,26 @@ export default function LoginRequiredBanner({ show }: LoginRequiredBannerProps) return ( } - title={t('admin.settings.loginDisabled.title', 'Login Mode Required')} + title={t("admin.settings.loginDisabled.title", "Login Mode Required")} color="blue" variant="light" styles={{ root: { - borderLeft: '4px solid var(--mantine-color-blue-6)' - } + borderLeft: "4px solid var(--mantine-color-blue-6)", + }, }} > - {t('admin.settings.loginDisabled.message', 'Login mode must be enabled to modify admin settings. Please set SECURITY_ENABLELOGIN=true in your environment or security.enableLogin: true in settings.yml, then restart the server.')} + {t( + "admin.settings.loginDisabled.message", + "Login mode must be enabled to modify admin settings. Please set SECURITY_ENABLELOGIN=true in your environment or security.enableLogin: true in settings.yml, then restart the server.", + )} - {t('admin.settings.loginDisabled.readOnly', 'The settings below show example values for reference. Enable login mode to view and edit actual configuration.')} + {t( + "admin.settings.loginDisabled.readOnly", + "The settings below show example values for reference. Enable login mode to view and edit actual configuration.", + )} ); diff --git a/frontend/src/core/components/shared/config/OverviewHeader.tsx b/frontend/src/core/components/shared/config/OverviewHeader.tsx index 7be820620a..9c4f3da994 100644 --- a/frontend/src/core/components/shared/config/OverviewHeader.tsx +++ b/frontend/src/core/components/shared/config/OverviewHeader.tsx @@ -1,14 +1,19 @@ -import { Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; +import { Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; export function OverviewHeader() { const { t } = useTranslation(); return (
- {t('config.overview.title', 'Application Configuration')} + + {t("config.overview.title", "Application Configuration")} + - {t('config.overview.description', 'Current application settings and configuration details.')} + {t( + "config.overview.description", + "Current application settings and configuration details.", + )}
); diff --git a/frontend/src/core/components/shared/config/PendingBadge.tsx b/frontend/src/core/components/shared/config/PendingBadge.tsx index cdb3306f80..625c2499e2 100644 --- a/frontend/src/core/components/shared/config/PendingBadge.tsx +++ b/frontend/src/core/components/shared/config/PendingBadge.tsx @@ -1,22 +1,22 @@ -import { Badge } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; +import { Badge } from "@mantine/core"; +import { useTranslation } from "react-i18next"; interface PendingBadgeProps { show: boolean; - size?: 'xs' | 'sm' | 'md' | 'lg'; + size?: "xs" | "sm" | "md" | "lg"; } /** * Badge to show when a setting has been saved but requires restart to take effect. */ -export default function PendingBadge({ show, size = 'xs' }: PendingBadgeProps) { +export default function PendingBadge({ show, size = "xs" }: PendingBadgeProps) { const { t } = useTranslation(); if (!show) return null; return ( - {t('admin.settings.restartRequired', 'Restart Required')} + {t("admin.settings.restartRequired", "Restart Required")} ); } diff --git a/frontend/src/core/components/shared/config/RestartConfirmationModal.tsx b/frontend/src/core/components/shared/config/RestartConfirmationModal.tsx index b97b17a0c6..af96918154 100644 --- a/frontend/src/core/components/shared/config/RestartConfirmationModal.tsx +++ b/frontend/src/core/components/shared/config/RestartConfirmationModal.tsx @@ -1,8 +1,8 @@ -import { Modal, Text, Group, Button, Stack } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import ScheduleIcon from '@mui/icons-material/Schedule'; -import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex'; +import { Modal, Text, Group, Button, Stack } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import ScheduleIcon from "@mui/icons-material/Schedule"; +import { Z_INDEX_OVER_CONFIG_MODAL } from "@app/styles/zIndex"; interface RestartConfirmationModalProps { opened: boolean; @@ -23,7 +23,7 @@ export default function RestartConfirmationModal({ onClose={onClose} title={ - {t('admin.settings.restart.title', 'Restart Required')} + {t("admin.settings.restart.title", "Restart Required")} } centered @@ -34,15 +34,15 @@ export default function RestartConfirmationModal({ {t( - 'admin.settings.restart.message', - 'Settings have been saved successfully. A server restart is required for the changes to take effect.' + "admin.settings.restart.message", + "Settings have been saved successfully. A server restart is required for the changes to take effect.", )} {t( - 'admin.settings.restart.question', - 'Would you like to restart the server now or later?' + "admin.settings.restart.question", + "Would you like to restart the server now or later?", )} @@ -52,14 +52,14 @@ export default function RestartConfirmationModal({ leftSection={} onClick={onClose} > - {t('admin.settings.restart.later', 'Restart Later')} + {t("admin.settings.restart.later", "Restart Later")} diff --git a/frontend/src/core/components/shared/config/SettingsSearchBar.tsx b/frontend/src/core/components/shared/config/SettingsSearchBar.tsx index cbbbd97076..e1114f3fce 100644 --- a/frontend/src/core/components/shared/config/SettingsSearchBar.tsx +++ b/frontend/src/core/components/shared/config/SettingsSearchBar.tsx @@ -1,10 +1,13 @@ -import React, { useMemo, useState, useCallback } from 'react'; -import { Select, Text } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import { NavKey, VALID_NAV_KEYS } from '@app/components/shared/config/types'; -import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex'; -import type { ConfigNavSection, ConfigNavItem } from '@app/components/shared/config/configNavSections'; +import React, { useMemo, useState, useCallback } from "react"; +import { Select, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { NavKey, VALID_NAV_KEYS } from "@app/components/shared/config/types"; +import { Z_INDEX_OVER_CONFIG_MODAL } from "@app/styles/zIndex"; +import type { + ConfigNavSection, + ConfigNavItem, +} from "@app/components/shared/config/configNavSections"; interface SettingsSearchBarProps { configNavSections: ConfigNavSection[]; @@ -21,46 +24,52 @@ interface SettingsSearchOption { matchedContext?: string; } -const SETTINGS_SEARCH_TRANSLATION_PREFIXES: Partial> = { - general: ['settings.general'], - hotkeys: ['settings.hotkeys'], - account: ['account'], - people: ['settings.workspace'], - teams: ['settings.workspace', 'settings.team'], - 'api-keys': ['settings.developer'], - connectionMode: ['settings.connection'], - planBilling: ['settings.planBilling'], - adminGeneral: ['admin.settings.general'], - adminFeatures: ['admin.settings.features'], - adminEndpoints: ['admin.settings.endpoints'], - adminDatabase: ['admin.settings.database'], - adminAdvanced: ['admin.settings.advanced'], - adminSecurity: ['admin.settings.security'], - adminConnections: [ - 'admin.settings.connections', - 'admin.settings.mail', - 'admin.settings.security', - 'admin.settings.telegram', - 'admin.settings.premium', - 'admin.settings.general', - 'settings.securityAuth', - 'settings.connection', - ], - adminPlan: ['settings.planBilling', 'admin.settings.premium', 'settings.licensingAnalytics'], - adminAudit: ['settings.licensingAnalytics'], - adminUsage: ['settings.licensingAnalytics'], - adminLegal: ['admin.settings.legal'], - adminPrivacy: ['admin.settings.privacy'], -}; +const SETTINGS_SEARCH_TRANSLATION_PREFIXES: Partial> = + { + general: ["settings.general"], + hotkeys: ["settings.hotkeys"], + account: ["account"], + people: ["settings.workspace"], + teams: ["settings.workspace", "settings.team"], + "api-keys": ["settings.developer"], + connectionMode: ["settings.connection"], + planBilling: ["settings.planBilling"], + adminGeneral: ["admin.settings.general"], + adminFeatures: ["admin.settings.features"], + adminEndpoints: ["admin.settings.endpoints"], + adminDatabase: ["admin.settings.database"], + adminAdvanced: ["admin.settings.advanced"], + adminSecurity: ["admin.settings.security"], + adminConnections: [ + "admin.settings.connections", + "admin.settings.mail", + "admin.settings.security", + "admin.settings.telegram", + "admin.settings.premium", + "admin.settings.general", + "settings.securityAuth", + "settings.connection", + ], + adminPlan: [ + "settings.planBilling", + "admin.settings.premium", + "settings.licensingAnalytics", + ], + adminAudit: ["settings.licensingAnalytics"], + adminUsage: ["settings.licensingAnalytics"], + adminLegal: ["admin.settings.legal"], + adminPrivacy: ["admin.settings.privacy"], + }; const getTranslationPrefixesForNavKey = (key: string): string[] => { const explicitPrefixes = SETTINGS_SEARCH_TRANSLATION_PREFIXES[key] ?? []; const inferredPrefixes: string[] = []; - if (key.startsWith('admin')) { - const adminSuffix = key.replace(/^admin/, ''); - const normalizedAdminSuffix = adminSuffix.charAt(0).toLowerCase() + adminSuffix.slice(1); + if (key.startsWith("admin")) { + const adminSuffix = key.replace(/^admin/, ""); + const normalizedAdminSuffix = + adminSuffix.charAt(0).toLowerCase() + adminSuffix.slice(1); inferredPrefixes.push(`admin.settings.${normalizedAdminSuffix}`); } else { inferredPrefixes.push(`settings.${key}`); @@ -70,7 +79,7 @@ const getTranslationPrefixesForNavKey = (key: string): string[] => { }; const flattenTranslationStrings = (value: unknown): string[] => { - if (typeof value === 'string') { + if (typeof value === "string") { const trimmed = value.trim(); return trimmed ? [trimmed] : []; } @@ -79,8 +88,10 @@ const flattenTranslationStrings = (value: unknown): string[] => { return value.flatMap(flattenTranslationStrings); } - if (value && typeof value === 'object') { - return Object.values(value as Record).flatMap(flattenTranslationStrings); + if (value && typeof value === "object") { + return Object.values(value as Record).flatMap( + flattenTranslationStrings, + ); } return []; @@ -102,10 +113,10 @@ const buildMatchSnippet = (text: string, query: string): string => { const snippet = text.slice(start, end); if (snippet.length <= maxLength) { - return `${start > 0 ? '…' : ''}${snippet}${end < text.length ? '…' : ''}`; + return `${start > 0 ? "…" : ""}${snippet}${end < text.length ? "…" : ""}`; } - return `${start > 0 ? '…' : ''}${snippet.slice(0, maxLength)}${end < text.length ? '…' : ''}`; + return `${start > 0 ? "…" : ""}${snippet.slice(0, maxLength)}${end < text.length ? "…" : ""}`; }; export const SettingsSearchBar: React.FC = ({ @@ -114,7 +125,7 @@ export const SettingsSearchBar: React.FC = ({ isMobile, }) => { const { t } = useTranslation(); - const [searchValue, setSearchValue] = useState(''); + const [searchValue, setSearchValue] = useState(""); // Build a global index from every accessible settings tab in the modal navigation. // This does not render section components, so API calls still happen only when a tab is opened. @@ -125,7 +136,9 @@ export const SettingsSearchBar: React.FC = ({ .map((item: ConfigNavItem) => { const translationPrefixes = getTranslationPrefixesForNavKey(item.key); const translationContent = translationPrefixes.flatMap((prefix) => - flattenTranslationStrings(t(prefix, { returnObjects: true, defaultValue: {} } as any)) + flattenTranslationStrings( + t(prefix, { returnObjects: true, defaultValue: {} } as any), + ), ); const searchableContent = Array.from( @@ -134,7 +147,7 @@ export const SettingsSearchBar: React.FC = ({ section.title, `/settings/${item.key}`, ...translationContent, - ]) + ]), ); return { @@ -144,7 +157,7 @@ export const SettingsSearchBar: React.FC = ({ destinationPath: `/settings/${item.key}`, searchableContent, }; - }) + }), ); }, [configNavSections, t]); @@ -156,30 +169,36 @@ export const SettingsSearchBar: React.FC = ({ const normalizedQuery = query.toLocaleLowerCase(); - return searchableSections.reduce((accumulator, option) => { - const matchedEntry = option.searchableContent.find((entry) => - entry.toLocaleLowerCase().includes(normalizedQuery) - ); + return searchableSections.reduce( + (accumulator, option) => { + const matchedEntry = option.searchableContent.find((entry) => + entry.toLocaleLowerCase().includes(normalizedQuery), + ); + + if (!matchedEntry) { + return accumulator; + } + + accumulator.push({ + ...option, + matchedContext: buildMatchSnippet(matchedEntry, query), + }); - if (!matchedEntry) { return accumulator; - } - - accumulator.push({ - ...option, - matchedContext: buildMatchSnippet(matchedEntry, query), - }); - - return accumulator; - }, []); + }, + [], + ); }, [searchValue, searchableSections]); - const handleSearchNavigation = useCallback(async (value: string | null) => { - if (!value) return; - if (!VALID_NAV_KEYS.includes(value as NavKey)) return; - await onNavigate(value as NavKey); - setSearchValue(''); - }, [onNavigate]); + const handleSearchNavigation = useCallback( + async (value: string | null) => { + if (!value) return; + if (!VALID_NAV_KEYS.includes(value as NavKey)) return; + await onNavigate(value as NavKey); + setSearchValue(""); + }, + [onNavigate], + ); return ( { + if (val) + updatePreference( + "defaultViewerZoom", + val as ViewerZoomSetting, + ); + }} + data={[ + { + label: t("settings.general.zoomLevel.auto", "Auto"), + value: "auto", + }, + { + label: t("settings.general.zoomLevel.fitWidth", "Fit width"), + value: "fitWidth", + }, + { + label: t("settings.general.zoomLevel.fitPage", "Fit page"), + value: "fitPage", + }, + { label: "50%", value: "50" }, + { label: "75%", value: "75" }, + { label: "100%", value: "100" }, + { label: "125%", value: "125" }, + { label: "150%", value: "150" }, + { label: "200%", value: "200" }, + ]} + style={{ width: 140 }} + allowDeselect={false} + comboboxProps={{ + withinPortal: true, + zIndex: Z_INDEX_OVER_CONFIG_MODAL, + }} + /> +
+
+
+ + {t( + "settings.general.hideUnavailableTools", + "Hide unavailable tools", + )} {t( @@ -307,13 +515,27 @@ const GeneralSection: React.FC = ({ hideTitle = false, hide
updatePreference("hideUnavailableTools", event.currentTarget.checked)} + onChange={(event) => + updatePreference( + "hideUnavailableTools", + event.currentTarget.checked, + ) + } />
-
+
- {t("settings.general.hideUnavailableConversions", "Hide unavailable conversions")} + {t( + "settings.general.hideUnavailableConversions", + "Hide unavailable conversions", + )} {t( @@ -324,7 +546,12 @@ const GeneralSection: React.FC = ({ hideTitle = false, hide
updatePreference("hideUnavailableConversions", event.currentTarget.checked)} + onChange={(event) => + updatePreference( + "hideUnavailableConversions", + event.currentTarget.checked, + ) + } />
= ({ hideTitle = false, hide w={300} withArrow > -
+
{t("settings.general.autoUnzip", "Auto-unzip API responses")} - {t("settings.general.autoUnzipDescription", "Automatically extract files from ZIP responses")} + {t( + "settings.general.autoUnzipDescription", + "Automatically extract files from ZIP responses", + )}
updatePreference("autoUnzip", event.currentTarget.checked)} + onChange={(event) => + updatePreference("autoUnzip", event.currentTarget.checked) + } />
@@ -361,13 +600,26 @@ const GeneralSection: React.FC = ({ hideTitle = false, hide w={300} withArrow > -
+
- {t("settings.general.autoUnzipFileLimit", "Auto-unzip file limit")} + {t( + "settings.general.autoUnzipFileLimit", + "Auto-unzip file limit", + )} - {t("settings.general.autoUnzipFileLimitDescription", "Maximum number of files to extract from ZIP")} + {t( + "settings.general.autoUnzipFileLimitDescription", + "Maximum number of files to extract from ZIP", + )}
= ({ hideTitle = false, hide onBlur={() => { const numValue = Number(fileLimitInput); const finalValue = - !fileLimitInput || isNaN(numValue) || numValue < 1 || numValue > 100 + !fileLimitInput || + isNaN(numValue) || + numValue < 1 || + numValue > 100 ? DEFAULT_AUTO_UNZIP_FILE_LIMIT : numValue; setFileLimitInput(finalValue); diff --git a/frontend/src/core/components/shared/config/configSections/HotkeysSection.tsx b/frontend/src/core/components/shared/config/configSections/HotkeysSection.tsx index 8d0774fbe1..e7e15c9e89 100644 --- a/frontend/src/core/components/shared/config/configSections/HotkeysSection.tsx +++ b/frontend/src/core/components/shared/config/configSections/HotkeysSection.tsx @@ -1,45 +1,73 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { Alert, Badge, Box, Button, Divider, Group, Paper, Stack, Text, TextInput } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; -import { useHotkeys } from '@app/contexts/HotkeyContext'; -import { ToolId } from '@app/types/toolId'; -import HotkeyDisplay from '@app/components/hotkeys/HotkeyDisplay'; -import { bindingEquals, eventToBinding, HotkeyBinding } from '@app/utils/hotkeys'; +import React, { useEffect, useMemo, useState } from "react"; +import { + Alert, + Badge, + Box, + Button, + Divider, + Group, + Paper, + Stack, + Text, + TextInput, +} from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext"; +import { useHotkeys } from "@app/contexts/HotkeyContext"; +import { ToolId } from "@app/types/toolId"; +import HotkeyDisplay from "@app/components/hotkeys/HotkeyDisplay"; +import { + bindingEquals, + eventToBinding, + HotkeyBinding, +} from "@app/utils/hotkeys"; import { ToolRegistryEntry } from "@app/data/toolsTaxonomy"; const rowStyle: React.CSSProperties = { - display: 'flex', - flexDirection: 'column', - gap: '0.5rem', + display: "flex", + flexDirection: "column", + gap: "0.5rem", }; const rowHeaderStyle: React.CSSProperties = { - display: 'flex', - flexWrap: 'wrap', - alignItems: 'center', - justifyContent: 'space-between', - gap: '0.5rem', + display: "flex", + flexWrap: "wrap", + alignItems: "center", + justifyContent: "space-between", + gap: "0.5rem", }; const HotkeysSection: React.FC = () => { const { t } = useTranslation(); const { toolRegistry } = useToolWorkflow(); - const { hotkeys, defaults, updateHotkey, resetHotkey, pauseHotkeys, resumeHotkeys, getDisplayParts, isMac } = useHotkeys(); + const { + hotkeys, + defaults, + updateHotkey, + resetHotkey, + pauseHotkeys, + resumeHotkeys, + getDisplayParts, + isMac, + } = useHotkeys(); const [editingTool, setEditingTool] = useState(null); const [error, setError] = useState(null); - const [searchQuery, setSearchQuery] = useState(''); + const [searchQuery, setSearchQuery] = useState(""); - const tools = useMemo(() => Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][], [toolRegistry]); + const tools = useMemo( + () => Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][], + [toolRegistry], + ); const filteredTools = useMemo(() => { if (!searchQuery.trim()) return tools; - + const query = searchQuery.toLowerCase(); - return tools.filter(([toolId, tool]) => - tool.name.toLowerCase().includes(query) || - tool.description.toLowerCase().includes(query) || - toolId.toLowerCase().includes(query) + return tools.filter( + ([toolId, tool]) => + tool.name.toLowerCase().includes(query) || + tool.description.toLowerCase().includes(query) || + toolId.toLowerCase().includes(query), ); }, [tools, searchQuery]); @@ -59,7 +87,7 @@ const HotkeysSection: React.FC = () => { } const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') { + if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); setEditingTool(null); @@ -72,21 +100,31 @@ const HotkeysSection: React.FC = () => { const binding = eventToBinding(event as KeyboardEvent); if (!binding) { - const osKey = isMac ? 'mac' : 'windows'; + const osKey = isMac ? "mac" : "windows"; setError(t(`settings.hotkeys.errorModifier.${osKey}`)); return; } - const conflictEntry = (Object.entries(hotkeys) as [ToolId, HotkeyBinding][]).find(([toolId, existing]) => ( - toolId !== editingTool && bindingEquals(existing, binding) - )); + const conflictEntry = ( + Object.entries(hotkeys) as [ToolId, HotkeyBinding][] + ).find( + ([toolId, existing]) => + toolId !== editingTool && bindingEquals(existing, binding), + ); if (conflictEntry) { const conflictKey = conflictEntry[0]; - const conflictTool = (conflictKey in toolRegistry) - ? toolRegistry[conflictKey as ToolId]?.name - : conflictKey; - setError(t('settings.hotkeys.errorConflict', 'Shortcut already used by {{tool}}.', { tool: conflictTool })); + const conflictTool = + conflictKey in toolRegistry + ? toolRegistry[conflictKey as ToolId]?.name + : conflictKey; + setError( + t( + "settings.hotkeys.errorConflict", + "Shortcut already used by {{tool}}.", + { tool: conflictTool }, + ), + ); return; } @@ -95,9 +133,9 @@ const HotkeysSection: React.FC = () => { setError(null); }; - window.addEventListener('keydown', handleKeyDown, true); + window.addEventListener("keydown", handleKeyDown, true); return () => { - window.removeEventListener('keydown', handleKeyDown, true); + window.removeEventListener("keydown", handleKeyDown, true); }; }, [editingTool, hotkeys, toolRegistry, updateHotkey, t]); @@ -109,14 +147,19 @@ const HotkeysSection: React.FC = () => { return (
- {t('settings.hotkeys.title', 'Keyboard Shortcuts')} + + {t("settings.hotkeys.title", "Keyboard Shortcuts")} + - {t('settings.hotkeys.description', 'Customize keyboard shortcuts for quick tool access. Click "Change shortcut" and press a new key combination. Press Esc to cancel.')} + {t( + "settings.hotkeys.description", + 'Customize keyboard shortcuts for quick tool access. Click "Change shortcut" and press a new key combination. Press Esc to cancel.', + )}
setSearchQuery(event.currentTarget.value)} size="md" @@ -127,70 +170,88 @@ const HotkeysSection: React.FC = () => { {filteredTools.length === 0 ? ( - {t('toolPicker.noToolsFound', 'No tools found')} + {t("toolPicker.noToolsFound", "No tools found")} ) : ( filteredTools.map(([toolId, tool], index) => { - const currentBinding = hotkeys[toolId]; - const defaultBinding = defaults[toolId]; - const isEditing = editingTool === toolId; - const defaultParts = getDisplayParts(defaultBinding); - const defaultLabel = defaultParts.length > 0 - ? defaultParts.join(' + ') - : t('settings.hotkeys.none', 'Not assigned'); + const currentBinding = hotkeys[toolId]; + const defaultBinding = defaults[toolId]; + const isEditing = editingTool === toolId; + const defaultParts = getDisplayParts(defaultBinding); + const defaultLabel = + defaultParts.length > 0 + ? defaultParts.join(" + ") + : t("settings.hotkeys.none", "Not assigned"); - return ( - - -
-
- {tool.name} - - - {!bindingEquals(currentBinding, defaultBinding) && ( - - {t('settings.hotkeys.customBadge', 'Custom')} - - )} - - {t('settings.hotkeys.defaultLabel', 'Default: {{shortcut}}', { shortcut: defaultLabel })} - + return ( + + +
+
+ {tool.name} + + + {!bindingEquals(currentBinding, defaultBinding) && ( + + {t("settings.hotkeys.customBadge", "Custom")} + + )} + + {t( + "settings.hotkeys.defaultLabel", + "Default: {{shortcut}}", + { shortcut: defaultLabel }, + )} + + +
+ + + +
- - - - -
+ {isEditing && error && ( + + {error} + + )} + - {isEditing && error && ( - - {error} - - )} - - - {index < filteredTools.length - 1 && } - - ); - }) + {index < filteredTools.length - 1 && } + + ); + }) )} diff --git a/frontend/src/core/components/shared/config/configSections/Overview.tsx b/frontend/src/core/components/shared/config/configSections/Overview.tsx index 3bc8e84d91..6121834027 100644 --- a/frontend/src/core/components/shared/config/configSections/Overview.tsx +++ b/frontend/src/core/components/shared/config/configSections/Overview.tsx @@ -1,33 +1,35 @@ -import React from 'react'; -import { Stack, Text, Code, Group, Badge, Alert, Loader } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import { useAppConfig } from '@app/contexts/AppConfigContext'; -import { OverviewHeader } from '@app/components/shared/config/OverviewHeader'; +import React from "react"; +import { Stack, Text, Code, Group, Badge, Alert, Loader } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { OverviewHeader } from "@app/components/shared/config/OverviewHeader"; const Overview: React.FC = () => { const { t } = useTranslation(); const { config, loading, error } = useAppConfig(); const renderConfigSection = (title: string, data: any) => { - if (!data || typeof data !== 'object') return null; + if (!data || typeof data !== "object") return null; return ( - {title} + + {title} + {Object.entries(data).map(([key, value]) => ( {key}: - {typeof value === 'boolean' ? ( - - {value ? 'true' : 'false'} + {typeof value === "boolean" ? ( + + {value ? "true" : "false"} - ) : typeof value === 'object' ? ( + ) : typeof value === "object" ? ( {JSON.stringify(value, null, 2)} ) : ( - String(value) || 'null' + String(value) || "null" )} ))} @@ -36,38 +38,48 @@ const Overview: React.FC = () => { ); }; - const basicConfig = config ? { - appNameNavbar: config.appNameNavbar, - baseUrl: config.baseUrl, - contextPath: config.contextPath, - serverPort: config.serverPort, - } : null; + const basicConfig = config + ? { + appNameNavbar: config.appNameNavbar, + baseUrl: config.baseUrl, + contextPath: config.contextPath, + serverPort: config.serverPort, + } + : null; - const securityConfig = config ? { - enableLogin: config.enableLogin, - } : null; + const securityConfig = config + ? { + enableLogin: config.enableLogin, + } + : null; - const systemConfig = config ? { - enableAlphaFunctionality: config.enableAlphaFunctionality, - enableAnalytics: config.enableAnalytics, - } : null; + const systemConfig = config + ? { + enableAlphaFunctionality: config.enableAlphaFunctionality, + enableAnalytics: config.enableAnalytics, + } + : null; - const integrationConfig = config ? { - SSOAutoLogin: config.SSOAutoLogin, - } : null; + const integrationConfig = config + ? { + SSOAutoLogin: config.SSOAutoLogin, + } + : null; if (loading) { return ( - {t('config.overview.loading', 'Loading configuration...')} + + {t("config.overview.loading", "Loading configuration...")} + ); } if (error) { return ( - + {error} ); @@ -79,13 +91,31 @@ const Overview: React.FC = () => { {config && ( <> - {renderConfigSection(t('config.overview.sections.basic', 'Basic Configuration'), basicConfig)} - {renderConfigSection(t('config.overview.sections.security', 'Security Configuration'), securityConfig)} - {renderConfigSection(t('config.overview.sections.system', 'System Configuration'), systemConfig)} - {renderConfigSection(t('config.overview.sections.integration', 'Integration Configuration'), integrationConfig)} + {renderConfigSection( + t("config.overview.sections.basic", "Basic Configuration"), + basicConfig, + )} + {renderConfigSection( + t("config.overview.sections.security", "Security Configuration"), + securityConfig, + )} + {renderConfigSection( + t("config.overview.sections.system", "System Configuration"), + systemConfig, + )} + {renderConfigSection( + t( + "config.overview.sections.integration", + "Integration Configuration", + ), + integrationConfig, + )} {config.error && ( - + {config.error} )} diff --git a/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx b/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx index be3348b794..3e2563c577 100644 --- a/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx +++ b/frontend/src/core/components/shared/config/configSections/ProviderCard.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState } from "react"; import { Paper, Group, @@ -12,11 +12,14 @@ import { NumberInput, TagsInput, Anchor, -} from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import LocalIcon from '@app/components/shared/LocalIcon'; -import EditableSecretField from '@app/components/shared/EditableSecretField'; -import { Provider, ProviderField } from '@app/components/shared/config/configSections/providerDefinitions'; +} from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import EditableSecretField from "@app/components/shared/EditableSecretField"; +import { + Provider, + ProviderField, +} from "@app/components/shared/config/configSections/providerDefinitions"; interface ProviderCardProps { provider: Provider; @@ -41,7 +44,8 @@ export default function ProviderCard({ }: ProviderCardProps) { const { t } = useTranslation(); const [expanded, setExpanded] = useState(false); - const [localSettings, setLocalSettings] = useState>(settings); + const [localSettings, setLocalSettings] = + useState>(settings); // Keep local settings in sync with incoming settings (values loaded from settings.yml) // Update whenever parent settings change, whether expanded or not (important for Discard to work) @@ -57,7 +61,8 @@ export default function ProviderCard({ const defaultSettings: Record = { ...settings }; provider.fields.forEach((field) => { if (field.defaultValue !== undefined) { - defaultSettings[field.key] = defaultSettings[field.key] ?? field.defaultValue; + defaultSettings[field.key] = + defaultSettings[field.key] ?? field.defaultValue; } }); setLocalSettings(defaultSettings); @@ -83,15 +88,26 @@ export default function ProviderCard({ }; const renderField = (field: ProviderField) => { - const value = localSettings[field.key] ?? field.defaultValue ?? ''; + const value = localSettings[field.key] ?? field.defaultValue ?? ""; switch (field.type) { - case 'switch': + case "switch": return ( -
+
- {field.label} - {field.description} + + {field.label} + + + {field.description} +
); - case 'password': + case "password": return ( ); - case 'textarea': + case "textarea": return (