Compare commits

...
Author SHA1 Message Date
Anthony StirlingandGitHub 2b25cce21c Merge branch 'main' into codex/fix-nullpointerexception-for-p12file 2025-12-13 16:52:10 +00:00
Anthony StirlingandGitHub 5f54308d2b Simplify V2 PR auto-deploy triggers (#5236)
## Summary
- deploy V2 environments for authorized PR authors without requiring
title or branch keywords
- update deployment messaging to reflect the new trigger policy

## Testing
- not run


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_693c9ac446d88328b8ae04609caec787)
2025-12-13 16:51:16 +00:00
Reece BrowneandGitHub 6f7b8ce433 Bug/v2/static wasm (#5238)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### 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 [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

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.
2025-12-13 12:57:10 +00:00
James BruntonandGitHub 69ffd29bb5 Fix German text for sign tool text entry (#5232)
# Description of Changes
Fix #5206

Required splitting out the logic for the text with font entry so that
the labels are configurable from the call-site instead of all using the
same ones for Sign.
2025-12-12 18:00:40 +00:00
Reece BrowneandGitHub f4cc87144d Fix language codes in picker (#5233)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### 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 [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

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.
2025-12-12 17:44:15 +00:00
Anthony StirlingandGitHub c86e2d6840 Delete .github/README.md 2025-12-11 16:38:32 +00:00
Anthony StirlingandGitHub 5f072f87bb Account change details (#5190)
## Summary
Accounts setting page to change a users password or username
Fix huge bug were users can see admin settings due to hard code
admin=true
## Testing
- not run (not requested)

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_6934b8ecdbf08328a0951b46db77dfd2)
2025-12-11 13:56:35 +00:00
Anthony StirlingandGitHub eb3e57577c Bump version from 2.1.2 to 2.1.3 (#5224)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### 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 [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

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.
2025-12-11 13:16:37 +00:00
Reece BrowneandGitHub f29d85565a Chore/v2/ctrlf (#5217)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### 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 [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

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.
2025-12-11 12:42:16 +00:00
Reece BrowneandGitHub ae72344317 Offline pdfium (#5213)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### 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 [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

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.
2025-12-11 11:23:20 +00:00
Reece BrowneandGitHub 6565a6ce18 Bug/v2/improved cache busting (#5107)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### 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 [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

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.
2025-12-11 11:19:55 +00:00
LudyandGitHub e26035c3b3 build(versioning): synchronize app version across Tauri and simulation configs (#5120)
# Description of Changes

- **What was changed**
- Added `groovy.json.JsonOutput` and `groovy.json.JsonSlurper` imports
to `build.gradle`.
- Introduced a reusable `writeIfChanged(File targetFile, String
newContent)` helper to avoid unnecessary file writes when content is
unchanged.
  - Added `updateTauriConfigVersion(String version)` to:
    - Parse `frontend/src-tauri/tauri.conf.json`.
    - Set the `version` field from `project.version`.
- Re-write the file as pretty-printed JSON (with a trailing line
separator) only if content actually changed.
- Added `updateSimulationVersion(File fileToUpdate, String version)` to:
- Locate the `appVersion: '<value>'` assignment via regex in simulation
files.
    - Replace the existing version with `project.version`.
- Fail the build with a clear `GradleException` if `appVersion` cannot
be found.
- Registered a new Gradle task `syncAppVersion` (group: `versioning`)
which:
    - Reads `project.version` as the canonical app version.
    - Updates `frontend/src-tauri/tauri.conf.json`.
- Updates `frontend/src/core/testing/serverExperienceSimulations.ts`.
- Updates
`frontend/src/proprietary/testing/serverExperienceSimulations.ts`.
- Updated the main `build` task so it now depends on `syncAppVersion` in
addition to `:stirling-pdf:bootJar` and `buildRestartHelper`.

- **Why the change was made**
- To ensure the desktop Tauri configuration and server experience
simulation configs consistently use the same application version as
defined in `project.version`.
- To remove manual version bumps in multiple files and eliminate the
risk of version mismatches between backend, desktop app, and
simulation/testing tooling.
- To minimize noise in commits and CI by only touching versioned files
when their content actually changes (using `writeIfChanged`).


---

## Checklist

### 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 [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

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.
2025-12-11 11:13:54 +00:00
ConnorYohandGitHub e474cc76ad Improved static upgrade flow (#5214)
<img width="996" height="621" alt="image"
src="https://github.com/user-attachments/assets/1ac87414-09ed-4307-8f7c-25984e0c89d1"
/>
<img width="608" height="351" alt="image"
src="https://github.com/user-attachments/assets/c271f75e-4844-4034-8905-007cc7ab1265"
/>
<img width="660" height="355" alt="image"
src="https://github.com/user-attachments/assets/34913b74-d4fa-418a-b098-fda48b41f0dd"
/>
<img width="1371" height="906" alt="image"
src="https://github.com/user-attachments/assets/35b61389-fd67-41b3-9969-e5409e53b362"
/>
<img width="639" height="450" alt="image"
src="https://github.com/user-attachments/assets/ae018bf3-0fcf-4221-892f-440d7325540a"
/>
<img width="963" height="599" alt="image"
src="https://github.com/user-attachments/assets/f6f67682-f43c-46f3-8632-16b209780b15"
/>
<img width="982" height="628" alt="image"
src="https://github.com/user-attachments/assets/45a7c171-3eb4-4271-a299-f3a6e78c1a52"
/>
2025-12-11 11:13:20 +00:00
Reece BrowneandGitHub 43eaa84a8f fix tooltips on tab (#5219)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### 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 [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

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.
2025-12-11 09:20:39 +00:00
James BruntonandGitHub 2cd4175689 Fix Mac app not being able to open files with spaces in their name (#5218)
# Description of Changes
Fix #5189.

Fix Mac app not being able to open files with spaces in their name,
which was happening because the URL was not being decoded on input.
2025-12-11 08:55:37 +00:00
Dario Ghunney WareandGitHub d6a83fe6a1 Fix: SSO Login Page (#5220)
Users logging in via OAuth2 were redirected to Spring's default login
form instead of the React frontend login page. This happened because the
OAuth2 configuration used `.loginPage("/oauth2")` which pointed to the
old Thymeleaf template.

### Testing (if applicable)

- [x] 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.
2025-12-10 19:46:48 +00:00
James BruntonandGitHub 3c92cb7c2b Improve styling of quick access bar (#5197)
# Description of Changes

Currently, the Quick Access Bar only renders well in Chrome. It's got
all sorts of layout issues in Firefox and Safari. This PR attempts to
retain the recent changes to make the bar thinner etc. but make it work
better in all browsers.
2025-12-10 17:21:07 +00:00
James BruntonandGitHub b83888c74a Make lite version of CI (#5188)
# Description of Changes
Add lite mode for CI which just runs the most important jobs for
deployment. This won't be used in this repo, but allows other repos
containing Stirling to easily disable jobs like desktop builds etc. if
they're unnecessary, without needing to deal with conflicts in the
files. They'll just need to set the repo variable `CI_PROFILE` to
`lite`. We have an upstream repo that we'd like these changes for.
2025-12-10 13:54:57 +00:00
Anthony StirlingandGitHub 787d0d21c9 lang updates plus --include-existing flag (#5212)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### 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 [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

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.
2025-12-10 11:41:11 +00:00
Dario Ghunney WareandGitHub 7b26b184d1 Fix: Access to Swagger UI when login enabled (#5194)
Fixes for /swagger-ui/index.html & /v1/api-docs endpoints not being
accessible when login was enabled.

- `UserAuthenticationFilter.isPublicAuthEndpoint()` had gaps in its
check, missing `/v1/api-docs`
- Refactored `UserAuthenticationFilter` to use
`RequestUriUtils.isPublicAuthEndpoint()` instead of its own incorrect
method

Closes #5125 & #5028

---

### Testing (if applicable)

- [x] 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.
2025-12-10 11:39:29 +00:00
Anthony StirlingandGitHub f17ad56def Handle restricted language configuration fallback V2 (#5154)
## Summary
- restrict supported languages to the validated list from app-config
instead of always adding an extra fallback
- set the effective fallback locale to the preferred configured language
and switch away from disallowed selections automatically

## Testing
- ./gradlew build


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_6930529bc6c08328a1ce05f7d1316e27)
2025-12-10 11:12:18 +00:00
Reece BrowneandGitHub de438d00e1 Fiix colours (#5211)
<img width="638" height="471" alt="image"
src="https://github.com/user-attachments/assets/c2352e25-6ee0-4726-9ce4-059870347ea6"
/>
<img width="638" height="450" alt="image"
src="https://github.com/user-attachments/assets/a17931ca-ffea-4dc3-a17a-629d98003da6"
/>
<img width="985" height="774" alt="image"
src="https://github.com/user-attachments/assets/b16203c6-a136-4fee-ba57-495f7b6e8c75"
/>
<img width="635" height="348" alt="image"
src="https://github.com/user-attachments/assets/4b4ab328-b2e4-442f-84a5-d947e97653ec"
/>
2025-12-10 11:10:34 +00:00
Anthony StirlingandGitHub 6787169583 Handle composition input in PDF text editor (#5192)
## Summary
- track IME composition state in the PDF text editor to avoid
interrupting phonetic input methods
- update text syncing to occur after composition completes and skip
redundant updates mid-composition

## Testing
- npm run lint -- --max-warnings 0


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_693744be74148328bd3bda9150de6e56)
2025-12-10 10:11:45 +00:00
Anthony StirlingandGitHub 291e1a392b extra font support in text editor (#5208)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### 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 [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

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.
2025-12-10 10:11:16 +00:00
Anthony StirlingandGitHub 7c5aa3685f Add configurable SMTP TLS/SSL options (#5204)
## Summary
- add optional STARTTLS and SSL-related fields to mail settings with
defaults matching prior behavior
- apply the new mail properties in the SMTP JavaMail configuration,
including trust and hostname verification overrides
- update mail settings template and tests to cover default behavior and
explicit TLS/SSL overrides
- clarify STARTTLS naming and sslTrust usage with examples in property
comments and the settings template
- default sslTrust to a wildcard when unset so TLS connections accept
any host by default unless tightened

## Testing
- ./gradlew :proprietary:test --tests
stirling.software.proprietary.security.service.MailConfigTest --console
plain

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_693864b2a6648328ae75c7e88a726a65)
2025-12-10 10:10:54 +00:00
Anthony StirlingGitHubCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
9c03914edd Add admin password reset option for users (#5180)
## Summary
- add backend support for admins to reset user passwords and optionally
email notifications when SMTP is enabled
- surface mail capability in admin settings data for the UI
- add a shared change-password modal hooked into People and Team user
actions with random password generation and email options

## Testing
- not run (not requested)


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_6934b978fe3c83289b5b95dec79b3d38)

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-12-10 10:10:40 +00:00
James BruntonandGitHub c980ee10c0 Backport fixes from SaaS (#5187)
# Description of Changes
- Add new skeleton loader style type (block - nothing's currently using
it but it might as well be available)
- Make Dev API overridable (and set to the new docs that actually work
while Swagger docs don't work properly)
2025-12-09 11:45:01 +00:00
James BruntonandGitHub fa4d2bc09a Fix path to sample file in tour (#5186)
# Description of Changes
Fix path to sample file in tour
2025-12-09 11:43:18 +00:00
Anthony Stirling 605ae98dc0 Validate certificate inputs for cert signing 2025-12-09 10:51:53 +00:00
Anthony StirlingandGitHub 7faf7e50fa Chang etext on intro (#5160)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### 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 [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

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.
2025-12-06 00:06:11 +00:00
EthanHealy01andGitHub bb201ef9c1 Chore/bump gradle version number (#5176)
bump version number
2025-12-05 23:22:32 +00:00
82dbcfbb9b SSO login fix (#5167)
Fixes bug where SSO login with custom providers caused an
`InvalidClientRegistrationIdException: Invalid Client Registration with
Id: oidc` errors.

  Root Cause:
- Backend: Redirect URI was hardcoded to `/login/oauth2/code/oidc`
regardless of provider registration ID
- Frontend: Unknown providers were mapped back to 'oidc' instead of
using actual provider ID

Closes #5141

---------

Co-authored-by: Anthony Stirling <77850077+frooodle@users.noreply.github.com>
Co-authored-by: Keon Chen <66115421+keonchennl@users.noreply.github.com>
2025-12-05 23:19:41 +00:00
EthanHealy01andGitHub 9fd8fd89ed add enum SERVER to list of valid licenses (#5172) 2025-12-05 13:18:23 +00:00
Keon ChenandGitHub 3a2370ea1f Update OCR setup guide link in LanguagePicker (#5162)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
Update the link to the doc page for language picker in OCR config
- Why the change was made
Old link doesn't work anymore
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ x] 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)
- [ x] I have performed a self-review of my own code
- [ x] My changes generate no new warnings

### Documentation

- [x ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [x ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.
2025-12-04 21:35:11 +00:00
e7db714091 More fixes for automate (#5168)
# Description of Changes
Fix file missed in #5127 to use `apiClient` instead of `axios` directly

Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2025-12-04 17:53:08 +00:00
c6b4a2b141 Desktop to match normal login screens (#5122)1
Also fixed issue with csrf
Also fixed issue with rust keychain

---------

Co-authored-by: James Brunton <jbrunton96@gmail.com>
2025-12-04 17:48:19 +00:00
Anthony StirlingandGitHub 7459463a3c V2 sso in server plan (#5158)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### 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 [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

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.
2025-12-03 21:12:29 +00:00
c9bf436895 couple of small fixes for text editor (#5155)
# Description of Changes

- Workbench.tsx: Allow PDF text editor to handle it's own custom state
(this is a bit of a hotfix, we're going to handle the PDF text editors
file upload flow better in a future PR).
- PdfTextEditorView.tsx: Added a dropzone and some helper text to upload
the first file.
- PdfTextEditorView.tsx: Hide document view when isConverting is true.
Prevents showing stale content from previous file during conversion.
- useTranslatedToolRegistry.tsx: Moved PDF Text Editor to top of
Recommended tools list. Increased visibility of the new feature.
- PdfTextEditor.tsx: Removed auto-navigation to PDF Editor workbench on
file selection. Stops the "jumpy" behavior when selecting files.
- HomePage.tsx: Check specifically for pdfTextEditor tool instead of any
custom workbench. Prevents auto-switch to viewer when uploading files
while in PDF Text Editor.

<img width="2056" height="1073" alt="Screenshot 2025-12-03 at 6 01
14 PM"
src="https://github.com/user-attachments/assets/dfc63a46-7991-486c-ba00-0ce7637502f5"
/>


---

## Checklist

### 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 [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

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2025-12-03 20:37:23 +00:00
EthanHealy01andGitHub f8dbf171e1 Feature/v2/get all info on pdf (#5105)
# Description of Changes

- Addition of the get all info on PDF tool

---

## Checklist

### 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 [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

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.
2025-12-03 20:02:42 +00:00
ConnorYohandGitHub e59c717dc0 Fixes state management loops around getting results V2 (#5153)
Makes sure settings step collapses in results step

Makes sure result step doesn't always reset even in development for
baseTool

Make sure result step doesn't reset for convert
2025-12-03 17:42:04 +00:00
ConnorYohandGitHub f2bffe2dc6 Fix-convert-V2 (#5147)
Custom processors can now return consume all inputs flag. This allows to
have many inputs to single output consumption

Fixed multi call conversion logic
2025-12-03 17:39:49 +00:00
Anthony StirlingandGitHub 5d827df08c Add onboarding bypass flag V2 version2 version 2 (#5151)
## Summary
- add a shared hook that honors a `bypassOnboarding` query parameter and
marks onboarding steps as completed for the session
- block onboarding orchestrator and UI elements when the bypass flag is
present so tours and popups stay hidden

## Testing
- ./gradlew build


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_693059f866a8832891dd97f3d52ca5a0)
2025-12-03 17:17:22 +00:00
Anthony StirlingandGitHub bdb3c887f3 opensource text editor (#5146)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### 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 [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

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### 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.
2025-12-03 12:55:34 +00:00
264 changed files with 12013 additions and 2038 deletions
+3 -14
View File
@@ -52,7 +52,6 @@ jobs:
core.setOutput('repository', pr.head.repo.full_name);
core.setOutput('ref', pr.head.ref);
core.setOutput('is_fork', String(pr.head.repo.fork));
core.setOutput('base_ref', pr.base.ref);
core.setOutput('author', pr.user.login);
core.setOutput('state', pr.state);
@@ -65,10 +64,6 @@ jobs:
IS_FORK: ${{ steps.resolve.outputs.is_fork }}
# nur bei workflow_dispatch gesetzt:
ALLOW_FORK_INPUT: ${{ inputs.allow_fork }}
# für Auto-PR-Logik:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_BASE: ${{ steps.resolve.outputs.base_ref }}
PR_AUTHOR: ${{ steps.resolve.outputs.author }}
run: |
set -e
@@ -89,14 +84,8 @@ jobs:
else
auth_users=("Frooodle" "sf298" "Ludy87" "LaserKaspar" "sbplat" "reecebrowne" "DarioGii" "ConnorYoh" "EthanHealy01" "jbrunton96" "balazs-szucs")
is_auth=false; for u in "${auth_users[@]}"; do [ "$u" = "$PR_AUTHOR" ] && is_auth=true && break; done
if [ "$PR_BASE" = "V2" ] && [ "$is_auth" = true ]; then
if [ "$is_auth" = true ]; then
should=true
else
title_has_v2=false; echo "$PR_TITLE" | grep -qiE 'v2|version.?2|version.?two' && title_has_v2=true
branch_has_kw=false; echo "$PR_BRANCH" | grep -qiE 'v2|react' && branch_has_kw=true
if [ "$is_auth" = true ] && { [ "$title_has_v2" = true ] || [ "$branch_has_kw" = true ]; }; then
should=true
fi
fi
fi
@@ -174,7 +163,7 @@ jobs:
owner,
repo,
issue_number: prNumber,
body: `🚀 **Auto-deploying V2 version** for PR #${prNumber}...\n\n_This is an automated deployment triggered by V2/version2 keywords in the PR title or V2/React keywords in the branch name._\n\n⚠️ **Note:** If new commits are pushed during deployment, this build will be cancelled and replaced with the latest version.`
body: `🚀 **Auto-deploying V2 version** for PR #${prNumber}...\n\n_This is an automated deployment for approved V2 contributors._\n\n⚠️ **Note:** If new commits are pushed during deployment, this build will be cancelled and replaced with the latest version.`
});
return newComment.id;
@@ -394,7 +383,7 @@ jobs:
`🔗 **Direct Test URL (non-SSL)** [${deploymentUrl}](${deploymentUrl})\n\n` +
`🔐 **Secure HTTPS URL**: [${httpsUrl}](${httpsUrl})\n\n` +
`_This deployment will be automatically cleaned up when the PR is closed._\n\n` +
`🔄 **Auto-deployed** because PR title or branch name contains V2/version2/React keywords.`;
`🔄 **Auto-deployed** for approved V2 contributors.`;
await github.rest.issues.createComment({
owner,
@@ -14,6 +14,7 @@ jobs:
permissions:
issues: write
if: |
vars.CI_PROFILE != 'lite' &&
github.event.issue.pull_request &&
(
contains(github.event.comment.body, 'prdeploy') ||
+15 -2
View File
@@ -262,7 +262,13 @@ jobs:
strategy:
fail-fast: false
matrix:
docker-rev: ["docker/embedded/Dockerfile", "docker/embedded/Dockerfile.ultra-lite", "docker/embedded/Dockerfile.fat"]
include:
- docker-rev: docker/embedded/Dockerfile
artifact-suffix: Dockerfile
- docker-rev: docker/embedded/Dockerfile.ultra-lite
artifact-suffix: Dockerfile.ultra-lite
- docker-rev: docker/embedded/Dockerfile.fat
artifact-suffix: Dockerfile.fat
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
@@ -272,6 +278,13 @@ jobs:
- name: Checkout Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Free disk space on runner
run: |
echo "Disk space before cleanup:" && df -h
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /usr/local/share/boost
docker system prune -af || true
echo "Disk space after cleanup:" && df -h
- name: Set up JDK 17
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
@@ -313,7 +326,7 @@ jobs:
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: reports-docker-${{ matrix.docker-rev }}
name: reports-docker-${{ matrix.artifact-suffix }}
path: |
build/reports/tests/
build/test-results/
+1
View File
@@ -31,6 +31,7 @@ permissions:
jobs:
determine-matrix:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
+1
View File
@@ -24,6 +24,7 @@ permissions:
jobs:
push:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-24.04-8core
permissions:
packages: write
+1
View File
@@ -24,6 +24,7 @@ permissions:
jobs:
push:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-latest
permissions:
packages: write
+1
View File
@@ -17,6 +17,7 @@ permissions: read-all
jobs:
analysis:
if: ${{ vars.CI_PROFILE != 'lite' }}
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
+1
View File
@@ -27,6 +27,7 @@ permissions:
jobs:
sonarqube:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-latest
steps:
- name: Harden Runner
+1
View File
@@ -10,6 +10,7 @@ permissions:
jobs:
stale:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-latest
permissions:
issues: write
+1
View File
@@ -23,6 +23,7 @@ permissions:
jobs:
push:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-latest
steps:
- name: Harden Runner
+3
View File
@@ -28,6 +28,7 @@ permissions:
jobs:
determine-matrix:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
@@ -636,6 +637,8 @@ jobs:
if [ "${{ needs.build.result }}" = "success" ]; then
echo "✅ All Tauri builds completed successfully!"
echo "Artifacts are ready for distribution."
elif [ "${{ needs.build.result }}" = "skipped" ]; then
echo "⏭️ Tauri builds skipped (CI lite mode enabled)"
else
echo "❌ Some Tauri builds failed."
echo "Please check the logs and fix any issues."
+1
View File
@@ -21,6 +21,7 @@ permissions:
jobs:
deploy:
if: ${{ vars.CI_PROFILE != 'lite' }}
runs-on: ubuntu-latest
steps:
- name: Harden Runner
@@ -112,7 +112,6 @@ public class ApplicationProperties {
@Data
public static class Security {
private Boolean enableLogin;
private Boolean csrfDisabled;
private InitialLogin initialLogin = new InitialLogin();
private OAUTH2 oauth2 = new OAUTH2();
private SAML2 saml2 = new SAML2();
@@ -575,6 +574,16 @@ public class ApplicationProperties {
private String username;
@ToString.Exclude private String password;
private String from;
// STARTTLS upgrades a plain SMTP connection to TLS after connecting (RFC 3207)
private Boolean startTlsEnable = true;
private Boolean startTlsRequired;
// SSL/TLS wrapper for implicit TLS (typically port 465)
private Boolean sslEnable;
// Hostnames or patterns (e.g., "smtp.example.com" or "*") to trust for TLS certificates;
// defaults to "*" (trust all) when not set
private String sslTrust;
// Enables hostname verification for TLS connections
private Boolean sslCheckServerIdentity;
}
@Data
@@ -254,10 +254,7 @@ public class PostHogService {
properties,
"security_enableLogin",
applicationProperties.getSecurity().getEnableLogin());
addIfNotEmpty(
properties,
"security_csrfDisabled",
applicationProperties.getSecurity().getCsrfDisabled());
addIfNotEmpty(properties, "security_csrfDisabled", true);
addIfNotEmpty(
properties,
"security_loginAttemptCount",
@@ -26,6 +26,7 @@ public class RequestUriUtils {
|| normalizedUri.startsWith("/public/")
|| normalizedUri.startsWith("/pdfjs/")
|| normalizedUri.startsWith("/pdfjs-legacy/")
|| normalizedUri.startsWith("/pdfium/")
|| normalizedUri.startsWith("/assets/")
|| normalizedUri.startsWith("/locales/")
|| normalizedUri.startsWith("/Login/")
@@ -61,7 +62,8 @@ public class RequestUriUtils {
|| normalizedUri.endsWith(".css")
|| normalizedUri.endsWith(".mjs")
|| normalizedUri.endsWith(".html")
|| normalizedUri.endsWith(".toml");
|| normalizedUri.endsWith(".toml")
|| normalizedUri.endsWith(".wasm");
}
public static boolean isFrontendRoute(String contextPath, String requestURI) {
@@ -125,11 +127,13 @@ public class RequestUriUtils {
|| requestURI.endsWith("popularity.txt")
|| requestURI.endsWith(".js")
|| requestURI.endsWith(".toml")
|| requestURI.endsWith(".wasm")
|| requestURI.contains("swagger")
|| requestURI.startsWith("/api/v1/info")
|| requestURI.startsWith("/site.webmanifest")
|| requestURI.startsWith("/fonts")
|| requestURI.startsWith("/pdfjs"));
|| requestURI.startsWith("/pdfjs")
|| requestURI.startsWith("/pdfium"));
}
/**
@@ -162,10 +166,9 @@ public class RequestUriUtils {
// enableLogin)
|| trimmedUri.startsWith(
"/api/v1/ui-data/footer-info") // Public footer configuration
|| trimmedUri.startsWith("/v1/api-docs")
|| trimmedUri.startsWith("/api/v1/invite/validate")
|| trimmedUri.startsWith("/api/v1/invite/accept")
|| trimmedUri.contains("/v1/api-docs");
|| trimmedUri.startsWith("/v1/api-docs");
}
private static String stripContextPath(String contextPath, String requestURI) {
@@ -24,6 +24,9 @@ public class RequestUriUtilsTest {
assertTrue(
RequestUriUtils.isStaticResource("/pdfjs/pdf.worker.js"),
"PDF.js files should be static");
assertTrue(
RequestUriUtils.isStaticResource("/pdfium/pdfium.wasm"),
"PDFium wasm should be static");
assertTrue(
RequestUriUtils.isStaticResource("/api/v1/info/status"),
"API status should be static");
@@ -110,7 +113,8 @@ public class RequestUriUtilsTest {
"/downloads/document.png",
"/assets/brand.ico",
"/any/path/with/image.svg",
"/deep/nested/folder/icon.png"
"/deep/nested/folder/icon.png",
"/pdfium/pdfium.wasm"
})
void testIsStaticResourceWithFileExtensions(String path) {
assertTrue(
@@ -148,6 +152,9 @@ public class RequestUriUtilsTest {
assertFalse(
RequestUriUtils.isTrackableResource("/script.js"),
"JS files should not be trackable");
assertFalse(
RequestUriUtils.isTrackableResource("/pdfium/pdfium.wasm"),
"PDFium wasm should not be trackable");
assertFalse(
RequestUriUtils.isTrackableResource("/swagger/index.html"),
"Swagger files should not be trackable");
@@ -224,7 +231,8 @@ public class RequestUriUtilsTest {
"/api/v1/info/health",
"/site.webmanifest",
"/fonts/roboto.woff",
"/pdfjs/viewer.js"
"/pdfjs/viewer.js",
"/pdfium/pdfium.wasm"
})
void testNonTrackableResources(String path) {
assertFalse(
@@ -34,7 +34,6 @@ public class InitialSetup {
public void init() throws IOException {
initUUIDKey();
initSecretKey();
initEnableCSRFSecurity();
initLegalUrls();
initSetAppVersion();
GeneralUtils.extractPipeline();
@@ -60,18 +59,6 @@ public class InitialSetup {
}
}
public void initEnableCSRFSecurity() throws IOException {
if (GeneralUtils.isVersionHigher(
"0.46.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) {
Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled();
if (!csrf) {
GeneralUtils.saveKeyToSettings("security.csrfDisabled", false);
GeneralUtils.saveKeyToSettings("system.enableAnalytics", true);
applicationProperties.getSecurity().setCsrfDisabled(false);
}
}
}
public void initLegalUrls() throws IOException {
// Initialize Terms and Conditions
String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
@@ -95,7 +82,7 @@ public class InitialSetup {
isNewServer =
existingVersion == null
|| existingVersion.isEmpty()
|| existingVersion.equals("0.0.0");
|| "0.0.0".equals(existingVersion);
String appVersion = "0.0.0";
Resource resource = new ClassPathResource("version.properties");
@@ -62,10 +62,15 @@ public class OpenApiConfig {
// Add server configuration from environment variable
String swaggerServerUrl = System.getenv("SWAGGER_SERVER_URL");
Server server;
if (swaggerServerUrl != null && !swaggerServerUrl.trim().isEmpty()) {
Server server = new Server().url(swaggerServerUrl).description("API Server");
openAPI.addServersItem(server);
server = new Server().url(swaggerServerUrl).description("API Server");
} else {
// Use relative path so Swagger uses the current browser origin to avoid CORS issues
// when accessing via different ports
server = new Server().url("/").description("Current Server");
}
openAPI.addServersItem(server);
// Add ErrorResponse schema to components
Schema<?> errorResponseSchema =
@@ -1,10 +1,14 @@
package stirling.software.SPDF.config;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import lombok.RequiredArgsConstructor;
@@ -25,6 +29,20 @@ public class WebMvcConfig implements WebMvcConfigurer {
registry.addInterceptor(endpointInterceptor);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Cache hashed assets (JS/CSS with content hashes) for 1 year
// These files have names like index-ChAS4tCC.js that change when content changes
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/static/assets/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic());
// Don't cache index.html - it needs to be fresh to reference latest hashed assets
registry.addResourceHandler("/index.html")
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.noCache().mustRevalidate());
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// Check if running in Tauri mode
@@ -124,7 +124,6 @@ public class SettingsController {
ApplicationProperties.Security security = applicationProperties.getSecurity();
settings.put("enableLogin", security.getEnableLogin());
settings.put("csrfDisabled", security.getCsrfDisabled());
settings.put("loginMethod", security.getLoginMethod());
settings.put("loginAttemptCount", security.getLoginAttemptCount());
settings.put("loginResetTimeMinutes", security.getLoginResetTimeMinutes());
@@ -159,12 +158,6 @@ public class SettingsController {
.getSecurity()
.setEnableLogin((Boolean) settings.get("enableLogin"));
}
if (settings.containsKey("csrfDisabled")) {
GeneralUtils.saveKeyToSettings("security.csrfDisabled", settings.get("csrfDisabled"));
applicationProperties
.getSecurity()
.setCsrfDisabled((Boolean) settings.get("csrfDisabled"));
}
if (settings.containsKey("loginMethod")) {
GeneralUtils.saveKeyToSettings("security.loginMethod", settings.get("loginMethod"));
applicationProperties
@@ -31,12 +31,10 @@ import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.JobOwnershipService;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.WebResponseUtils;
import stirling.software.proprietary.security.config.PremiumEndpoint;
@Slf4j
@ConvertApi
@RequiredArgsConstructor
@PremiumEndpoint
public class ConvertPdfJsonController {
private final PdfJsonConversionService pdfJsonConversionService;
@@ -74,6 +74,7 @@ public class ConfigController {
configData.put("appNameNavbar", applicationProperties.getUi().getAppNameNavbar());
configData.put("languages", applicationProperties.getUi().getLanguages());
configData.put("logoStyle", applicationProperties.getUi().getLogoStyle());
configData.put("defaultLocale", applicationProperties.getSystem().getDefaultLocale());
// Security settings
// enableLogin requires both the config flag AND proprietary features to be loaded
@@ -191,6 +191,12 @@ public class CertSignController {
switch (certType) {
case "PEM":
privateKeyFile =
validateFilePresent(
privateKeyFile, "PEM private key", "private key file is required");
certFile =
validateFilePresent(
certFile, "PEM certificate", "certificate file is required");
ks = KeyStore.getInstance("JKS");
ks.load(null);
PrivateKey privateKey = getPrivateKeyFromPEM(privateKeyFile.getBytes(), password);
@@ -200,10 +206,16 @@ public class CertSignController {
break;
case "PKCS12":
case "PFX":
p12File =
validateFilePresent(
p12File, "PKCS12 keystore", "PKCS12/PFX keystore file is required");
ks = KeyStore.getInstance("PKCS12");
ks.load(p12File.getInputStream(), password.toCharArray());
break;
case "JKS":
jksfile =
validateFilePresent(
jksfile, "JKS keystore", "JKS keystore file is required");
ks = KeyStore.getInstance("JKS");
ks.load(jksfile.getInputStream(), password.toCharArray());
break;
@@ -251,6 +263,17 @@ public class CertSignController {
GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_signed.pdf"));
}
private MultipartFile validateFilePresent(
MultipartFile file, String argumentName, String errorDescription) {
if (file == null || file.isEmpty()) {
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidArgument",
"Invalid argument: {0}",
argumentName + " - " + errorDescription);
}
return file;
}
private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password)
throws IOException, OperatorCreationException, PKCSException {
try (PEMParser pemParser =
@@ -4,8 +4,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
@@ -13,6 +11,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
@Controller
@@ -63,9 +62,10 @@ public class ReactRoutingController {
}
}
@GetMapping(value = {"/", "/index.html"}, produces = MediaType.TEXT_HTML_VALUE)
public ResponseEntity<String> serveIndexHtml(HttpServletRequest request)
throws IOException {
@GetMapping(
value = {"/", "/index.html"},
produces = MediaType.TEXT_HTML_VALUE)
public ResponseEntity<String> serveIndexHtml(HttpServletRequest request) throws IOException {
if (indexHtmlExists && cachedIndexHtml != null) {
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(cachedIndexHtml);
}
@@ -75,8 +75,7 @@ public class ReactRoutingController {
@GetMapping(
"/{path:^(?!api|static|robots\\.txt|favicon\\.ico|manifest.*\\.json|pipeline|pdfjs|pdfjs-legacy|fonts|images|files|css|js|assets|locales|modern-logo|classic-logo|Login|og_images|samples)[^\\.]*$}")
public ResponseEntity<String> forwardRootPaths(HttpServletRequest request)
throws IOException {
public ResponseEntity<String> forwardRootPaths(HttpServletRequest request) throws IOException {
return serveIndexHtml(request);
}
@@ -33,8 +33,12 @@ public class PdfJsonFallbackFontService {
public static final String FALLBACK_FONT_CJK_ID = "fallback-noto-cjk";
public static final String FALLBACK_FONT_JP_ID = "fallback-noto-jp";
public static final String FALLBACK_FONT_KR_ID = "fallback-noto-korean";
public static final String FALLBACK_FONT_TC_ID = "fallback-noto-tc";
public static final String FALLBACK_FONT_AR_ID = "fallback-noto-arabic";
public static final String FALLBACK_FONT_TH_ID = "fallback-noto-thai";
public static final String FALLBACK_FONT_DEVANAGARI_ID = "fallback-noto-devanagari";
public static final String FALLBACK_FONT_MALAYALAM_ID = "fallback-noto-malayalam";
public static final String FALLBACK_FONT_TIBETAN_ID = "fallback-noto-tibetan";
// Font name aliases map PDF font names to available fallback fonts
// This provides better visual consistency when editing PDFs
@@ -59,6 +63,22 @@ public class PdfJsonFallbackFontService {
Map.entry("dejavuserif", "fallback-dejavu-serif"),
Map.entry("dejavumono", "fallback-dejavu-mono"),
Map.entry("dejavusansmono", "fallback-dejavu-mono"),
// Traditional Chinese fonts (Taiwan, Hong Kong, Macau)
Map.entry("mingliu", "fallback-noto-tc"),
Map.entry("pmingliu", "fallback-noto-tc"),
Map.entry("microsoftjhenghei", "fallback-noto-tc"),
Map.entry("jhenghei", "fallback-noto-tc"),
Map.entry("kaiti", "fallback-noto-tc"),
Map.entry("kaiu", "fallback-noto-tc"),
Map.entry("dfkaib5", "fallback-noto-tc"),
Map.entry("dfkai", "fallback-noto-tc"),
// Simplified Chinese fonts (Mainland China) - more common
Map.entry("simsun", "fallback-noto-cjk"),
Map.entry("simhei", "fallback-noto-cjk"),
Map.entry("microsoftyahei", "fallback-noto-cjk"),
Map.entry("yahei", "fallback-noto-cjk"),
Map.entry("songti", "fallback-noto-cjk"),
Map.entry("heiti", "fallback-noto-cjk"),
// Noto Sans - Google's universal font (use as last resort generic fallback)
Map.entry("noto", "fallback-noto-sans"),
Map.entry("notosans", "fallback-noto-sans"));
@@ -83,6 +103,12 @@ public class PdfJsonFallbackFontService {
"classpath:/static/fonts/NotoSansKR-Regular.ttf",
"NotoSansKR-Regular",
"ttf")),
Map.entry(
FALLBACK_FONT_TC_ID,
new FallbackFontSpec(
"classpath:/static/fonts/NotoSansTC-Regular.ttf",
"NotoSansTC-Regular",
"ttf")),
Map.entry(
FALLBACK_FONT_AR_ID,
new FallbackFontSpec(
@@ -95,6 +121,24 @@ public class PdfJsonFallbackFontService {
"classpath:/static/fonts/NotoSansThai-Regular.ttf",
"NotoSansThai-Regular",
"ttf")),
Map.entry(
FALLBACK_FONT_DEVANAGARI_ID,
new FallbackFontSpec(
"classpath:/static/fonts/NotoSansDevanagari-Regular.ttf",
"NotoSansDevanagari-Regular",
"ttf")),
Map.entry(
FALLBACK_FONT_MALAYALAM_ID,
new FallbackFontSpec(
"classpath:/static/fonts/NotoSansMalayalam-Regular.ttf",
"NotoSansMalayalam-Regular",
"ttf")),
Map.entry(
FALLBACK_FONT_TIBETAN_ID,
new FallbackFontSpec(
"classpath:/static/fonts/NotoSerifTibetan-Regular.ttf",
"NotoSerifTibetan-Regular",
"ttf")),
// Liberation Sans family
Map.entry(
"fallback-liberation-sans",
@@ -484,6 +528,20 @@ public class PdfJsonFallbackFontService {
*/
public String resolveFallbackFontId(int codePoint) {
Character.UnicodeBlock block = Character.UnicodeBlock.of(codePoint);
// Bopomofo is primarily used in Taiwan for Traditional Chinese phonetic annotation
if (block == Character.UnicodeBlock.BOPOMOFO
|| block == Character.UnicodeBlock.BOPOMOFO_EXTENDED) {
return FALLBACK_FONT_TC_ID;
}
// Compatibility ideographs are primarily used by Traditional Chinese encodings (e.g., Big5,
// HKSCS) so prefer the Traditional Chinese fallback here.
if (block == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
|| block == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT) {
return FALLBACK_FONT_TC_ID;
}
if (block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
|| block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
|| block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B
@@ -492,19 +550,23 @@ public class PdfJsonFallbackFontService {
|| block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_E
|| block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_F
|| block == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION
|| block == Character.UnicodeBlock.BOPOMOFO
|| block == Character.UnicodeBlock.BOPOMOFO_EXTENDED
|| block == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS) {
return FALLBACK_FONT_CJK_ID;
}
Character.UnicodeScript script = Character.UnicodeScript.of(codePoint);
return switch (script) {
// HAN script is used by both Simplified and Traditional Chinese
// Default to Simplified (mainland China, 1.4B speakers) as it's more common
// Traditional Chinese PDFs are detected via font name aliases (MingLiU, PMingLiU, etc.)
case HAN -> FALLBACK_FONT_CJK_ID;
case HIRAGANA, KATAKANA -> FALLBACK_FONT_JP_ID;
case HANGUL -> FALLBACK_FONT_KR_ID;
case ARABIC -> FALLBACK_FONT_AR_ID;
case THAI -> FALLBACK_FONT_TH_ID;
case DEVANAGARI -> FALLBACK_FONT_DEVANAGARI_ID;
case MALAYALAM -> FALLBACK_FONT_MALAYALAM_ID;
case TIBETAN -> FALLBACK_FONT_TIBETAN_ID;
default -> FALLBACK_FONT_ID;
};
}
@@ -12,7 +12,6 @@
security:
enableLogin: true # set to 'true' to enable login
csrfDisabled: false # set to 'true' to disable CSRF protection (not recommended for production)
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
loginMethod: all # Accepts values like 'all' and 'normal'(only Login with Username/Password), 'oauth2'(only Login with OAuth2) or 'saml2'(only Login with SAML2)
@@ -105,6 +104,11 @@ mail:
username: '' # SMTP server username
password: '' # SMTP server password
from: '' # sender email address
startTlsEnable: true # enable STARTTLS (explicit TLS upgrade after connecting) when supported by the SMTP server
startTlsRequired: false # require STARTTLS; connection fails if the upgrade command is not supported
sslEnable: false # enable SSL/TLS wrapper for implicit TLS (typically used with port 465)
sslTrust: '' # optional trusted host override, e.g. "smtp.example.com" or "*"; defaults to "*" (trust all) when empty
sslCheckServerIdentity: false # enable hostname verification when using SSL/TLS
legal:
termsAndConditions: https://www.stirling.com/legal/terms-of-service # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder
@@ -1,9 +1,10 @@
package stirling.software.SPDF.controller.api.security;
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.Mockito.when;
import static org.mockito.Mockito.lenient;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
@@ -107,7 +108,8 @@ class CertSignControllerTest {
derCertBytes = baos.toByteArray();
}
when(pdfDocumentFactory.load(any(MultipartFile.class)))
lenient()
.when(pdfDocumentFactory.load(any(MultipartFile.class)))
.thenAnswer(
invocation -> {
MultipartFile file = invocation.getArgument(0);
@@ -167,6 +169,31 @@ class CertSignControllerTest {
assertTrue(response.getBody().length > 0);
}
@Test
void testSignPdfWithMissingPkcs12FileThrowsError() {
MockMultipartFile pdfFile =
new MockMultipartFile(
"fileInput", "test.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
SignPDFWithCertRequest request = new SignPDFWithCertRequest();
request.setFileInput(pdfFile);
request.setCertType("PFX");
request.setPassword("password");
request.setShowSignature(false);
request.setReason("test");
request.setLocation("test");
request.setName("tester");
request.setPageNumber(1);
request.setShowLogo(false);
IllegalArgumentException exception =
assertThrows(
IllegalArgumentException.class,
() -> certSignController.signPDFWithCert(request));
assertTrue(exception.getMessage().contains("PKCS12 keystore"));
}
@Test
void testSignPdfWithJks() throws Exception {
MockMultipartFile pdfFile =
@@ -205,6 +205,10 @@ public class ProprietaryUIDataController {
data.setLoginMethod(securityProps.getLoginMethod());
data.setAltLogin(!providerList.isEmpty() && securityProps.isAltLogin());
// Add language configuration for login page
data.setLanguages(applicationProperties.getUi().getLanguages());
data.setDefaultLocale(applicationProperties.getSystem().getDefaultLocale());
return ResponseEntity.ok(data);
}
@@ -328,6 +332,7 @@ public class ProprietaryUIDataController {
data.setGrandfatheredUserCount(grandfatheredCount);
data.setLicenseMaxUsers(licenseMaxUsers);
data.setPremiumEnabled(premiumEnabled);
data.setMailEnabled(applicationProperties.getMail().isEnabled());
return ResponseEntity.ok(data);
}
@@ -376,7 +381,7 @@ public class ProprietaryUIDataController {
data.setUsername(username);
data.setRole(user.get().getRolesAsString());
data.setSettings(settingsJson);
data.setChangeCredsFlag(user.get().isFirstLogin());
data.setChangeCredsFlag(user.get().isFirstLogin() || user.get().isForcePasswordChange());
data.setOAuth2Login(isOAuth2Login);
data.setSaml2Login(isSaml2Login);
@@ -491,6 +496,8 @@ public class ProprietaryUIDataController {
private boolean altLogin;
private boolean firstTimeSetup;
private boolean showDefaultCredentials;
private List<String> languages;
private String defaultLocale;
}
@Data
@@ -510,6 +517,7 @@ public class ProprietaryUIDataController {
private int grandfatheredUserCount;
private int licenseMaxUsers;
private boolean premiumEnabled;
private boolean mailEnabled;
}
@Data
@@ -33,7 +33,8 @@ public class MailConfig {
// Creates a new instance of JavaMailSenderImpl, which is a Spring implementation
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(mailProperties.getHost());
String host = mailProperties.getHost();
mailSender.setHost(host);
mailSender.setPort(mailProperties.getPort());
mailSender.setDefaultEncoding("UTF-8");
@@ -70,8 +71,32 @@ public class MailConfig {
log.info("SMTP authentication disabled - no credentials provided");
}
boolean startTlsEnabled =
mailProperties.getStartTlsEnable() == null || mailProperties.getStartTlsEnable();
// Enables STARTTLS to encrypt the connection if supported by the SMTP server
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.starttls.enable", Boolean.toString(startTlsEnabled));
if (mailProperties.getStartTlsRequired() != null) {
props.put(
"mail.smtp.starttls.required", mailProperties.getStartTlsRequired().toString());
}
if (mailProperties.getSslEnable() != null) {
props.put("mail.smtp.ssl.enable", mailProperties.getSslEnable().toString());
}
// Trust the configured host to allow STARTTLS with self-signed certificates
String sslTrust = mailProperties.getSslTrust();
if (sslTrust == null || sslTrust.trim().isEmpty()) {
sslTrust = "*";
}
if (sslTrust != null && !sslTrust.trim().isEmpty()) {
props.put("mail.smtp.ssl.trust", sslTrust);
}
if (mailProperties.getSslCheckServerIdentity() != null) {
props.put(
"mail.smtp.ssl.checkserveridentity",
mailProperties.getSslCheckServerIdentity().toString());
}
// Returns the configured mail sender, ready to send emails
return mailSender;
@@ -1,7 +1,6 @@
package stirling.software.proprietary.security.configuration;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -25,8 +24,6 @@ import org.springframework.security.saml2.provider.service.web.authentication.Op
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
@@ -47,7 +44,6 @@ import stirling.software.proprietary.security.database.repository.PersistentLogi
import stirling.software.proprietary.security.filter.IPRateLimitingFilter;
import stirling.software.proprietary.security.filter.JwtAuthenticationFilter;
import stirling.software.proprietary.security.filter.UserAuthenticationFilter;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.proprietary.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticationFailureHandler;
@@ -198,74 +194,19 @@ public class SecurityConfiguration {
http.cors(cors -> cors.disable());
}
if (securityProperties.getCsrfDisabled() || !loginEnabledValue) {
http.csrf(CsrfConfigurer::disable);
}
http.csrf(CsrfConfigurer::disable);
if (loginEnabledValue) {
boolean v2Enabled = appConfig.v2Enabled();
http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(rateLimitingFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtAuthenticationFilter, UserAuthenticationFilter.class);
if (!securityProperties.getCsrfDisabled()) {
CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler =
new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName(null);
http.csrf(
csrf ->
csrf.ignoringRequestMatchers(
request -> {
String uri = request.getRequestURI();
// Ignore CSRF for auth endpoints
if (uri.startsWith("/api/v1/auth/")) {
return true;
}
String apiKey = request.getHeader("X-API-KEY");
// If there's no API key, don't ignore CSRF
// (return false)
if (apiKey == null || apiKey.trim().isEmpty()) {
return false;
}
// Validate API key using existing UserService
try {
Optional<User> user =
userService.getUserByApiKey(apiKey);
// If API key is valid, ignore CSRF (return
// true)
// If API key is invalid, don't ignore CSRF
// (return false)
return user.isPresent();
} catch (Exception e) {
// If there's any error validating the API
// key, don't ignore CSRF
return false;
}
})
.csrfTokenRepository(cookieRepo)
.csrfTokenRequestHandler(requestHandler));
}
http.sessionManagement(
sessionManagement -> {
if (v2Enabled) {
sessionManagement ->
sessionManagement.sessionCreationPolicy(
SessionCreationPolicy.STATELESS);
} else {
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(10)
.maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry)
.expiredUrl("/login?logout=true");
}
});
SessionCreationPolicy.STATELESS));
http.authenticationProvider(daoAuthenticationProvider());
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
@@ -331,7 +272,9 @@ public class SecurityConfiguration {
formLogin ->
formLogin
.loginPage("/login") // Redirect here when unauthenticated
.loginProcessingUrl("/perform_login") // Process form posts here (not /login)
.loginProcessingUrl(
"/perform_login") // Process form posts here (not
// /login)
.successHandler(
new CustomAuthenticationSuccessHandler(
loginAttemptService,
@@ -346,18 +289,7 @@ public class SecurityConfiguration {
if (securityProperties.isOauth2Active()) {
http.oauth2Login(
oauth2 -> {
// v1: Use /oauth2 as login page for Thymeleaf templates
if (!v2Enabled) {
oauth2.loginPage("/oauth2");
}
// v2: Don't set loginPage, let default OAuth2 flow handle it
oauth2
/*
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
If user exists, login proceeds as usual. If user does not exist, then it is auto-created but only if 'OAUTH2AutoCreateUser'
is set as true, else login fails with an error message advising the same.
*/
oauth2.loginPage("/login")
.successHandler(
new CustomOAuth2AuthenticationSuccessHandler(
loginAttemptService,
@@ -391,12 +323,8 @@ public class SecurityConfiguration {
.saml2Login(
saml2 -> {
try {
// Only set login page for v1/Thymeleaf mode
if (!v2Enabled) {
saml2.loginPage("/saml2");
}
saml2.relyingPartyRegistrationRepository(
saml2.loginPage("/login")
.relyingPartyRegistrationRepository(
saml2RelyingPartyRegistrations)
.authenticationManager(
new ProviderManager(authenticationProvider))
@@ -7,6 +7,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -18,6 +19,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.bind.annotation.*;
import jakarta.mail.MessagingException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.transaction.Transactional;
@@ -236,6 +238,8 @@ public class UserController {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "incorrectPassword", "message", "Incorrect password"));
}
// Set flags before changing password so they're saved together
user.setForcePasswordChange(false);
userService.changePassword(user, newPassword);
userService.changeFirstUse(user, false);
// Logout using Spring's utility
@@ -584,6 +588,79 @@ public class UserController {
return ResponseEntity.ok(Map.of("message", "User role updated successfully"));
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/changePasswordForUser")
public ResponseEntity<?> changePasswordForUser(
@RequestParam(name = "username") String username,
@RequestParam(name = "newPassword", required = false) String newPassword,
@RequestParam(name = "generateRandom", defaultValue = "false") boolean generateRandom,
@RequestParam(name = "sendEmail", defaultValue = "false") boolean sendEmail,
@RequestParam(name = "includePassword", defaultValue = "false") boolean includePassword,
@RequestParam(name = "forcePasswordChange", defaultValue = "false")
boolean forcePasswordChange,
HttpServletRequest request,
Authentication authentication)
throws SQLException, UnsupportedProviderException, MessagingException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (userOpt.isEmpty()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("error", "User not found."));
}
String currentUsername = authentication.getName();
if (currentUsername.equalsIgnoreCase(username)) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", "Cannot change your own password."));
}
User user = userOpt.get();
String finalPassword = newPassword;
if (generateRandom) {
finalPassword = UUID.randomUUID().toString().replace("-", "").substring(0, 12);
}
if (finalPassword == null || finalPassword.trim().isEmpty()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", "New password is required."));
}
// Set force password change flag before changing password so both are saved together
user.setForcePasswordChange(forcePasswordChange);
userService.changePassword(user, finalPassword);
// Invalidate all active sessions to force reauthentication
userService.invalidateUserSessions(username);
if (sendEmail) {
if (emailService.isEmpty() || !applicationProperties.getMail().isEnabled()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", "Email is not configured."));
}
String userEmail = user.getUsername();
// Check if username is a valid email format
if (userEmail == null || userEmail.isBlank() || !userEmail.contains("@")) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(
Map.of(
"error",
"User's email is not a valid email address. Notifications are disabled."));
}
String loginUrl = buildLoginUrl(request);
emailService
.get()
.sendPasswordChangedNotification(
userEmail,
user.getUsername(),
includePassword ? finalPassword : null,
loginUrl);
}
return ResponseEntity.ok(Map.of("message", "User password updated successfully"));
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/changeUserEnabled/{username}")
public ResponseEntity<?> changeUserEnabled(
@@ -26,6 +26,7 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
@@ -39,6 +40,7 @@ import stirling.software.proprietary.security.service.JwtServiceInterface;
import stirling.software.proprietary.security.service.UserService;
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtServiceInterface jwtService;
@@ -47,19 +49,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final AuthenticationEntryPoint authenticationEntryPoint;
private final ApplicationProperties.Security securityProperties;
public JwtAuthenticationFilter(
JwtServiceInterface jwtService,
UserService userService,
CustomUserDetailsService userDetailsService,
AuthenticationEntryPoint authenticationEntryPoint,
ApplicationProperties.Security securityProperties) {
this.jwtService = jwtService;
this.userService = userService;
this.userDetailsService = userDetailsService;
this.authenticationEntryPoint = authenticationEntryPoint;
this.securityProperties = securityProperties;
}
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
@@ -68,7 +57,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response);
return;
}
if (isStaticResource(request.getContextPath(), request.getRequestURI())) {
String requestURI = request.getRequestURI();
String contextPath = request.getContextPath();
if (isStaticResource(contextPath, requestURI)) {
filterChain.doFilter(request, response);
return;
}
@@ -77,10 +70,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
String jwtToken = jwtService.extractToken(request);
if (jwtToken == null) {
// Allow specific auth endpoints to pass through without JWT
String requestURI = request.getRequestURI();
String contextPath = request.getContextPath();
// Allow auth endpoints to pass through without JWT
if (!isPublicAuthEndpoint(requestURI, contextPath)) {
// For API requests, return 401 JSON
String acceptHeader = request.getHeader("Accept");
@@ -241,24 +241,6 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response);
}
private static boolean isPublicAuthEndpoint(String requestURI, String contextPath) {
// Remove context path from URI to normalize path matching
String trimmedUri =
requestURI.startsWith(contextPath)
? requestURI.substring(contextPath.length())
: requestURI;
// Public auth endpoints that don't require authentication
return trimmedUri.startsWith("/login")
|| trimmedUri.startsWith("/auth/")
|| trimmedUri.startsWith("/oauth2")
|| trimmedUri.startsWith("/saml2")
|| trimmedUri.startsWith("/api/v1/auth/login")
|| trimmedUri.startsWith("/api/v1/auth/refresh")
|| trimmedUri.startsWith("/api/v1/auth/logout")
|| trimmedUri.startsWith("/api/v1/proprietary/ui-data/login");
}
private enum UserLoginType {
USERDETAILS("UserDetails"),
OAUTH2USER("OAuth2User"),
@@ -59,6 +59,9 @@ public class User implements UserDetails, Serializable {
@Column(name = "hasCompletedInitialSetup")
private Boolean hasCompletedInitialSetup = false;
@Column(name = "forcePasswordChange")
private Boolean forcePasswordChange = false;
@Column(name = "roleName")
private String roleName;
@@ -117,6 +120,14 @@ public class User implements UserDetails, Serializable {
this.hasCompletedInitialSetup = hasCompletedInitialSetup;
}
public boolean isForcePasswordChange() {
return forcePasswordChange != null && forcePasswordChange;
}
public void setForcePasswordChange(boolean forcePasswordChange) {
this.forcePasswordChange = forcePasswordChange;
}
public void setAuthenticationType(AuthenticationType authenticationType) {
this.authenticationType = authenticationType.toString().toLowerCase();
}
@@ -27,6 +27,7 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.exception.UnsupportedProviderException;
@@ -39,6 +40,7 @@ import stirling.software.proprietary.security.service.JwtServiceInterface;
import stirling.software.proprietary.security.service.LoginAttemptService;
import stirling.software.proprietary.security.service.UserService;
@Slf4j
@RequiredArgsConstructor
public class CustomOAuth2AuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
@@ -77,12 +79,18 @@ public class CustomOAuth2AuthenticationSuccessHandler
if (user != null && !licenseSettingsService.isOAuthEligible(user)) {
// User is not grandfathered and no paid license - block OAuth login
log.warn(
"OAuth login blocked for existing user '{}' - not eligible (not grandfathered and no paid license)",
username);
response.sendRedirect(
request.getContextPath() + "/logout?oAuth2RequiresLicense=true");
return;
}
} else if (!licenseSettingsService.isOAuthEligible(null)) {
// No existing user and no paid license -> block auto creation
log.warn(
"OAuth login blocked for new user '{}' - not eligible (no paid license for auto-creation)",
username);
response.sendRedirect(request.getContextPath() + "/logout?oAuth2RequiresLicense=true");
return;
}
@@ -67,10 +67,15 @@ public class OAuth2Configuration {
keycloakClientRegistration().ifPresent(registrations::add);
if (registrations.isEmpty()) {
log.error("No OAuth2 provider registered");
log.error("No OAuth2 provider registered - check your OAuth2 configuration");
throw new NoProviderFoundException("At least one OAuth2 provider must be configured.");
}
log.info(
"OAuth2 ClientRegistrationRepository created with {} provider(s): {}",
registrations.size(),
registrations.stream().map(ClientRegistration::getRegistrationId).toList());
return new InMemoryClientRegistrationRepository(registrations);
}
@@ -165,7 +170,6 @@ public class OAuth2Configuration {
githubClient.getUseAsUsername());
boolean isValid = validateProvider(github);
log.info("Initialised GitHub OAuth2 provider");
return isValid
? Optional.of(
@@ -208,7 +212,19 @@ public class OAuth2Configuration {
null,
null);
return !isStringEmpty(oidcProvider.getIssuer()) || validateProvider(oidcProvider)
boolean isValid =
!isStringEmpty(oidcProvider.getIssuer()) || validateProvider(oidcProvider);
if (isValid) {
log.info(
"Initialised OIDC OAuth2 provider: registrationId='{}', issuer='{}', redirectUri='{}'",
name,
oauth.getIssuer(),
REDIRECT_URI_PATH + name);
} else {
log.warn("OIDC OAuth2 provider validation failed - provider will not be registered");
}
return isValid
? Optional.of(
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
.registrationId(name)
@@ -217,7 +233,7 @@ public class OAuth2Configuration {
.scope(oidcProvider.getScopes())
.userNameAttributeName(oidcProvider.getUseAsUsername().getName())
.clientName(clientName)
.redirectUri(REDIRECT_URI_PATH + "oidc")
.redirectUri(REDIRECT_URI_PATH + name)
.authorizationGrantType(AUTHORIZATION_CODE)
.build())
: Optional.empty();
@@ -67,19 +67,25 @@ public class CustomSaml2AuthenticationSuccessHandler
boolean userExists = userService.usernameExistsIgnoreCase(username);
// Check if user is eligible for SAML (grandfathered or system has paid license)
// Check if user is eligible for SAML (grandfathered or system has ENTERPRISE license)
if (userExists) {
stirling.software.proprietary.security.model.User user =
userService.findByUsernameIgnoreCase(username).orElse(null);
if (user != null && !licenseSettingsService.isOAuthEligible(user)) {
// User is not grandfathered and no paid license - block SAML login
if (user != null && !licenseSettingsService.isSamlEligible(user)) {
// User is not grandfathered and no ENTERPRISE license - block SAML login
log.warn(
"SAML2 login blocked for existing user '{}' - not eligible (not grandfathered and no ENTERPRISE license)",
username);
response.sendRedirect(
request.getContextPath() + "/logout?saml2RequiresLicense=true");
return;
}
} else if (!licenseSettingsService.isOAuthEligible(null)) {
// No existing user and no paid license -> block auto creation
} else if (!licenseSettingsService.isSamlEligible(null)) {
// No existing user and no ENTERPRISE license -> block auto creation
log.warn(
"SAML2 login blocked for new user '{}' - not eligible (no ENTERPRISE license for auto-creation)",
username);
response.sendRedirect(
request.getContextPath() + "/logout?saml2RequiresLicense=true");
return;
@@ -223,4 +223,54 @@ public class EmailService {
sendPlainEmail(to, subject, body, true);
}
@Async
public void sendPasswordChangedNotification(
String to, String username, String newPassword, String loginUrl)
throws MessagingException {
String subject = "Your Stirling PDF password has been updated";
String passwordSection =
newPassword == null
? ""
: """
<div style=\"background-color: #f8f9fa; border-left: 4px solid #007bff; padding: 15px; margin: 20px 0; border-radius: 4px;\">
<p style=\"margin: 0;\"><strong>Temporary Password:</strong> %s</p>
</div>
"""
.formatted(newPassword);
String body =
"""
<html><body style=\"margin: 0; padding: 0;\">
<div style=\"font-family: Arial, sans-serif; background-color: #f8f9fa; padding: 20px;\">
<div style=\"max-width: 600px; margin: auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; border: 1px solid #e0e0e0;\">
<div style=\"text-align: center; padding: 20px; background-color: #222;\">
<img src=\"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling-transparent.svg\" alt=\"Stirling PDF\" style=\"max-height: 60px;\">
</div>
<div style=\"padding: 30px; color: #333;\">
<h2 style=\"color: #222; margin-top: 0;\">Your password was changed</h2>
<p>Hello %s,</p>
<p>An administrator has updated the password for your Stirling PDF account.</p>
%s
<p>If you did not expect this change, please contact your administrator immediately.</p>
<div style=\"text-align: center; margin: 30px 0;\">
<a href=\"%s\" style=\"display: inline-block; background-color: #007bff; color: #ffffff; padding: 14px 28px; text-decoration: none; border-radius: 5px; font-weight: bold;\">Go to Stirling PDF</a>
</div>
<p style=\"font-size: 14px; color: #666;\">Or copy and paste this link in your browser:</p>
<div style=\"background-color: #f8f9fa; padding: 12px; margin: 15px 0; border-radius: 4px; word-break: break-all; font-size: 13px; color: #555;\">
%s
</div>
</div>
<div style=\"text-align: center; padding: 15px; font-size: 12px; color: #777; background-color: #f0f0f0;\">
&copy; 2025 Stirling PDF. All rights reserved.
</div>
</div>
</div>
</body></html>
"""
.formatted(username, passwordSection, loginUrl, loginUrl);
sendPlainEmail(to, subject, body, true);
}
}
@@ -21,6 +21,7 @@ import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.model.UserLicenseSettings;
import stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License;
import stirling.software.proprietary.security.configuration.ee.LicenseKeyChecker;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.repository.UserLicenseSettingsRepository;
import stirling.software.proprietary.security.service.UserService;
@@ -343,17 +344,76 @@ public class UserLicenseSettingsService {
* @param user The user to check
* @return true if the user can use OAuth/SAML
*/
public boolean isOAuthEligible(stirling.software.proprietary.security.model.User user) {
public boolean isOAuthEligible(User user) {
String username = (user != null) ? user.getUsername() : "<new user>";
log.info("OAuth eligibility check for user: {}", username);
// Grandfathered users always have OAuth access
if (user != null && user.isOauthGrandfathered()) {
log.debug("User {} is grandfathered for OAuth", user.getUsername());
return true;
}
// Users can use OAuth/SAML only if system has ENTERPRISE license
boolean hasEnterpriseLicense = hasEnterpriseLicense();
log.debug("OAuth eligibility check: hasEnterpriseLicense={}", hasEnterpriseLicense);
return hasEnterpriseLicense;
// todo: remove
if (user != null) {
log.info(
"User {} is NOT grandfathered (isOauthGrandfathered={})",
username,
user.isOauthGrandfathered());
} else {
log.info("New user attempting OAuth login - checking license requirement");
}
// Users can use OAuth with SERVER or ENTERPRISE license
boolean hasPaid = hasPaidLicense();
log.info(
"OAuth eligibility result: hasPaidLicense={}, user={}, eligible={}",
hasPaid,
username,
hasPaid);
return hasPaid;
}
/**
* Checks if a user is eligible to use SAML authentication.
*
* <p>A user is eligible if:
*
* <ul>
* <li>They are grandfathered for OAuth (existing user before policy change), OR
* <li>The system has an ENTERPRISE license (SAML is enterprise-only)
* </ul>
*
* @param user The user to check
* @return true if the user can use SAML
*/
public boolean isSamlEligible(User user) {
String username = (user != null) ? user.getUsername() : "<new user>";
log.info("SAML2 eligibility check for user: {}", username);
// Grandfathered users always have SAML access
if (user != null && user.isOauthGrandfathered()) {
log.info("User {} is grandfathered for SAML2 - ELIGIBLE", username);
return true;
}
if (user != null) {
log.info(
"User {} is NOT grandfathered (isOauthGrandfathered={})",
username,
user.isOauthGrandfathered());
} else {
log.info("New user attempting SAML2 login - checking license requirement");
}
// Users can use SAML only with ENTERPRISE license
boolean hasEnterprise = hasEnterpriseLicense();
log.info(
"SAML2 eligibility result: hasEnterpriseLicense={}, user={}, eligible={}",
hasEnterprise,
username,
hasEnterprise);
return hasEnterprise;
}
/**
@@ -495,8 +555,12 @@ public class UserLicenseSettingsService {
if (checker == null) {
return false;
}
License license = checker.getPremiumLicenseEnabledResult();
return license == License.SERVER || license == License.ENTERPRISE;
boolean hasPaid = (license == License.SERVER || license == License.ENTERPRISE);
log.info("License check result: type={}, requiresPaid=true, hasPaid={}", license, hasPaid);
return hasPaid;
}
/**
@@ -510,7 +574,19 @@ public class UserLicenseSettingsService {
if (checker == null) {
return false;
}
License license = checker.getPremiumLicenseEnabledResult();
log.info(
"License check result: type={}, requiresEnterprise=true, hasEnterprise={}",
license,
(license == License.ENTERPRISE));
if (license != License.ENTERPRISE) {
log.warn(
"SAML2 requires ENTERPRISE license but found: {}. SAML2 login will be blocked.",
license);
}
return license == License.ENTERPRISE;
}
}
@@ -0,0 +1,162 @@
package stirling.software.proprietary.security.oauth2;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;
/**
* Unit tests for OAuth2Configuration redirect URI logic.
*
* <p>These tests validate the critical fix for GitHub issue #5141: The redirect URI path segment
* MUST match the registration ID. Previously, the redirect URI was hardcoded to 'oidc', causing
* InvalidClientRegistrationIdException when custom provider names were used.
*
* <p>Note: These are conceptual tests documenting the expected behavior. Full integration testing
* with actual OIDC discovery would require: 1. Mock HTTP server for OIDC discovery endpoints 2.
* Valid OIDC configuration responses 3. Network mocking infrastructure
*/
class OAuth2ConfigurationTest {
/**
* Tests the redirect URI pattern for OIDC provider configurations.
*
* <p>Critical behavior (GitHub issue #5141 fix): The redirect URI path segment MUST match the
* registration ID. For example: - Provider name: "authentik" Redirect URI:
* "/login/oauth2/code/authentik" - Provider name: "mycompany" Redirect URI:
* "/login/oauth2/code/mycompany" - Provider name: "oidc" Redirect URI:
* "/login/oauth2/code/oidc"
*
* <p>Previously, the redirect URI was hardcoded to 'oidc', causing Spring Security to look for
* a registration with ID 'oidc' when the provider redirected back. This caused
* InvalidClientRegistrationIdException when custom provider names were used.
*/
@Test
void testRedirectUriPattern_usesProviderNameNotHardcodedOidc() {
// Verify the redirect URI pattern constant
String redirectUriBase = "{baseUrl}/login/oauth2/code/";
// Test cases: provider name expected redirect URI
String[][] testCases = {
{"authentik", redirectUriBase + "authentik"},
{"mycompany", redirectUriBase + "mycompany"},
{"oidc", redirectUriBase + "oidc"},
{"okta", redirectUriBase + "okta"},
{"auth0", redirectUriBase + "auth0"}
};
for (String[] testCase : testCases) {
String providerName = testCase[0];
String expectedRedirectUri = testCase[1];
// The fix ensures: .redirectUri(REDIRECT_URI_PATH + name)
// instead of: .redirectUri(REDIRECT_URI_PATH + "oidc")
String actualRedirectUri = redirectUriBase + providerName;
assertEquals(
expectedRedirectUri,
actualRedirectUri,
String.format(
"Redirect URI for provider '%s' must use provider name, not hardcoded 'oidc'",
providerName));
}
}
/**
* Documents the critical fix for OAuth2 redirect URI mismatch.
*
* <p>This test validates the logic that was changed in OAuth2Configuration.java line 220:
*
* <pre>
* // BEFORE (bug):
* .redirectUri(REDIRECT_URI_PATH + "oidc") // Always "oidc"
*
* // AFTER (fix):
* .redirectUri(REDIRECT_URI_PATH + name) // Dynamic provider name
* </pre>
*/
@Test
void testCriticalFix_redirectUriMatchesRegistrationId() {
// The redirect URI path segment extraction by Spring Security
String callbackUrl = "http://localhost:8080/login/oauth2/code/authentik?code=abc123";
// Spring extracts the path segment between "code/" and "?"
String extractedRegistrationId = extractRegistrationIdFromCallback(callbackUrl);
// The extracted ID MUST match an actual registration ID
assertEquals("authentik", extractedRegistrationId);
// If we had used hardcoded "oidc", the callback would be:
String buggyCallbackUrl = "http://localhost:8080/login/oauth2/code/oidc?code=abc123";
String buggyExtractedId = extractRegistrationIdFromCallback(buggyCallbackUrl);
// This would look for registration with ID "oidc" but we registered "authentik"
assertEquals("oidc", buggyExtractedId);
// The mismatch: registrationId="authentik", but Spring looks for "oidc"
// Result: InvalidClientRegistrationIdException
assertNotNull(buggyExtractedId, "This demonstrates the bug that was fixed");
}
/** Helper method simulating Spring's extraction of registration ID from callback URL */
private String extractRegistrationIdFromCallback(String callbackUrl) {
// Simplified version of what Spring Security does
// Actual: OAuth2AuthorizationRequestRedirectFilter extracts from path
String path = callbackUrl.split("\\?")[0];
String[] parts = path.split("/");
return parts[parts.length - 1]; // Last path segment
}
/**
* Validates the frontend-backend flow for custom provider names.
*
* <p>Complete flow: 1. Backend: Provider configured as "authentik" in settings.yml 2. Backend:
* ClientRegistration created with registrationId="authentik" 3. Backend: Redirect URI set to
* "{baseUrl}/login/oauth2/code/authentik" 4. Backend: Login endpoint returns providerList with
* "/oauth2/authorization/authentik" 5. Frontend: Extracts "authentik" from path and uses it for
* OAuth login 6. Frontend: Redirects to "/oauth2/authorization/authentik" 7. Backend: Spring
* Security redirects to provider with redirect_uri containing "authentik" 8. Provider:
* Redirects back to "/login/oauth2/code/authentik?code=..." 9. Backend: Spring Security
* extracts "authentik" from callback URL 10. Backend: Looks up ClientRegistration with ID
* "authentik" SUCCESS
*
* <p>If redirect URI was hardcoded to "oidc" (the bug): Step 7: Provider redirects to
* "/login/oauth2/code/oidc?code=..." Step 9: Spring Security looks for registration ID "oidc"
* Step 10: FAIL - No registration found with ID "oidc" (we registered "authentik") Result:
* InvalidClientRegistrationIdException
*/
@Test
void testEndToEndFlow_registrationIdConsistency() {
String providerName = "authentik";
// Step 2: Registration ID
String registrationId = providerName;
assertEquals("authentik", registrationId);
// Step 3: Redirect URI (MUST use same name)
String redirectUri = "{baseUrl}/login/oauth2/code/" + providerName;
assertEquals("{baseUrl}/login/oauth2/code/authentik", redirectUri);
// Step 4: Provider list endpoint
String authorizationPath = "/oauth2/authorization/" + providerName;
assertEquals("/oauth2/authorization/authentik", authorizationPath);
// Step 5: Frontend extracts provider ID
String frontendProviderId =
authorizationPath.substring(authorizationPath.lastIndexOf('/') + 1);
assertEquals("authentik", frontendProviderId);
// Step 6-8: OAuth flow (external)
// Step 9: Callback URL from provider
String callbackUrl =
"http://localhost:8080/login/oauth2/code/" + providerName + "?code=abc123";
String extractedId = extractRegistrationIdFromCallback(callbackUrl);
// Step 10: Registration lookup
assertEquals(
registrationId,
extractedId,
"Registration ID from callback MUST match original registration ID");
}
}
@@ -27,6 +27,11 @@ class MailConfigTest {
when(mailProps.getPort()).thenReturn(587);
when(mailProps.getUsername()).thenReturn("user@example.com");
when(mailProps.getPassword()).thenReturn("password");
when(mailProps.getStartTlsEnable()).thenReturn(null);
when(mailProps.getStartTlsRequired()).thenReturn(null);
when(mailProps.getSslEnable()).thenReturn(null);
when(mailProps.getSslTrust()).thenReturn(null);
when(mailProps.getSslCheckServerIdentity()).thenReturn(null);
}
@Test
@@ -50,6 +55,32 @@ class MailConfigTest {
() -> assertEquals("password", impl.getPassword()),
() -> assertEquals("UTF-8", impl.getDefaultEncoding()),
() -> assertEquals("true", props.getProperty("mail.smtp.auth")),
() -> assertEquals("true", props.getProperty("mail.smtp.starttls.enable")));
() -> assertEquals("true", props.getProperty("mail.smtp.starttls.enable")),
() -> assertEquals(null, props.getProperty("mail.smtp.starttls.required")),
() -> assertEquals(null, props.getProperty("mail.smtp.ssl.enable")),
() -> assertEquals("*", props.getProperty("mail.smtp.ssl.trust")));
}
@Test
void shouldRespectExplicitTlsOverrides() {
ApplicationProperties appProps = mock(ApplicationProperties.class);
when(mailProps.getStartTlsEnable()).thenReturn(false);
when(mailProps.getStartTlsRequired()).thenReturn(true);
when(mailProps.getSslEnable()).thenReturn(true);
when(mailProps.getSslTrust()).thenReturn("*");
when(mailProps.getSslCheckServerIdentity()).thenReturn(true);
when(appProps.getMail()).thenReturn(mailProps);
MailConfig config = new MailConfig(appProps);
JavaMailSenderImpl impl = (JavaMailSenderImpl) config.javaMailSender();
Properties props = impl.getJavaMailProperties();
assertAll(
() -> assertEquals("false", props.getProperty("mail.smtp.starttls.enable")),
() -> assertEquals("true", props.getProperty("mail.smtp.starttls.required")),
() -> assertEquals("true", props.getProperty("mail.smtp.ssl.enable")),
() -> assertEquals("*", props.getProperty("mail.smtp.ssl.trust")),
() -> assertEquals("true", props.getProperty("mail.smtp.ssl.checkserveridentity")));
}
}
@@ -267,4 +267,222 @@ class UserLicenseSettingsServiceTest {
verify(userService, times(1)).grandfatherAllOAuthUsers();
verify(userService, times(1)).grandfatherPendingSsoUsersWithoutSession();
}
// ===== OAuth Eligibility Tests =====
@Test
void isOAuthEligible_grandfatheredUser_returnsTrue() {
// Grandfathered user should be eligible regardless of license
stirling.software.proprietary.security.model.User user =
new stirling.software.proprietary.security.model.User();
user.setUsername("grandfathered-user");
user.setOauthGrandfathered(true);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.NORMAL);
boolean result = service.isOAuthEligible(user);
assertEquals(true, result, "Grandfathered user should be eligible for OAuth");
}
@Test
void isOAuthEligible_nonGrandfatheredUserWithServerLicense_returnsTrue() {
// Non-grandfathered user with SERVER license should be eligible
stirling.software.proprietary.security.model.User user =
new stirling.software.proprietary.security.model.User();
user.setUsername("test-user");
user.setOauthGrandfathered(false);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.SERVER);
boolean result = service.isOAuthEligible(user);
assertEquals(true, result, "Non-grandfathered user with SERVER license should be eligible");
}
@Test
void isOAuthEligible_nonGrandfatheredUserWithEnterpriseLicense_returnsTrue() {
// Non-grandfathered user with ENTERPRISE license should be eligible
stirling.software.proprietary.security.model.User user =
new stirling.software.proprietary.security.model.User();
user.setUsername("test-user");
user.setOauthGrandfathered(false);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.ENTERPRISE);
boolean result = service.isOAuthEligible(user);
assertEquals(
true, result, "Non-grandfathered user with ENTERPRISE license should be eligible");
}
@Test
void isOAuthEligible_nonGrandfatheredUserWithNoLicense_returnsFalse() {
// Non-grandfathered user without license should NOT be eligible
stirling.software.proprietary.security.model.User user =
new stirling.software.proprietary.security.model.User();
user.setUsername("test-user");
user.setOauthGrandfathered(false);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.NORMAL);
boolean result = service.isOAuthEligible(user);
assertEquals(
false,
result,
"Non-grandfathered user without paid license should NOT be eligible");
}
@Test
void isOAuthEligible_newUserWithServerLicense_returnsTrue() {
// New user (null) with SERVER license should be eligible for auto-creation
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.SERVER);
boolean result = service.isOAuthEligible(null);
assertEquals(
true, result, "New user with SERVER license should be eligible for auto-creation");
}
@Test
void isOAuthEligible_newUserWithNoLicense_returnsFalse() {
// New user (null) without license should NOT be eligible
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.NORMAL);
boolean result = service.isOAuthEligible(null);
assertEquals(
false,
result,
"New user without paid license should NOT be eligible for auto-creation");
}
@Test
void isOAuthEligible_licenseCheckerUnavailable_returnsFalse() {
// If LicenseKeyChecker is unavailable, OAuth should be blocked
when(licenseKeyCheckerProvider.getIfAvailable()).thenReturn(null);
stirling.software.proprietary.security.model.User user =
new stirling.software.proprietary.security.model.User();
user.setUsername("test-user");
user.setOauthGrandfathered(false);
boolean result = service.isOAuthEligible(user);
assertEquals(
false, result, "OAuth should be blocked when LicenseKeyChecker is unavailable");
}
// ===== SAML Eligibility Tests =====
@Test
void isSamlEligible_grandfatheredUser_returnsTrue() {
// Grandfathered user should be eligible for SAML regardless of license
stirling.software.proprietary.security.model.User user =
new stirling.software.proprietary.security.model.User();
user.setUsername("grandfathered-user");
user.setOauthGrandfathered(true);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.NORMAL);
boolean result = service.isSamlEligible(user);
assertEquals(true, result, "Grandfathered user should be eligible for SAML");
}
@Test
void isSamlEligible_nonGrandfatheredUserWithEnterpriseLicense_returnsTrue() {
// Non-grandfathered user with ENTERPRISE license should be eligible
stirling.software.proprietary.security.model.User user =
new stirling.software.proprietary.security.model.User();
user.setUsername("test-user");
user.setOauthGrandfathered(false);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.ENTERPRISE);
boolean result = service.isSamlEligible(user);
assertEquals(
true,
result,
"Non-grandfathered user with ENTERPRISE license should be eligible for SAML");
}
@Test
void isSamlEligible_nonGrandfatheredUserWithServerLicense_returnsFalse() {
// Non-grandfathered user with SERVER license should NOT be eligible for SAML
stirling.software.proprietary.security.model.User user =
new stirling.software.proprietary.security.model.User();
user.setUsername("test-user");
user.setOauthGrandfathered(false);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.SERVER);
boolean result = service.isSamlEligible(user);
assertEquals(
false,
result,
"Non-grandfathered user with SERVER license should NOT be eligible for SAML");
}
@Test
void isSamlEligible_nonGrandfatheredUserWithNoLicense_returnsFalse() {
// Non-grandfathered user without license should NOT be eligible
stirling.software.proprietary.security.model.User user =
new stirling.software.proprietary.security.model.User();
user.setUsername("test-user");
user.setOauthGrandfathered(false);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.NORMAL);
boolean result = service.isSamlEligible(user);
assertEquals(
false,
result,
"Non-grandfathered user without ENTERPRISE license should NOT be eligible for SAML");
}
@Test
void isSamlEligible_newUserWithEnterpriseLicense_returnsTrue() {
// New user (null) with ENTERPRISE license should be eligible for auto-creation
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.ENTERPRISE);
boolean result = service.isSamlEligible(null);
assertEquals(
true,
result,
"New user with ENTERPRISE license should be eligible for SAML auto-creation");
}
@Test
void isSamlEligible_newUserWithServerLicense_returnsFalse() {
// New user (null) with SERVER license should NOT be eligible for SAML
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.SERVER);
boolean result = service.isSamlEligible(null);
assertEquals(
false,
result,
"New user with SERVER license should NOT be eligible for SAML (requires ENTERPRISE)");
}
@Test
void isSamlEligible_licenseCheckerUnavailable_returnsFalse() {
// If LicenseKeyChecker is unavailable, SAML should be blocked
when(licenseKeyCheckerProvider.getIfAvailable()).thenReturn(null);
stirling.software.proprietary.security.model.User user =
new stirling.software.proprietary.security.model.User();
user.setUsername("test-user");
user.setOauthGrandfathered(false);
boolean result = service.isSamlEligible(user);
assertEquals(false, result, "SAML should be blocked when LicenseKeyChecker is unavailable");
}
}
+49 -2
View File
@@ -12,6 +12,8 @@ plugins {
}
import com.github.jk1.license.render.*
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
ext {
springBootVersion = "3.5.6"
@@ -57,7 +59,7 @@ repositories {
allprojects {
group = 'stirling.software'
version = '2.0.3'
version = '2.1.3'
configurations.configureEach {
exclude group: 'commons-logging', module: 'commons-logging'
@@ -65,6 +67,51 @@ allprojects {
}
}
def writeIfChanged(File targetFile, String newContent) {
if (targetFile.getText('UTF-8') != newContent) {
targetFile.write(newContent, 'UTF-8')
}
}
def updateTauriConfigVersion(String version) {
File tauriConfig = file('frontend/src-tauri/tauri.conf.json')
def parsed = new JsonSlurper().parse(tauriConfig)
parsed.version = version
def formatted = JsonOutput.prettyPrint(JsonOutput.toJson(parsed)) + System.lineSeparator()
writeIfChanged(tauriConfig, formatted)
}
def updateSimulationVersion(File fileToUpdate, String version) {
def content = fileToUpdate.getText('UTF-8')
def matcher = content =~ /(appVersion:\s*')([^']*)(')/
if (!matcher.find()) {
throw new GradleException("Could not locate appVersion in ${fileToUpdate} for synchronization")
}
def updatedContent = matcher.replaceFirst("${matcher.group(1)}${version}${matcher.group(3)}")
writeIfChanged(fileToUpdate, updatedContent)
}
tasks.register('syncAppVersion') {
group = 'versioning'
description = 'Synchronizes app version across desktop and simulation configs.'
doLast {
def appVersion = project.version.toString()
println "Synchronizing application version to ${appVersion}"
updateTauriConfigVersion(appVersion)
[
'frontend/src/core/testing/serverExperienceSimulations.ts',
'frontend/src/proprietary/testing/serverExperienceSimulations.ts'
].each { path ->
updateSimulationVersion(file(path), appVersion)
}
}
}
tasks.register('writeVersion', WriteProperties) {
destinationFile = layout.projectDirectory.file('app/common/src/main/resources/version.properties')
println "Writing version.properties to ${destinationFile.get().asFile.path}"
@@ -314,7 +361,7 @@ tasks.named('bootRun') {
tasks.named('build') {
group = 'build'
description = 'Delegates to :stirling-pdf:bootJar'
dependsOn ':stirling-pdf:bootJar', 'buildRestartHelper'
dependsOn ':stirling-pdf:bootJar', 'buildRestartHelper', 'syncAppVersion'
doFirst {
println "Delegating to :stirling-pdf:bootJar"
+74 -16
View File
@@ -105,6 +105,7 @@
"typescript": "^5.9.2",
"typescript-eslint": "^8.44.1",
"vite": "^7.1.7",
"vite-plugin-static-copy": "^3.1.4",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4"
}
@@ -456,6 +457,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -499,6 +501,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -579,6 +582,7 @@
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.5.0.tgz",
"integrity": "sha512-Yrh9XoVaT8cUgzgqpJ7hx5wg6BqQrCFirqqlSwVb+Ly9oNn4fZbR9GycIWmzJOU5XBnaOJjXfQSaDyoNP0woNA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/engines": "1.5.0",
"@embedpdf/models": "1.5.0"
@@ -678,6 +682,7 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.5.0.tgz",
"integrity": "sha512-p7PTNNaIr4gH3jLwX+eLJe1DeUXgi21kVGN6SRx/pocH8esg4jqoOeD/YiRRZoZnPOiy0jBXVhkPkwSmY7a2hQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@@ -694,6 +699,7 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.5.0.tgz",
"integrity": "sha512-ckHgTfvkW6c5Ta7Mc+Dl9C2foVnvEpqEJ84wyBnqrU0OWbe/jsiPhyKBVeartMGqNI/kVfaQTXupyrKhekAVmg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@@ -711,6 +717,7 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.5.0.tgz",
"integrity": "sha512-P4YpIZfaW69etYIjphyaL4cGl2pB14h3OdTE0tRQ2pZYZHFLTvlt4q9B3PVSdhlSrHK5nob7jfLGon2U7xCslg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@@ -764,6 +771,7 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.5.0.tgz",
"integrity": "sha512-ywwSj0ByrlkvrJIHKRzqxARkOZriki8VJUC+T4MV8fGyF4CzvCRJyKlPktahFz+VxhoodqTh7lBCib68dH+GvA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@@ -798,6 +806,7 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.5.0.tgz",
"integrity": "sha512-RNmTZCZ8X1mA8cw9M7TMDuhO9GtkOalGha2bBL3En3D1IlDRS7PzNNMSMV7eqT7OQICSTltlpJ8p8Qi5esvL/Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@@ -834,6 +843,7 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.5.0.tgz",
"integrity": "sha512-zrxLBAZQoPswDuf9q9DrYaQc6B0Ysc2U1hueTjNH/4+ydfl0BFXZkKR63C2e3YmWtXvKjkoIj0GyPzsiBORLUw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@@ -909,6 +919,7 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.5.0.tgz",
"integrity": "sha512-G8GDyYRhfehw72+r4qKkydnA5+AU8qH67g01Y12b0DzI0VIzymh/05Z4dK8DsY3jyWPXJfw2hlg5+KDHaMBHgQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.5.0"
},
@@ -1064,6 +1075,7 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -1107,6 +1119,7 @@
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -2137,6 +2150,7 @@
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.6.tgz",
"integrity": "sha512-paTl+0x+O/QtgMtqVJaG8maD8sfiOdgPmLOyG485FmeGZ1L3KMdEkhxZtmdGlDFsLXhmMGQ57ducT90bvhXX5A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@floating-ui/react": "^0.27.16",
"clsx": "^2.1.1",
@@ -2187,6 +2201,7 @@
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.6.tgz",
"integrity": "sha512-liHfaWXHAkLjJy+Bkr29UsCwAoDQ/a64WrM67lksx8F0qqyjR5RQH8zVlhuOjdpQnwtlUkE/YiTvbJiPcoI0bw==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "^18.x || ^19.x"
}
@@ -2254,6 +2269,7 @@
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz",
"integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/core-downloads-tracker": "^7.3.5",
@@ -3186,6 +3202,7 @@
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz",
"integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.16"
}
@@ -3304,7 +3321,6 @@
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz",
"integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"acorn": "^8.9.0"
}
@@ -4081,6 +4097,7 @@
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -4409,6 +4426,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4419,6 +4437,7 @@
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -4488,6 +4507,7 @@
"integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.3",
"@typescript-eslint/types": "8.46.3",
@@ -5201,7 +5221,6 @@
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz",
"integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/shared": "3.5.24"
}
@@ -5211,7 +5230,6 @@
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz",
"integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.24",
"@vue/shared": "3.5.24"
@@ -5222,7 +5240,6 @@
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz",
"integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.24",
"@vue/runtime-core": "3.5.24",
@@ -5235,7 +5252,6 @@
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz",
"integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-ssr": "3.5.24",
"@vue/shared": "3.5.24"
@@ -5262,6 +5278,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5669,7 +5686,6 @@
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">= 0.4"
}
@@ -5946,6 +5962,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@@ -6993,7 +7010,8 @@
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz",
"integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==",
"dev": true,
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/dezalgo": {
"version": "1.0.4",
@@ -7388,6 +7406,7 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7558,6 +7577,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -7724,8 +7744,7 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/espree": {
"version": "10.4.0",
@@ -7790,7 +7809,6 @@
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.2.tgz",
"integrity": "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
}
@@ -8881,6 +8899,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -9357,7 +9376,6 @@
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "^1.0.6"
}
@@ -9678,6 +9696,7 @@
"integrity": "sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.19",
"@asamuzakjp/dom-selector": "^6.7.3",
@@ -10264,8 +10283,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/locate-path": {
"version": "6.0.0",
@@ -11076,6 +11094,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-map": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
"integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pac-proxy-agent": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
@@ -11411,6 +11442,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -11690,6 +11722,7 @@
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
"integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -12072,6 +12105,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12081,6 +12115,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -13592,7 +13627,6 @@
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">= 0.4"
}
@@ -13801,6 +13835,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -14102,6 +14137,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -14183,6 +14219,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"napi-postinstall": "^0.3.0"
},
@@ -14387,6 +14424,7 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -14479,6 +14517,25 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/vite-plugin-static-copy": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.4.tgz",
"integrity": "sha512-iCmr4GSw4eSnaB+G8zc2f4dxSuDjbkjwpuBLLGvQYR9IW7rnDzftnUjOH5p4RYR+d4GsiBqXRvzuFhs5bnzVyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.6.0",
"p-map": "^7.0.3",
"picocolors": "^1.1.1",
"tinyglobby": "^0.2.15"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/vite-tsconfig-paths": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz",
@@ -14538,6 +14595,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -14551,6 +14609,7 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -15162,8 +15221,7 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/zod": {
"version": "3.25.76",
+1
View File
@@ -152,6 +152,7 @@
"typescript": "^5.9.2",
"typescript-eslint": "^8.44.1",
"vite": "^7.1.7",
"vite-plugin-static-copy": "^3.1.4",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4"
},
+125 -3
View File
@@ -3036,6 +3036,91 @@ title = "الحصول على معلومات عن PDF"
header = "الحصول على معلومات عن PDF"
submit = "الحصول على المعلومات"
downloadJson = "تحميل JSON"
processing = "جارٍ استخراج المعلومات..."
results = "النتائج"
noResults = "شغّل الأداة لإنشاء تقرير."
downloads = "التنزيلات"
noneDetected = "لم يتم اكتشاف أي شيء"
indexTitle = "الفهرس"
[getPdfInfo.report]
entryLabel = "ملخص المعلومات الكامل"
shortTitle = "معلومات PDF"
[getPdfInfo.sections]
metadata = "البيانات الوصفية"
formFields = "حقول النماذج"
basicInfo = "معلومات أساسية"
documentInfo = "معلومات المستند"
compliance = "الامتثال"
encryption = "التشفير"
permissions = "الأذونات"
other = "أخرى"
perPageInfo = "معلومات لكل صفحة"
tableOfContents = "جدول المحتويات"
[getPdfInfo.other]
attachments = "المرفقات"
embeddedFiles = "ملفات مضمنة"
javaScript = "JavaScript"
layers = "الطبقات"
structureTree = "شجرة البنية"
xmp = "بيانات XMP الوصفية"
[getPdfInfo.perPage]
size = "الحجم"
annotations = "التعليقات التوضيحية"
images = "الصور"
links = "الروابط"
fonts = "الخطوط"
xobjects = "عدد كائنات XObject"
multimedia = "وسائط متعددة"
[getPdfInfo.summary]
pages = "الصفحات"
fileSize = "حجم الملف"
pdfVersion = "إصدار PDF"
language = "اللغة"
title = "ملخص PDF"
author = "المؤلف"
created = "تم الإنشاء"
modified = "تم التعديل"
permsAll = "جميع الأذونات مسموح بها"
permsRestricted = "{{count}} قيود"
permsMixed = "بعض الأذونات مقيّدة"
hasCompliance = "يتضمن معايير امتثال"
noCompliance = "لا توجد معايير امتثال"
basic = "معلومات أساسية"
documentInfo = "معلومات المستند"
securityTitle = "حالة الأمان"
technical = "تقني"
overviewTitle = "نظرة عامة على PDF"
[getPdfInfo.summary.security]
encrypted = "PDF مشفّر - توجد حماية بكلمة مرور"
unencrypted = "PDF غير مُشفّر - لا توجد حماية بكلمة مرور"
[getPdfInfo.summary.tech]
images = "الصور"
fonts = "الخطوط"
formFields = "حقول النماذج"
embeddedFiles = "ملفات مضمنة"
javaScript = "JavaScript"
layers = "الطبقات"
bookmarks = "الإشارات المرجعية"
multimedia = "وسائط متعددة"
[getPdfInfo.summary.overview]
untitled = "مستند بلا عنوان"
unknown = "مؤلف غير معروف"
text = "هذا ملف PDF يحتوي على {{pages}} صفحة بعنوان {{title}} وتم إنشاؤه بواسطة {{author}} (إصدار PDF {{version}})."
[getPdfInfo.error]
partial = "تعذّر معالجة بعض الملفات."
unexpected = "حدث خطأ غير متوقع أثناء الاستخراج."
[getPdfInfo.status]
complete = "اكتمل الاستخراج"
[extractPage]
tags = "استخراج"
@@ -3828,8 +3913,8 @@ title = "التحليلات"
description = "تساعدنا هذه الملفات على فهم كيفية استخدام أدواتنا، كي نركّز على بناء الميزات الأكثر قيمة لمجتمعنا. كن مطمئنًا—‏Stirling PDF لا يمكنه ولن يتتبع محتوى المستندات التي تعمل عليها."
[cookieBanner.services]
posthog = "PostHog Analytics"
scarf = "Scarf Pixel"
posthog = "تحليلات PostHog"
scarf = "Scarf بكسل"
[removeMetadata]
submit = "إزالة البيانات الوصفية"
@@ -5177,7 +5262,7 @@ upgrade = "الترقية الآن →"
freeTitle = "ترخيص الخادم"
overLimitTitle = "مطلوب ترخيص خادم"
overLimitBody = "ترخيصنا يسمح حتى <strong>{{freeTierLimit}}</strong> مستخدمين مجاناً لكل خادم. لديك <strong>{{overLimitUserCopy}}</strong> مستخدمي Stirling. للمتابعة دون انقطاع، ارقَ إلى خطة خادم Stirling - <strong>مقاعد غير محدودة</strong>، تحرير نصوص PDF، وتحكم إداري كامل مقابل $99/خادم/شهرياً."
freeBody = "ترخيص <strong>Open-Core</strong> لدينا يسمح حتى <strong>{{freeTierLimit}}</strong> مستخدمين مجاناً لكل خادم. للتوسع بسلاسة والحصول على وصول مبكر إلى <strong>أداة تحرير نصوص PDF</strong> الجديدة، نوصي بخطة خادم Stirling - تحرير كامل و<strong>مقاعد غير محدودة</strong> مقابل $99/خادم/شهرياً."
freeBody = "يتيح ترخيصنا <strong>Open-Core</strong> ما يصل إلى <strong>{{freeTierLimit}}</strong> مستخدمًا مجانًا لكل خادم. للتوسع دون انقطاع، نوصي بخطة Stirling Server - <strong>مقاعد غير محدودة</strong> و<strong>دعم SSO</strong> مقابل $99/server/mo."
[onboarding.desktopInstall]
title = "تنزيل"
@@ -5282,6 +5367,31 @@ error = "فشل تحديث حالة المستخدم"
success = "تم حذف المستخدم بنجاح"
error = "فشل حذف المستخدم"
[workspace.people.changePassword]
action = "تغيير كلمة المرور"
title = "تغيير كلمة المرور"
subtitle = "تحديث كلمة المرور لـ"
newPassword = "كلمة مرور جديدة"
confirmPassword = "تأكيد كلمة المرور"
placeholder = "أدخل كلمة مرور جديدة"
confirmPlaceholder = "أعد إدخال كلمة المرور الجديدة"
passwordRequired = "يرجى إدخال كلمة مرور جديدة"
passwordMismatch = "كلمتا المرور غير متطابقتين"
generateRandom = "إنشاء كلمة مرور آمنة"
generatedPreview = "كلمة المرور المُنشأة:"
copyTooltip = "نسخ إلى الحافظة"
copiedToClipboard = "تم نسخ كلمة المرور إلى الحافظة"
copyFailed = "فشل نسخ كلمة المرور"
sendEmail = "إرسال بريد إلكتروني للمستخدم حول هذا التغيير"
includePassword = "تضمين كلمة المرور الجديدة في البريد الإلكتروني"
forcePasswordChange = "إلزام المستخدم بتغيير كلمة المرور عند تسجيل الدخول التالي"
emailUnavailable = "بريد هذا المستخدم الإلكتروني غير صالح. تم تعطيل الإشعارات."
smtpDisabled = "تتطلب إشعارات البريد الإلكتروني تفعيل SMTP في الإعدادات."
notifyOnly = "سيتم إرسال بريد إلكتروني بدون كلمة المرور لإبلاغ المستخدم بأن المشرف قد غيّرها."
submit = "تحديث كلمة المرور"
success = "تم تحديث كلمة المرور بنجاح"
error = "فشل تحديث كلمة المرور"
[workspace.people.emailInvite]
tab = "دعوة عبر البريد الإلكتروني"
description = "اكتب أو الصق عناوين البريد الإلكتروني أدناه مفصولة بفواصل. سيتلقى المستخدمون بيانات اعتماد تسجيل الدخول عبر البريد الإلكتروني."
@@ -5815,6 +5925,7 @@ subtitle = "سجّل الدخول بحساب Stirling الخاص بك"
[setup.selfhosted]
title = "سجّل الدخول إلى الخادم"
subtitle = "أدخل بيانات اعتماد الخادم"
link = "أو الاتصال بحساب مُستضاف ذاتيًا"
[setup.server]
title = "الاتصال بالخادم"
@@ -5833,6 +5944,14 @@ description = "أدخل عنوان URL الكامل لخادم Stirling PDF ال
emptyUrl = "يرجى إدخال عنوان URL للخادم"
unreachable = "تعذّر الاتصال بالخادم"
testFailed = "فشل اختبار الاتصال"
configFetch = "فشل في جلب إعدادات الخادم. يرجى التحقق من عنوان URL والمحاولة مرة أخرى."
[setup.server.error.securityDisabled]
title = "تسجيل الدخول غير مفعّل"
body = "لا يحتوي هذا الخادم على تسجيل دخول مفعّل. للاتصال بهذا الخادم، يجب تمكين المصادقة:"
step1 = "عيّن DOCKER_ENABLE_SECURITY=true في بيئتك"
step2 = "أو عيّن security.enableLogin=true في settings.yml"
step3 = "أعد تشغيل الخادم"
[setup.login]
title = "تسجيل الدخول"
@@ -5905,6 +6024,7 @@ earlyAccess = "وصول مبكر"
reset = "إعادة تعيين التغييرات"
downloadJson = "تنزيل JSON"
generatePdf = "توليد PDF"
saveChanges = "حفظ التغييرات"
[pdfTextEditor.options.autoScaleText]
title = "ضبط النص تلقائياً ليتناسب مع الصناديق"
@@ -5942,6 +6062,8 @@ alpha = "هذا العارض بنسخة ألفا ولا يزال يتطور—ق
[pdfTextEditor.empty]
title = "لم يتم تحميل مستند"
subtitle = "حمّل ملف PDF أو JSON لبدء تحرير محتوى النص."
dropzone = "اسحب وأفلت ملف PDF أو JSON هنا، أو انقر للاستعراض"
dropzoneWithFiles = "حدد ملفًا من علامة تبويب الملفات، أو اسحب وأفلت ملف PDF أو JSON هنا، أو انقر للاستعراض"
[pdfTextEditor.welcomeBanner]
title = "مرحباً بك في محرر نصوص PDF (وصول مبكر)"
+123 -1
View File
@@ -3036,6 +3036,91 @@ title = "PDF Barəsində Məlumat Əldə Et"
header = "PDF Barəsində Məlumat Əldə Et"
submit = "Məlumat Əldə Et"
downloadJson = "JSON yüklə"
processing = "Məlumat çıxarılır..."
results = "Nəticələr"
noResults = "Hesabat yaratmaq üçün aləti işə salın."
downloads = "Yükləmələr"
noneDetected = "Heç nə aşkar edilmədi"
indexTitle = "İndeks"
[getPdfInfo.report]
entryLabel = "Tam məlumat xülasəsi"
shortTitle = "PDF Məlumatı"
[getPdfInfo.sections]
metadata = "Metaməlumat"
formFields = "Forma sahələri"
basicInfo = "Əsas Məlumat"
documentInfo = "Sənəd Məlumatı"
compliance = "Uyğunluq"
encryption = "Şifrələmə"
permissions = "İcazələr"
other = "Digər"
perPageInfo = "Hər səhifə üzrə məlumat"
tableOfContents = "Mündəricat"
[getPdfInfo.other]
attachments = "Əlavələr"
embeddedFiles = "Gömülü fayllar"
javaScript = "JavaScript"
layers = "Qatlar"
structureTree = "Struktur ağacı"
xmp = "XMPMetadata"
[getPdfInfo.perPage]
size = "Ölçü"
annotations = "Annotasiyalar"
images = "Şəkillər"
links = "Keçidlər"
fonts = "Şriftlər"
xobjects = "XObject sayları"
multimedia = "Multimedia"
[getPdfInfo.summary]
pages = "Səhifələr"
fileSize = "Fayl Ölçüsü"
pdfVersion = "PDF Versiyası"
language = "Dil"
title = "PDF Xülasəsi"
author = "Müəllif"
created = "Yaradılıb"
modified = "Dəyişdirilib"
permsAll = "Bütün icazələr verilib"
permsRestricted = "{{count}} məhdudiyyət"
permsMixed = "Bəzi icazələr məhdudlaşdırılıb"
hasCompliance = "Uyğunluq standartları mövcuddur"
noCompliance = "Uyğunluq standartları yoxdur"
basic = "Əsas Məlumat"
documentInfo = "Sənəd Məlumatı"
securityTitle = "Təhlükəsizlik Vəziyyəti"
technical = "Texniki"
overviewTitle = "PDF İcmalı"
[getPdfInfo.summary.security]
encrypted = "Şifrələnmiş PDF - Parol ilə qorunur"
unencrypted = "Şifrələnməmiş PDF - Parol qorunması yoxdur"
[getPdfInfo.summary.tech]
images = "Şəkillər"
fonts = "Şriftlər"
formFields = "Forma sahələri"
embeddedFiles = "Gömülü fayllar"
javaScript = "JavaScript"
layers = "Qatlar"
bookmarks = "Əlfəcinlər"
multimedia = "Multimedia"
[getPdfInfo.summary.overview]
untitled = "adsız sənəd"
unknown = "Naməlum müəllif"
text = "Bu, {{author}} tərəfindən yaradılmış, {{title}} adlı, {{pages}} səhifəlik PDF-dir (PDF versiyası {{version}})."
[getPdfInfo.error]
partial = "Bəzi faylları emal etmək mümkün olmadı."
unexpected = "Çıxarılma zamanı gözlənilməz xəta baş verdi."
[getPdfInfo.status]
complete = "Çıxarılma tamamlandı"
[extractPage]
tags = "çıxar"
@@ -5177,7 +5262,7 @@ upgrade = "İndi yüksəlt →"
freeTitle = "Server lisenziyası"
overLimitTitle = "Server lisenziyası tələb olunur"
overLimitBody = "Lisenziyalaşmamız hər server üçün pulsuz olaraq maksimum <strong>{{freeTierLimit}}</strong> istifadəçiyə icazə verir. Sizdə <strong>{{overLimitUserCopy}}</strong> Stirling istifadəçisi var. Fasiləsiz davam etmək üçün Stirling Server planına yüksəldin - <strong>limitsiz yerlər</strong>, PDF mətn redaktəsi və tam admin nəzarəti cəmi $99/server/ay."
freeBody = "Bizim <strong>Open-Core</strong> lisenziyası hər server üçün pulsuz olaraq maksimum <strong>{{freeTierLimit}}</strong> istifadəçiyə icazə verir. Fasiləsiz miqyaslanmaq və yeni <strong>PDF mətn redaktəsi alətimizə</strong> erkən çıxış əldə etmək üçün Stirling Server planını tövsiyə edirik — tam redaktə və <strong>limitsiz yerlər</strong> $99/server/ay."
freeBody = "Bizim <strong>Open-Core</strong> lisenziyalaşdırmamız hər server üçün pulsuz olaraq ən çox <strong>{{freeTierLimit}}</strong> istifadəçiyə icazə verir. Fasiləsiz miqyaslama üçün Stirling Server planını tövsiyə edirik - <strong>limitsiz yerlər</strong> və <strong>SSO dəstəyi</strong> $99/server/ay."
[onboarding.desktopInstall]
title = "Yüklə"
@@ -5282,6 +5367,31 @@ error = "İstifadəçi statusunu yeniləmək alınmadı"
success = "İstifadəçi uğurla silindi"
error = "İstifadəçini silmək alınmadı"
[workspace.people.changePassword]
action = "Parolu dəyiş"
title = "Parolu dəyiş"
subtitle = "Aşağıdakı istifadəçi üçün parolu yeniləyin"
newPassword = "Yeni parol"
confirmPassword = "Parolu təsdiq edin"
placeholder = "Yeni parolu daxil edin"
confirmPlaceholder = "Yeni parolu yenidən daxil edin"
passwordRequired = "Zəhmət olmasa yeni parolu daxil edin"
passwordMismatch = "Parollar uyğun gəlmir"
generateRandom = "Təhlükəsiz parol yaradın"
generatedPreview = "Yaradılmış parol:"
copyTooltip = "Buferə kopyala"
copiedToClipboard = "Parol buferə kopyalandı"
copyFailed = "Parolu kopyalamaq alınmadı"
sendEmail = "Bu dəyişiklik barədə istifadəçiyə e-poçt göndərin"
includePassword = "E-poçta yeni parolu daxil edin"
forcePasswordChange = "Növbəti girişdə istifadəçini parolu dəyişməyə məcbur et"
emailUnavailable = "Bu istifadəçinin e-poçtu etibarlı e-poçt ünvanı deyil. Bildirişlər söndürülüb."
smtpDisabled = "E-poçt bildirişləri üçün parametrlərdə SMTP aktiv olmalıdır."
notifyOnly = "Parol olmadan e-poçt göndəriləcək; istifadəçiyə adminin onu dəyişdiyi bildiriləcək."
submit = "Parolu yenilə"
success = "Parol uğurla yeniləndi"
error = "Parolu yeniləmək alınmadı"
[workspace.people.emailInvite]
tab = "E-poçt Dəvəti"
description = "Aşağıya vergüllə ayrılmış e-poçtları yazın və ya yapışdırın. İstifadəçilərə giriş məlumatları e-poçtla göndəriləcək."
@@ -5815,6 +5925,7 @@ subtitle = "Stirling hesabınızla daxil olun"
[setup.selfhosted]
title = "Serverə daxil olun"
subtitle = "Server məlumatlarınızı daxil edin"
link = "və ya self-hosted hesaba qoşulun"
[setup.server]
title = "Serverə qoşulun"
@@ -5833,6 +5944,14 @@ description = "Öz Stirling PDF serverinizin tam URL ünvanını daxil edin"
emptyUrl = "Zəhmət olmasa server URL-i daxil edin"
unreachable = "Serverə qoşulmaq mümkün olmadı"
testFailed = "Bağlantı testi uğursuz oldu"
configFetch = "Server konfiqurasiyasını əldə etmək mümkün olmadı. URL-i yoxlayın və yenidən cəhd edin."
[setup.server.error.securityDisabled]
title = "Giriş aktiv deyil"
body = "Bu serverdə giriş aktiv deyil. Bu serverə qoşulmaq üçün autentifikasiya aktiv edilməlidir:"
step1 = "Mühitinizdə DOCKER_ENABLE_SECURITY=true təyin edin"
step2 = "Yaxud settings.yml faylında security.enableLogin=true təyin edin"
step3 = "Serveri yenidən başladın"
[setup.login]
title = "Daxil ol"
@@ -5905,6 +6024,7 @@ earlyAccess = "Erkən Giriş"
reset = "Dəyişiklikləri sıfırla"
downloadJson = "JSON-u endir"
generatePdf = "PDF yarat"
saveChanges = "Dəyişiklikləri yadda saxla"
[pdfTextEditor.options.autoScaleText]
title = "Mətni avtomatik miqyasla"
@@ -5942,6 +6062,8 @@ alpha = "Bu alfa görüntüləyici hələ inkişaf edir—bəzi şriftlər, rən
[pdfTextEditor.empty]
title = "Sənəd yüklənməyib"
subtitle = "Mətn məzmununu redaktə etməyə başlamaq üçün PDF və ya JSON faylı yükləyin."
dropzone = "Buraya PDF və ya JSON faylını sürükləyib buraxın və ya baxmaq üçün klikləyin"
dropzoneWithFiles = "Fayllar vərəqindən bir fayl seçin və ya buraya PDF və ya JSON faylını sürükləyib buraxın, yaxud baxmaq üçün klikləyin"
[pdfTextEditor.welcomeBanner]
title = "PDF Text Editor-ə xoş gəldiniz (Erkən Giriş)"
+123 -1
View File
@@ -3036,6 +3036,91 @@ title = "Вземете информация за PDF"
header = "Вземете информация за PDF"
submit = "Вземете информация"
downloadJson = "Изтеглете JSON"
processing = "Извличане на информация..."
results = "Резултати"
noResults = "Стартирайте инструмента, за да генерирате отчет."
downloads = "Изтегляния"
noneDetected = "Нищо не е открито"
indexTitle = "Индекс"
[getPdfInfo.report]
entryLabel = "Пълно резюме на информацията"
shortTitle = "Информация за PDF"
[getPdfInfo.sections]
metadata = "Метаданни"
formFields = "Полета на формуляра"
basicInfo = "Основна информация"
documentInfo = "Информация за документа"
compliance = "Съответствие"
encryption = "Шифриране"
permissions = "Разрешения"
other = "Друго"
perPageInfo = "Информация по страници"
tableOfContents = "Съдържание"
[getPdfInfo.other]
attachments = "Прикачени файлове"
embeddedFiles = "Вградени файлове"
javaScript = "JavaScript"
layers = "Слоеве"
structureTree = "StructureTree"
xmp = "XMPMetadata"
[getPdfInfo.perPage]
size = "Размер"
annotations = "Анотации"
images = "Изображения"
links = "Връзки"
fonts = "Шрифтове"
xobjects = "Брой XObject"
multimedia = "Мултимедия"
[getPdfInfo.summary]
pages = "Страници"
fileSize = "Размер на файла"
pdfVersion = "Версия на PDF"
language = "Език"
title = "Обобщение на PDF"
author = "Автор"
created = "Създаден"
modified = "Променен"
permsAll = "Всички разрешения са позволени"
permsRestricted = "{{count}} ограничения"
permsMixed = "Някои разрешения са ограничени"
hasCompliance = "Има стандарти за съответствие"
noCompliance = "Няма стандарти за съответствие"
basic = "Основна информация"
documentInfo = "Информация за документа"
securityTitle = "Състояние на сигурността"
technical = "Технически"
overviewTitle = "Преглед на PDF"
[getPdfInfo.summary.security]
encrypted = "Шифриран PDF - налична защита с парола"
unencrypted = "Нешифриран PDF - няма защита с парола"
[getPdfInfo.summary.tech]
images = "Изображения"
fonts = "Шрифтове"
formFields = "Полета на формуляра"
embeddedFiles = "Вградени файлове"
javaScript = "JavaScript"
layers = "Слоеве"
bookmarks = "Отметки"
multimedia = "Мултимедия"
[getPdfInfo.summary.overview]
untitled = "неозаглавен документ"
unknown = "Неизвестен автор"
text = "Това е {{pages}}-страничен PDF със заглавие {{title}}, създаден от {{author}} (версия на PDF {{version}})."
[getPdfInfo.error]
partial = "Някои файлове не можаха да бъдат обработени."
unexpected = "Неочаквана грешка по време на извличане."
[getPdfInfo.status]
complete = "Извличането е завършено"
[extractPage]
tags = "извличане"
@@ -5177,7 +5262,7 @@ upgrade = "Надградете сега →"
freeTitle = "Лиценз за сървър"
overLimitTitle = "Необходим е лиценз за сървър"
overLimitBody = "Нашият лиценз позволява до <strong>{{freeTierLimit}}</strong> безплатни потребители на сървър. Имате <strong>{{overLimitUserCopy}}</strong> потребители на Stirling. За да продължите без прекъсвания, надградете до плана Stirling Server <strong>неограничени места</strong>, редакция на PDF текст и пълен админ контрол за $99/сървър/месец."
freeBody = "Нашият <strong>Open-Core</strong> лиценз позволява до <strong>{{freeTierLimit}}</strong> безплатни потребители на сървър. За да мащабирате без прекъсвания и да получите ранен достъп до нашия нов <strong>инструмент за редакция на PDF текст</strong>, препоръчваме плана Stirling Server – пълно редактиране и <strong>неограничени места</strong> за $99/сървър/месец."
freeBody = "Нашият лицензен модел <strong>Open-Core</strong> позволява до <strong>{{freeTierLimit}}</strong> потребители безплатно на сървър. За безпрепятствено мащабиране препоръчваме плана Stirling Server - <strong>неограничени места</strong> и <strong>поддръжка на SSO</strong> за $99/server/mo."
[onboarding.desktopInstall]
title = "Изтегляне"
@@ -5282,6 +5367,31 @@ error = "Неуспешно обновяване на статус на потр
success = "Потребителят е изтрит успешно"
error = "Неуспешно изтриване на потребител"
[workspace.people.changePassword]
action = "Промяна на парола"
title = "Промяна на парола"
subtitle = "Актуализирайте паролата за"
newPassword = "Нова парола"
confirmPassword = "Потвърдете паролата"
placeholder = "Въведете нова парола"
confirmPlaceholder = "Въведете отново новата парола"
passwordRequired = "Моля, въведете нова парола"
passwordMismatch = "Паролите не съвпадат"
generateRandom = "Генерирайте сигурна парола"
generatedPreview = "Генерирана парола:"
copyTooltip = "Копиране в клипборда"
copiedToClipboard = "Паролата е копирана в клипборда"
copyFailed = "Неуспешно копиране на паролата"
sendEmail = "Изпратете имейл на потребителя за тази промяна"
includePassword = "Включете новата парола в имейла"
forcePasswordChange = "Принудете потребителя да смени паролата при следващо влизане"
emailUnavailable = "Имейлът на този потребител не е валиден адрес. Известията са изключени."
smtpDisabled = "Имейл известията изискват SMTP да е активиран в настройките."
notifyOnly = "Ще бъде изпратен имейл без паролата, за да уведоми потребителя, че администратор я е променил."
submit = "Актуализиране на паролата"
success = "Паролата е актуализирана успешно"
error = "Неуспешно актуализиране на паролата"
[workspace.people.emailInvite]
tab = "Покана по имейл"
description = "Въведете или поставете имейли по-долу, разделени със запетаи. Потребителите ще получат данни за вход по имейл."
@@ -5815,6 +5925,7 @@ subtitle = "Впишете се с вашия Stirling акаунт"
[setup.selfhosted]
title = "Впишете се в сървъра"
subtitle = "Въведете своите данни за сървъра"
link = "или се свържете със самостоятелно хостван акаунт"
[setup.server]
title = "Свързване към сървър"
@@ -5833,6 +5944,14 @@ description = "Въведете пълния URL на вашия самосто
emptyUrl = "Моля, въведете URL на сървър"
unreachable = "Неуспешна връзка със сървъра"
testFailed = "Тестът на връзката е неуспешен"
configFetch = "Неуспешно извличане на конфигурацията на сървъра. Моля, проверете URL адреса и опитайте отново."
[setup.server.error.securityDisabled]
title = "Входът не е активиран"
body = "На този сървър не е активиран вход. За да се свържете, трябва да активирате удостоверяване:"
step1 = "Задайте DOCKER_ENABLE_SECURITY=true във вашата среда"
step2 = "Или задайте security.enableLogin=true в settings.yml"
step3 = "Рестартирайте сървъра"
[setup.login]
title = "Вписване"
@@ -5905,6 +6024,7 @@ earlyAccess = "Ранен достъп"
reset = "Отмени промените"
downloadJson = "Изтегли JSON"
generatePdf = "Генерирай PDF"
saveChanges = "Запази промените"
[pdfTextEditor.options.autoScaleText]
title = "Авто-мащабиране на текст за напасване в полетата"
@@ -5942,6 +6062,8 @@ alpha = "Този алфа визуализатор все още се разв
[pdfTextEditor.empty]
title = "Няма зареден документ"
subtitle = "Заредете PDF или JSON файл, за да започнете да редактирате текстовото съдържание."
dropzone = "Плъзнете и пуснете тук PDF или JSON файл или щракнете, за да прегледате"
dropzoneWithFiles = "Изберете файл от раздела Файлове или плъзнете и пуснете тук PDF или JSON файл, или щракнете, за да прегледате"
[pdfTextEditor.welcomeBanner]
title = "Добре дошли в PDF Text Editor (ранен достъп)"
+127 -5
View File
@@ -352,7 +352,7 @@ teams = "Equips"
title = "Configuració"
systemSettings = "Configuració del sistema"
features = "Funcions"
endpoints = "Endpoints"
endpoints = "Punts finals"
database = "Base de dades"
advanced = "Avançat"
@@ -561,7 +561,7 @@ totalEndpoints = "Total d'endpoints"
totalVisits = "Total de visites"
showing = "Mostrant"
selectedVisits = "Visites seleccionades"
endpoint = "Endpoint"
endpoint = "Punt final"
visits = "Visites"
percentage = "Percentatge"
loading = "Carregant..."
@@ -3036,6 +3036,91 @@ title = "Obteniu Informació del PDF"
header = "Obteniu Informació del PDF"
submit = "Obteniu Informació"
downloadJson = "Descarrega JSON"
processing = "Extraient informació..."
results = "Resultats"
noResults = "Executeu l'eina per generar un informe."
downloads = "Descàrregues"
noneDetected = "No se n'ha detectat cap"
indexTitle = "Índex"
[getPdfInfo.report]
entryLabel = "Resum d'informació complet"
shortTitle = "Informació del PDF"
[getPdfInfo.sections]
metadata = "Metadades"
formFields = "Camps de formulari"
basicInfo = "Informació bàsica"
documentInfo = "Informació del document"
compliance = "Conformitat"
encryption = "Xifratge"
permissions = "Permisos"
other = "Altres"
perPageInfo = "Informació per pàgina"
tableOfContents = "Taula de continguts"
[getPdfInfo.other]
attachments = "Fitxers adjunts"
embeddedFiles = "Fitxers incrustats"
javaScript = "JavaScript"
layers = "Capes"
structureTree = "StructureTree"
xmp = "XMPMetadata"
[getPdfInfo.perPage]
size = "Mida"
annotations = "Anotacions"
images = "Imatges"
links = "Enllaços"
fonts = "Tipus de lletra"
xobjects = "Recompte d'XObject"
multimedia = "Multimèdia"
[getPdfInfo.summary]
pages = "Pàgines"
fileSize = "Mida del fitxer"
pdfVersion = "Versió del PDF"
language = "Idioma"
title = "Resum del PDF"
author = "Autor"
created = "Creat"
modified = "Modificat"
permsAll = "Tots els permisos permesos"
permsRestricted = "{{count}} restriccions"
permsMixed = "Alguns permisos restringits"
hasCompliance = "Té estàndards de conformitat"
noCompliance = "Sense estàndards de conformitat"
basic = "Informació bàsica"
documentInfo = "Informació del document"
securityTitle = "Estat de seguretat"
technical = "Tècnic"
overviewTitle = "Visió general del PDF"
[getPdfInfo.summary.security]
encrypted = "PDF xifrat - Protecció amb contrasenya present"
unencrypted = "PDF no xifrat - Sense protecció amb contrasenya"
[getPdfInfo.summary.tech]
images = "Imatges"
fonts = "Tipus de lletra"
formFields = "Camps de formulari"
embeddedFiles = "Fitxers incrustats"
javaScript = "JavaScript"
layers = "Capes"
bookmarks = "Marcadors"
multimedia = "Multimèdia"
[getPdfInfo.summary.overview]
untitled = "un document sense títol"
unknown = "Autor desconegut"
text = "Aquest és un PDF de {{pages}} pàgines titulat {{title}} creat per {{author}} (versió del PDF {{version}})."
[getPdfInfo.error]
partial = "Alguns fitxers no s'han pogut processar."
unexpected = "Error inesperat durant l'extracció."
[getPdfInfo.status]
complete = "Extracció completada"
[extractPage]
tags = "extreure"
@@ -4366,7 +4451,7 @@ features = "Banderes de funcions"
processing = "Processament"
[admin.settings.advanced.endpoints]
label = "Endpoints"
label = "Punts finals"
manage = "Gestiona els endpoints de l'API"
description = "La gestió d'endpoints es configura via YAML. Consulteu la documentació per a detalls sobre com habilitar/deshabilitar endpoints específics."
@@ -5177,7 +5262,7 @@ upgrade = "Actualitza ara →"
freeTitle = "Llicència del servidor"
overLimitTitle = "Cal una llicència de servidor"
overLimitBody = "La nostra llicència permet fins a <strong>{{freeTierLimit}}</strong> usuaris gratuïts per servidor. Tens <strong>{{overLimitUserCopy}}</strong> usuaris de Stirling. Per continuar sense interrupcions, actualitza al pla Stirling Server: <strong>seients il·limitats</strong>, edició de text de PDF i control d'administració complet per 99 $/servidor/mes."
freeBody = "La nostra llicència <strong>Open-Core</strong> permet fins a <strong>{{freeTierLimit}}</strong> usuaris gratuïts per servidor. Per escalar sense interrupcions i obtenir accés anticipat a la nova <strong>eina d'edició de text PDF</strong>, recomanem el pla Stirling Server: edició completa i <strong>seients il·limitats</strong> per 99 $/servidor/mes."
freeBody = "La nostra llicència <strong>Open-Core</strong> permet fins a <strong>{{freeTierLimit}}</strong> usuaris gratuïts per servidor. Per escalar sense interrupcions, recomanem el pla Stirling Server - <strong>places il·limitades</strong> i <strong>suport SSO</strong> per $99/servidor/mes."
[onboarding.desktopInstall]
title = "Baixa"
@@ -5282,6 +5367,31 @@ error = "No sha pogut actualitzar lestat de lusuari"
success = "Usuari suprimit correctament"
error = "No sha pogut suprimir lusuari"
[workspace.people.changePassword]
action = "Canvieu la contrasenya"
title = "Canvi de contrasenya"
subtitle = "Actualitza la contrasenya de"
newPassword = "Contrasenya nova"
confirmPassword = "Confirma la contrasenya"
placeholder = "Introduïu una contrasenya nova"
confirmPlaceholder = "Torneu a introduir la contrasenya nova"
passwordRequired = "Introduïu una contrasenya nova"
passwordMismatch = "Les contrasenyes no coincideixen"
generateRandom = "Genereu una contrasenya segura"
generatedPreview = "Contrasenya generada:"
copyTooltip = "Copieu al portapapers"
copiedToClipboard = "Contrasenya copiada al portapapers"
copyFailed = "No s'ha pogut copiar la contrasenya"
sendEmail = "Envieu un correu a l'usuari sobre aquest canvi"
includePassword = "Incloeu la contrasenya nova al correu"
forcePasswordChange = "Obligueu l'usuari a canviar la contrasenya en el pròxim inici de sessió"
emailUnavailable = "El correu d'aquest usuari no és una adreça de correu vàlida. Les notificacions estan desactivades."
smtpDisabled = "Les notificacions per correu electrònic requereixen habilitar SMTP als paràmetres."
notifyOnly = "S'enviarà un correu sense la contrasenya, informant l'usuari que un administrador l'ha canviada."
submit = "Actualitzeu la contrasenya"
success = "La contrasenya s'ha actualitzat correctament"
error = "No s'ha pogut actualitzar la contrasenya"
[workspace.people.emailInvite]
tab = "Invitació per correu"
description = "Escriviu o enganxeu correus a continuació, separats per comes. Els usuaris rebran credencials dinici de sessió per correu electrònic."
@@ -5754,7 +5864,7 @@ title = "Gràfic d'ús dels endpoints"
[usage.table]
title = "Estadístiques detallades"
endpoint = "Endpoint"
endpoint = "Punt final"
visits = "Visites"
percentage = "Percentatge"
noData = "No hi ha dades disponibles"
@@ -5815,6 +5925,7 @@ subtitle = "Inicia sessió amb el teu compte de Stirling"
[setup.selfhosted]
title = "Inicia sessió al servidor"
subtitle = "Introdueix les credencials del servidor"
link = "o connecteu-vos a un compte autoallotjat"
[setup.server]
title = "Connecta't al servidor"
@@ -5833,6 +5944,14 @@ description = "Introdueix la URL completa del teu servidor autoallotjat de Stirl
emptyUrl = "Introdueix una URL de servidor"
unreachable = "No s'ha pogut connectar amb el servidor"
testFailed = "Ha fallat la prova de connexió"
configFetch = "No s'ha pogut obtenir la configuració del servidor. Comproveu l'URL i torneu-ho a provar."
[setup.server.error.securityDisabled]
title = "Inici de sessió no habilitat"
body = "Aquest servidor no té l'inici de sessió habilitat. Per connectar-hi, heu d'habilitar l'autenticació:"
step1 = "Establiu DOCKER_ENABLE_SECURITY=true al vostre entorn"
step2 = "O establiu security.enableLogin=true a settings.yml"
step3 = "Reinicieu el servidor"
[setup.login]
title = "Inicia sessió"
@@ -5905,6 +6024,7 @@ earlyAccess = "Accés anticipat"
reset = "Restableix els canvis"
downloadJson = "Descarrega JSON"
generatePdf = "Genera PDF"
saveChanges = "Deseu els canvis"
[pdfTextEditor.options.autoScaleText]
title = "Autoajusta el text a les caixes"
@@ -5942,6 +6062,8 @@ alpha = "Aquest visor alfa encara evoluciona—certs tipus de lletra, colors, ef
[pdfTextEditor.empty]
title = "No s'ha carregat cap document"
subtitle = "Carrega un fitxer PDF o JSON per començar a editar el contingut de text."
dropzone = "Arrossegueu i deixeu anar un fitxer PDF o JSON aquí, o feu clic per explorar"
dropzoneWithFiles = "Seleccioneu un fitxer de la pestanya Fitxers o arrossegueu i deixeu anar aquí un fitxer PDF o JSON, o feu clic per explorar"
[pdfTextEditor.welcomeBanner]
title = "Benvingut a l'Editor de text PDF (accés anticipat)"
+124 -2
View File
@@ -3036,6 +3036,91 @@ title = "Získat informace o PDF"
header = "Získat informace o PDF"
submit = "Získat informace"
downloadJson = "Stáhnout JSON"
processing = "Probíhá extrahování informací..."
results = "Výsledky"
noResults = "Spusťte nástroj pro vygenerování zprávy."
downloads = "Stažení"
noneDetected = "Nic nebylo zjištěno"
indexTitle = "Rejstřík"
[getPdfInfo.report]
entryLabel = "Úplné shrnutí informací"
shortTitle = "Informace o PDF"
[getPdfInfo.sections]
metadata = "Metadata"
formFields = "Formulářová pole"
basicInfo = "Základní informace"
documentInfo = "Informace o dokumentu"
compliance = "Shoda"
encryption = "Šifrování"
permissions = "Oprávnění"
other = "Ostatní"
perPageInfo = "Informace po stránkách"
tableOfContents = "Obsah"
[getPdfInfo.other]
attachments = "Přílohy"
embeddedFiles = "Vložené soubory"
javaScript = "JavaScript"
layers = "Vrstvy"
structureTree = "StructureTree"
xmp = "XMPMetadata"
[getPdfInfo.perPage]
size = "Velikost"
annotations = "Anotace"
images = "Obrázky"
links = "Odkazy"
fonts = "Písma"
xobjects = "Počty XObjectů"
multimedia = "Multimédia"
[getPdfInfo.summary]
pages = "Stránky"
fileSize = "Velikost souboru"
pdfVersion = "Verze PDF"
language = "Jazyk"
title = "Souhrn PDF"
author = "Autor"
created = "Vytvořeno"
modified = "Upraveno"
permsAll = "Všechna oprávnění povolena"
permsRestricted = "{{count}} omezení"
permsMixed = "Některá oprávnění jsou omezena"
hasCompliance = "Obsahuje standardy shody"
noCompliance = "Žádné standardy shody"
basic = "Základní informace"
documentInfo = "Informace o dokumentu"
securityTitle = "Stav zabezpečení"
technical = "Technické"
overviewTitle = "Přehled PDF"
[getPdfInfo.summary.security]
encrypted = "Šifrované PDF chráněno heslem"
unencrypted = "Nešifrované PDF bez ochrany heslem"
[getPdfInfo.summary.tech]
images = "Obrázky"
fonts = "Písma"
formFields = "Formulářová pole"
embeddedFiles = "Vložené soubory"
javaScript = "JavaScript"
layers = "Vrstvy"
bookmarks = "Záložky"
multimedia = "Multimédia"
[getPdfInfo.summary.overview]
untitled = "nepojmenovaný dokument"
unknown = "Neznámý autor"
text = "Toto je PDF o {{pages}} stránkách s názvem {{title}} od autora {{author}} (verze PDF {{version}})."
[getPdfInfo.error]
partial = "Některé soubory se nepodařilo zpracovat."
unexpected = "Během extrahování došlo k neočekávané chybě."
[getPdfInfo.status]
complete = "Extrahování dokončeno"
[extractPage]
tags = "extrahovat"
@@ -4176,7 +4261,7 @@ description = "Sledovat akce uživatelů a systémové události pro compliance
[admin.settings.security.audit.level]
label = "Úroveň auditu"
description = "0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE"
description = "0=VYPNUTO, 1=ZÁKLADNÍ, 2=STANDARD, 3=PODROBNÝ"
[admin.settings.security.audit.retentionDays]
label = "Doba uchování auditů (dny)"
@@ -5177,7 +5262,7 @@ upgrade = "Upgradovat nyní →"
freeTitle = "Serverová licence"
overLimitTitle = "Vyžadována serverová licence"
overLimitBody = "Naše licencování umožňuje až <strong>{{freeTierLimit}}</strong> uživatelů zdarma na server. Máte <strong>{{overLimitUserCopy}}</strong> uživatelů Stirling. Pro nepřerušené používání přejděte na plán Stirling Server <strong>neomezený počet míst</strong>, úpravy textu PDF a plná správa za 99 $/server/měsíc."
freeBody = "Naše licencování <strong>Open-Core</strong> umožňuje až <strong>{{freeTierLimit}}</strong> uživatelů zdarma na server. Pro nepřerušený růst a přednostní přístup k našemu novému <strong>nástroji pro úpravu textu PDF</strong> doporučujeme plán Stirling Server plné úpravy a <strong>neomezený počet míst</strong> za 99 $/server/měsíc."
freeBody = "Naše licencování <strong>Open-Core</strong> umožňuje až <strong>{{freeTierLimit}}</strong> uživatelů zdarma na server. Pro nepřerušované škálování doporučujeme plán Stirling Server - <strong>neomezený počet míst</strong> a <strong>podpora SSO</strong> za $99/server/měs."
[onboarding.desktopInstall]
title = "Stáhnout"
@@ -5282,6 +5367,31 @@ error = "Nepodařilo se aktualizovat stav uživatele"
success = "Uživatel úspěšně smazán"
error = "Nepodařilo se smazat uživatele"
[workspace.people.changePassword]
action = "Změnit heslo"
title = "Změna hesla"
subtitle = "Aktualizovat heslo pro"
newPassword = "Nové heslo"
confirmPassword = "Potvrzení hesla"
placeholder = "Zadejte nové heslo"
confirmPlaceholder = "Zadejte nové heslo znovu"
passwordRequired = "Zadejte prosím nové heslo"
passwordMismatch = "Hesla se neshodují"
generateRandom = "Vygenerovat bezpečné heslo"
generatedPreview = "Vygenerované heslo:"
copyTooltip = "Zkopírovat do schránky"
copiedToClipboard = "Heslo zkopírováno do schránky"
copyFailed = "Heslo se nepodařilo zkopírovat"
sendEmail = "Odeslat uživateli e-mail o této změně"
includePassword = "Zahrnout nové heslo do e-mailu"
forcePasswordChange = "Vynutit změnu hesla při příštím přihlášení"
emailUnavailable = "E-mailová adresa tohoto uživatele není platná. Oznámení jsou deaktivována."
smtpDisabled = "E-mailová oznámení vyžadují, aby bylo v nastavení povoleno SMTP."
notifyOnly = "Bude odeslán e-mail bez hesla, který uživateli oznámí, že ho změnil administrátor."
submit = "Aktualizovat heslo"
success = "Heslo bylo úspěšně aktualizováno"
error = "Heslo se nepodařilo aktualizovat"
[workspace.people.emailInvite]
tab = "Pozvánka emailem"
description = "Níže napište nebo vložte emaily oddělené čárkami. Uživatelé obdrží přihlašovací údaje emailem."
@@ -5815,6 +5925,7 @@ subtitle = "Přihlaste se svým účtem Stirling"
[setup.selfhosted]
title = "Přihlásit se k serveru"
subtitle = "Zadejte přihlašovací údaje k vašemu serveru"
link = "nebo se připojte k účtu s vlastním hostováním"
[setup.server]
title = "Připojit k serveru"
@@ -5833,6 +5944,14 @@ description = "Zadejte úplnou URL vašeho samohostovaného serveru Stirling PDF
emptyUrl = "Zadejte URL serveru"
unreachable = "Nelze se připojit k serveru"
testFailed = "Test připojení selhal"
configFetch = "Nepodařilo se načíst konfiguraci serveru. Zkontrolujte prosím URL a zkuste to znovu."
[setup.server.error.securityDisabled]
title = "Přihlášení není povoleno"
body = "Na tomto serveru není povoleno přihlašování. Pokud se chcete připojit, musíte povolit ověřování:"
step1 = "Nastavte DOCKER_ENABLE_SECURITY=true ve svém prostředí"
step2 = "Nebo nastavte security.enableLogin=true v souboru settings.yml"
step3 = "Restartujte server"
[setup.login]
title = "Přihlášení"
@@ -5905,6 +6024,7 @@ earlyAccess = "Předběžný přístup"
reset = "Obnovit změny"
downloadJson = "Stáhnout JSON"
generatePdf = "Vytvořit PDF"
saveChanges = "Uložit změny"
[pdfTextEditor.options.autoScaleText]
title = "Automaticky přizpůsobit text rámečkům"
@@ -5942,6 +6062,8 @@ alpha = "Tento alfa prohlížeč se stále vyvíjí — některé fonty, barvy,
[pdfTextEditor.empty]
title = "Není načten žádný dokument"
subtitle = "Načtěte soubor PDF nebo JSON a začněte upravovat text."
dropzone = "Sem přetáhněte soubor PDF nebo JSON, případně kliknutím vyberte"
dropzoneWithFiles = "Vyberte soubor na kartě Soubory, nebo sem přetáhněte soubor PDF či JSON, případně kliknutím vyberte"
[pdfTextEditor.welcomeBanner]
title = "Vítejte v editoru textu PDF (předběžný přístup)"
+128 -6
View File
@@ -1221,9 +1221,9 @@ pdfaDigitalSignatureWarning = "PDF'en indeholder en digital signatur. Dette vil
fileFormat = "Filformat"
wordDoc = "Word-dokument"
wordDocExt = "Word-dokument (.docx)"
odtExt = "OpenDocument Text (.odt)"
odtExt = "OpenDocument-tekst (.odt)"
pptExt = "PowerPoint (.pptx)"
odpExt = "OpenDocument Presentation (.odp)"
odpExt = "OpenDocument-præsentation (.odp)"
txtExt = "Almindelig tekst (.txt)"
rtfExt = "Rich Text Format (.rtf)"
selectedFiles = "Valgte filer"
@@ -3036,6 +3036,91 @@ title = "Få Info om PDF"
header = "Få Info om PDF"
submit = "Få Info"
downloadJson = "Download JSON"
processing = "Udtrækker oplysninger..."
results = "Resultater"
noResults = "Kør værktøjet for at generere en rapport."
downloads = "Downloads"
noneDetected = "Ingen registreret"
indexTitle = "Indeks"
[getPdfInfo.report]
entryLabel = "Fuldt informationsresumé"
shortTitle = "PDF-oplysninger"
[getPdfInfo.sections]
metadata = "Metadata"
formFields = "Formularfelter"
basicInfo = "Grundlæggende info"
documentInfo = "Dokumentinfo"
compliance = "Overensstemmelse"
encryption = "Kryptering"
permissions = "Tilladelser"
other = "Andet"
perPageInfo = "Info pr. side"
tableOfContents = "Indholdsfortegnelse"
[getPdfInfo.other]
attachments = "Vedhæftninger"
embeddedFiles = "Indlejrede filer"
javaScript = "JavaScript"
layers = "Lag"
structureTree = "StructureTree"
xmp = "XMPMetadata"
[getPdfInfo.perPage]
size = "Størrelse"
annotations = "Anmærkninger"
images = "Billeder"
links = "Links"
fonts = "Skrifttyper"
xobjects = "Antal XObjects"
multimedia = "Multimedie"
[getPdfInfo.summary]
pages = "Sider"
fileSize = "Filstørrelse"
pdfVersion = "PDF-version"
language = "Sprog"
title = "PDF-resumé"
author = "Forfatter"
created = "Oprettet"
modified = "Ændret"
permsAll = "Alle tilladelser tilladt"
permsRestricted = "{{count}} begrænsninger"
permsMixed = "Nogle tilladelser er begrænsede"
hasCompliance = "Har overensstemmelsesstandarder"
noCompliance = "Ingen overensstemmelsesstandarder"
basic = "Grundlæggende oplysninger"
documentInfo = "Dokumentoplysninger"
securityTitle = "Sikkerhedsstatus"
technical = "Teknisk"
overviewTitle = "PDF-oversigt"
[getPdfInfo.summary.security]
encrypted = "Krypteret PDF - med adgangskodebeskyttelse"
unencrypted = "Ukrypteret PDF - ingen adgangskodebeskyttelse"
[getPdfInfo.summary.tech]
images = "Billeder"
fonts = "Skrifttyper"
formFields = "Formularfelter"
embeddedFiles = "Indlejrede filer"
javaScript = "JavaScript"
layers = "Lag"
bookmarks = "Bogmærker"
multimedia = "Multimedie"
[getPdfInfo.summary.overview]
untitled = "et dokument uden titel"
unknown = "Ukendt forfatter"
text = "Dette er en PDF på {{pages}} sider med titlen {{title}}, oprettet af {{author}} (PDF-version {{version}})."
[getPdfInfo.error]
partial = "Nogle filer kunne ikke behandles."
unexpected = "Uventet fejl under udtrækning."
[getPdfInfo.status]
complete = "Udtrækning fuldført"
[extractPage]
tags = "udtræk"
@@ -3790,7 +3875,7 @@ version = "Nuværende udgivelse"
title = "API-dokumentation"
header = "API-dokumentation"
desc = "Se og test Stirling PDF API-endpoints"
tags = "api,documentation,swagger,endpoints,development"
tags = "api,dokumentation,swagger,endepunkter,udvikling"
[cookieBanner.popUp]
title = "Sådan bruger vi cookies"
@@ -4258,11 +4343,11 @@ label = "Issuer-URL"
description = "OAuth2-udbyderens issuer-URL"
[admin.settings.connections.oauth2.clientId]
label = "Client ID"
label = "Klient-ID"
description = "OAuth2 Client ID fra din udbyder"
[admin.settings.connections.oauth2.clientSecret]
label = "Client Secret"
label = "Klienthemmelighed"
description = "OAuth2 Client Secret fra din udbyder"
[admin.settings.connections.oauth2.useAsUsername]
@@ -5177,7 +5262,7 @@ upgrade = "Opgrader nu →"
freeTitle = "Serverlicens"
overLimitTitle = "Serverlicens påkrævet"
overLimitBody = "Vores licens tillader op til <strong>{{freeTierLimit}}</strong> brugere gratis pr. server. Du har <strong>{{overLimitUserCopy}}</strong> Stirling-brugere. For at fortsætte uden afbrydelser skal du opgradere til Stirling Server-abonnementet <strong>ubegrænsede pladser</strong>, PDF-tekstredigering og fuld admin-kontrol for $99/server/md."
freeBody = "Vores <strong>Open-Core</strong>-licens tillader op til <strong>{{freeTierLimit}}</strong> brugere gratis pr. server. For at skalere uden afbrydelser og få tidlig adgang til vores nye <strong>PDF-tekstredigeringsværktøj</strong> anbefaler vi Stirling Server-planen fuld redigering og <strong>ubegrænsede pladser</strong> for $99/server/md."
freeBody = "Vores <strong>Open-Core</strong>-licens tillader op til <strong>{{freeTierLimit}}</strong> brugere gratis pr. server. For at skalere uden afbrydelser anbefaler vi Stirling Server-planen <strong>ubegrænsede pladser</strong> og <strong>SSO-understøttelse</strong> for $99/server/md."
[onboarding.desktopInstall]
title = "Download"
@@ -5282,6 +5367,31 @@ error = "Kunne ikke opdatere brugerstatus"
success = "Bruger slettet"
error = "Kunne ikke slette bruger"
[workspace.people.changePassword]
action = "Skift adgangskode"
title = "Skift adgangskode"
subtitle = "Opdater adgangskoden for"
newPassword = "Ny adgangskode"
confirmPassword = "Bekræft adgangskode"
placeholder = "Indtast en ny adgangskode"
confirmPlaceholder = "Indtast den nye adgangskode igen"
passwordRequired = "Angiv en ny adgangskode"
passwordMismatch = "Adgangskoderne matcher ikke"
generateRandom = "Generér sikker adgangskode"
generatedPreview = "Genereret adgangskode:"
copyTooltip = "Kopiér til udklipsholder"
copiedToClipboard = "Adgangskode kopieret til udklipsholderen"
copyFailed = "Kunne ikke kopiere adgangskoden"
sendEmail = "Send en e-mail til brugeren om denne ændring"
includePassword = "Medtag den nye adgangskode i e-mailen"
forcePasswordChange = "Tving brugeren til at ændre adgangskode ved næste login"
emailUnavailable = "Denne brugers e-mail er ikke en gyldig e-mailadresse. Meddelelser er deaktiveret."
smtpDisabled = "E-mailmeddelelser kræver, at SMTP er aktiveret i indstillingerne."
notifyOnly = "Der sendes en e-mail uden adgangskoden, som informerer brugeren om, at en administrator har ændret den."
submit = "Opdater adgangskode"
success = "Adgangskoden blev opdateret"
error = "Kunne ikke opdatere adgangskoden"
[workspace.people.emailInvite]
tab = "E-mailinvitation"
description = "Skriv eller indsæt e-mails nedenfor, adskilt af kommaer. Brugere modtager loginoplysninger via e-mail."
@@ -5815,6 +5925,7 @@ subtitle = "Log ind med din Stirling-konto"
[setup.selfhosted]
title = "Log ind på server"
subtitle = "Indtast dine server-loginoplysninger"
link = "eller opret forbindelse til en selvhostet konto"
[setup.server]
title = "Forbind til server"
@@ -5833,6 +5944,14 @@ description = "Indtast den fulde URL til din selvhostede Stirling PDF-server"
emptyUrl = "Indtast en server-URL"
unreachable = "Kunne ikke forbinde til server"
testFailed = "Forbindelsestest mislykkedes"
configFetch = "Kunne ikke hente serverkonfiguration. Kontrollér URL'en, og prøv igen."
[setup.server.error.securityDisabled]
title = "Login ikke aktiveret"
body = "Denne server har ikke login aktiveret. For at oprette forbindelse til denne server skal du aktivere godkendelse:"
step1 = "Sæt DOCKER_ENABLE_SECURITY=true i dit miljø"
step2 = "Eller sæt security.enableLogin=true i settings.yml"
step3 = "Genstart serveren"
[setup.login]
title = "Log ind"
@@ -5905,6 +6024,7 @@ earlyAccess = "Tidlig adgang"
reset = "Nulstil ændringer"
downloadJson = "Download JSON"
generatePdf = "Generer PDF"
saveChanges = "Gem ændringer"
[pdfTextEditor.options.autoScaleText]
title = "Autoskalér tekst, så den passer i bokse"
@@ -5942,6 +6062,8 @@ alpha = "Denne alpha-fremviser er stadig under udvikling—visse skrifttyper, fa
[pdfTextEditor.empty]
title = "Intet dokument indlæst"
subtitle = "Indlæs en PDF- eller JSON-fil for at begynde at redigere tekstindhold."
dropzone = "Træk og slip en PDF- eller JSON-fil her, eller klik for at gennemse"
dropzoneWithFiles = "Vælg en fil fra fanen Filer, eller træk og slip en PDF- eller JSON-fil her, eller klik for at gennemse"
[pdfTextEditor.welcomeBanner]
title = "Velkommen til PDF-teksteditor (Early Access)"
+125 -3
View File
@@ -3036,6 +3036,91 @@ title = "Alle Informationen anzeigen"
header = "Alle Informationen anzeigen"
submit = "Informationen anzeigen"
downloadJson = "Als JSON herunterladen"
processing = "Informationen werden extrahiert..."
results = "Ergebnisse"
noResults = "Führen Sie das Tool aus, um einen Bericht zu erstellen."
downloads = "Downloads"
noneDetected = "Keine erkannt"
indexTitle = "Index"
[getPdfInfo.report]
entryLabel = "Vollständige Informationsübersicht"
shortTitle = "PDF-Informationen"
[getPdfInfo.sections]
metadata = "Metadaten"
formFields = "Formularfelder"
basicInfo = "Grundlegende Informationen"
documentInfo = "Dokumentinformationen"
compliance = "Compliance"
encryption = "Verschlüsselung"
permissions = "Berechtigungen"
other = "Sonstiges"
perPageInfo = "Informationen pro Seite"
tableOfContents = "Inhaltsverzeichnis"
[getPdfInfo.other]
attachments = "Anhänge"
embeddedFiles = "Eingebettete Dateien"
javaScript = "JavaScript"
layers = "Ebenen"
structureTree = "Strukturbaum"
xmp = "XMPMetadata"
[getPdfInfo.perPage]
size = "Größe"
annotations = "Anmerkungen"
images = "Bilder"
links = "Links"
fonts = "Schriftarten"
xobjects = "XObject-Anzahl"
multimedia = "Multimedia"
[getPdfInfo.summary]
pages = "Seiten"
fileSize = "Dateigröße"
pdfVersion = "PDF-Version"
language = "Sprache"
title = "PDF-Zusammenfassung"
author = "Autor"
created = "Erstellt"
modified = "Geändert"
permsAll = "Alle Berechtigungen erlaubt"
permsRestricted = "{{count}} Einschränkungen"
permsMixed = "Einige Berechtigungen eingeschränkt"
hasCompliance = "Entspricht Compliance-Standards"
noCompliance = "Keine Compliance-Standards"
basic = "Grundlegende Informationen"
documentInfo = "Dokumentinformationen"
securityTitle = "Sicherheitsstatus"
technical = "Technisch"
overviewTitle = "PDF-Übersicht"
[getPdfInfo.summary.security]
encrypted = "Verschlüsseltes PDF - Passwortschutz vorhanden"
unencrypted = "Unverschlüsseltes PDF - Kein Passwortschutz"
[getPdfInfo.summary.tech]
images = "Bilder"
fonts = "Schriftarten"
formFields = "Formularfelder"
embeddedFiles = "Eingebettete Dateien"
javaScript = "JavaScript"
layers = "Ebenen"
bookmarks = "Lesezeichen"
multimedia = "Multimedia"
[getPdfInfo.summary.overview]
untitled = "ein unbenanntes Dokument"
unknown = "Unbekannter Autor"
text = "Dies ist ein {{pages}}-seitiges PDF mit dem Titel {{title}}, erstellt von {{author}} (PDF-Version {{version}})."
[getPdfInfo.error]
partial = "Einige Dateien konnten nicht verarbeitet werden."
unexpected = "Unerwarteter Fehler während der Extraktion."
[getPdfInfo.status]
complete = "Extraktion abgeschlossen"
[extractPage]
tags = "extrahieren,seite"
@@ -5177,7 +5262,7 @@ upgrade = "Jetzt upgraden →"
freeTitle = "Server-Lizenz"
overLimitTitle = "Server-Lizenz erforderlich"
overLimitBody = "Unsere Lizenz erlaubt bis zu <strong>{{freeTierLimit}}</strong> Nutzer pro Server kostenlos. Sie haben <strong>{{overLimitUserCopy}}</strong> Stirling-Nutzer. Um ohne Unterbrechung fortzufahren, upgraden Sie auf den Stirling-Server-Plan <strong>unbegrenzte Plätze</strong>, PDF-Textbearbeitung und volle Admin-Kontrolle für $99/Server/Monat."
freeBody = "Unsere <strong>Open-Core</strong>-Lizenz erlaubt bis zu <strong>{{freeTierLimit}}</strong> Nutzer pro Server kostenlos. Für unterbrechungsfreies Skalieren und frühen Zugriff auf unser neues <strong>PDF-Textbearbeitungs-Tool</strong> empfehlen wir den Stirling-Server-Plan volle Bearbeitung und <strong>unbegrenzte Plätze</strong> für $99/Server/Monat."
freeBody = "Unsere <strong>Open-Core</strong>-Lizenz erlaubt bis zu <strong>{{freeTierLimit}}</strong> Nutzern pro Server kostenlos. Um unterbrechungsfrei zu skalieren, empfehlen wir den Stirling Server-Plan - <strong>unbegrenzte Plätze</strong> und <strong>SSO-Unterstützung</strong> für $99/Server/Monat."
[onboarding.desktopInstall]
title = "Download"
@@ -5282,6 +5367,31 @@ error = "Benutzerstatus konnte nicht aktualisiert werden"
success = "Benutzer erfolgreich gelöscht"
error = "Benutzer konnte nicht gelöscht werden"
[workspace.people.changePassword]
action = "Passwort ändern"
title = "Passwort ändern"
subtitle = "Passwort aktualisieren für"
newPassword = "Neues Passwort"
confirmPassword = "Passwort bestätigen"
placeholder = "Neues Passwort eingeben"
confirmPlaceholder = "Neues Passwort erneut eingeben"
passwordRequired = "Bitte geben Sie ein neues Passwort ein"
passwordMismatch = "Passwörter stimmen nicht überein"
generateRandom = "Sicheres Passwort generieren"
generatedPreview = "Generiertes Passwort:"
copyTooltip = "In Zwischenablage kopieren"
copiedToClipboard = "Passwort in die Zwischenablage kopiert"
copyFailed = "Kopieren des Passworts fehlgeschlagen"
sendEmail = "Den Benutzer per E-Mail über diese Änderung informieren"
includePassword = "Neues Passwort in die E-Mail aufnehmen"
forcePasswordChange = "Benutzer zwingen, das Passwort bei der nächsten Anmeldung zu ändern"
emailUnavailable = "Die E-Mail-Adresse dieses Benutzers ist keine gültige E-Mail-Adresse. Benachrichtigungen sind deaktiviert."
smtpDisabled = "E-Mail-Benachrichtigungen erfordern, dass SMTP in den Einstellungen aktiviert ist."
notifyOnly = "Es wird eine E-Mail ohne das Passwort gesendet, die den Benutzer darüber informiert, dass ein Admin es geändert hat."
submit = "Passwort aktualisieren"
success = "Passwort erfolgreich aktualisiert"
error = "Aktualisieren des Passworts fehlgeschlagen"
[workspace.people.emailInvite]
tab = "E-Mail-Einladung"
description = "Geben Sie unten E-Mails ein oder fügen Sie sie ein, getrennt durch Kommas. Benutzer erhalten Anmeldedaten per E-Mail."
@@ -5815,6 +5925,7 @@ subtitle = "Mit Ihrem Stirling-Konto anmelden"
[setup.selfhosted]
title = "Am Server anmelden"
subtitle = "Geben Sie Ihre Server-Anmeldedaten ein"
link = "oder mit einem selbstgehosteten Konto verbinden"
[setup.server]
title = "Mit Server verbinden"
@@ -5833,6 +5944,14 @@ description = "Geben Sie die vollständige URL Ihres selbst gehosteten Stirling
emptyUrl = "Bitte eine Server-URL eingeben"
unreachable = "Verbindung zum Server konnte nicht hergestellt werden"
testFailed = "Verbindungstest fehlgeschlagen"
configFetch = "Serverkonfiguration konnte nicht abgerufen werden. Bitte überprüfen Sie die URL und versuchen Sie es erneut."
[setup.server.error.securityDisabled]
title = "Anmeldung nicht aktiviert"
body = "Auf diesem Server ist die Anmeldung nicht aktiviert. Um eine Verbindung zu diesem Server herzustellen, müssen Sie die Authentifizierung aktivieren:"
step1 = "Setzen Sie DOCKER_ENABLE_SECURITY=true in Ihrer Umgebung"
step2 = "Oder setzen Sie security.enableLogin=true in der settings.yml"
step3 = "Starten Sie den Server neu"
[setup.login]
title = "Anmelden"
@@ -5905,6 +6024,7 @@ earlyAccess = "Früher Zugriff"
reset = "Änderungen zurücksetzen"
downloadJson = "JSON herunterladen"
generatePdf = "PDF generieren"
saveChanges = "Änderungen speichern"
[pdfTextEditor.options.autoScaleText]
title = "Text automatisch in Rahmen einpassen"
@@ -5942,6 +6062,8 @@ alpha = "Dieser Alpha-Viewer entwickelt sich noch weiter bestimmte Schriften
[pdfTextEditor.empty]
title = "Kein Dokument geladen"
subtitle = "Laden Sie eine PDF- oder JSON-Datei, um mit der Textbearbeitung zu beginnen."
dropzone = "Ziehen Sie eine PDF- oder JSON-Datei hierher, oder klicken Sie zum Durchsuchen"
dropzoneWithFiles = "Wählen Sie eine Datei auf der Registerkarte Dateien aus oder ziehen Sie eine PDF- oder JSON-Datei hierher, oder klicken Sie zum Durchsuchen"
[pdfTextEditor.welcomeBanner]
title = "Willkommen beim PDF-Texteditor (Early Access)"
@@ -6009,8 +6131,8 @@ tags = "text,anmerkung,beschriftung"
applySignatures = "Text anwenden"
[addText.text]
name = "Textinhalt"
placeholder = "Geben Sie den hinzuzufügenden Text ein"
name = "Text"
placeholder = "Text eingeben"
fontLabel = "Schriftart"
fontSizeLabel = "Schriftgröße"
fontSizePlaceholder = "Schriftgröße eingeben oder wählen (8-200)"
+125 -3
View File
@@ -3036,6 +3036,91 @@ title = "Λήψη πληροφοριών PDF"
header = "Λήψη πληροφοριών PDF"
submit = "Λήψη πληροφοριών"
downloadJson = "Λήψη JSON"
processing = "Εξαγωγή πληροφοριών..."
results = "Αποτελέσματα"
noResults = "Εκτελέστε το εργαλείο για να δημιουργήσετε αναφορά."
downloads = "Λήψεις"
noneDetected = "Δεν εντοπίστηκε κανένα"
indexTitle = "Ευρετήριο"
[getPdfInfo.report]
entryLabel = "Πλήρης σύνοψη πληροφοριών"
shortTitle = "Πληροφορίες PDF"
[getPdfInfo.sections]
metadata = "Μεταδεδομένα"
formFields = "Πεδία φόρμας"
basicInfo = "Βασικές πληροφορίες"
documentInfo = "Πληροφορίες εγγράφου"
compliance = "Συμμόρφωση"
encryption = "Κρυπτογράφηση"
permissions = "Δικαιώματα"
other = "Άλλα"
perPageInfo = "Πληροφορίες ανά σελίδα"
tableOfContents = "Πίνακας περιεχομένων"
[getPdfInfo.other]
attachments = "Συνημμένα"
embeddedFiles = "Ενσωματωμένα αρχεία"
javaScript = "JavaScript"
layers = "Επίπεδα"
structureTree = "Δέντρο δομής"
xmp = "XMPMetadata"
[getPdfInfo.perPage]
size = "Μέγεθος"
annotations = "Επισημειώσεις"
images = "Εικόνες"
links = "Σύνδεσμοι"
fonts = "Γραμματοσειρές"
xobjects = "Πλήθος XObject"
multimedia = "Πολυμέσα"
[getPdfInfo.summary]
pages = "Σελίδες"
fileSize = "Μέγεθος αρχείου"
pdfVersion = "Έκδοση PDF"
language = "Γλώσσα"
title = "Σύνοψη PDF"
author = "Συγγραφέας"
created = "Δημιουργήθηκε"
modified = "Τροποποιήθηκε"
permsAll = "Όλα τα δικαιώματα επιτρέπονται"
permsRestricted = "{{count}} περιορισμοί"
permsMixed = "Ορισμένα δικαιώματα είναι περιορισμένα"
hasCompliance = "Διαθέτει πρότυπα συμμόρφωσης"
noCompliance = "Χωρίς πρότυπα συμμόρφωσης"
basic = "Βασικές πληροφορίες"
documentInfo = "Πληροφορίες εγγράφου"
securityTitle = "Κατάσταση ασφάλειας"
technical = "Τεχνικά"
overviewTitle = "Επισκόπηση PDF"
[getPdfInfo.summary.security]
encrypted = "Κρυπτογραφημένο PDF - Υπάρχει προστασία με κωδικό πρόσβασης"
unencrypted = "Μη κρυπτογραφημένο PDF - Χωρίς προστασία με κωδικό πρόσβασης"
[getPdfInfo.summary.tech]
images = "Εικόνες"
fonts = "Γραμματοσειρές"
formFields = "Πεδία φόρμας"
embeddedFiles = "Ενσωματωμένα αρχεία"
javaScript = "JavaScript"
layers = "Επίπεδα"
bookmarks = "Σελιδοδείκτες"
multimedia = "Πολυμέσα"
[getPdfInfo.summary.overview]
untitled = "ένα έγγραφο χωρίς τίτλο"
unknown = "Άγνωστος συγγραφέας"
text = "Πρόκειται για ένα PDF {{pages}} σελίδων με τίτλο {{title}} που δημιουργήθηκε από τον/την {{author}} (έκδοση PDF {{version}})."
[getPdfInfo.error]
partial = "Δεν ήταν δυνατή η επεξεργασία ορισμένων αρχείων."
unexpected = "Μη αναμενόμενο σφάλμα κατά την εξαγωγή."
[getPdfInfo.status]
complete = "Η εξαγωγή ολοκληρώθηκε"
[extractPage]
tags = "εξαγωγή"
@@ -3790,7 +3875,7 @@ version = "Τρέχουσα έκδοση"
title = "Τεκμηρίωση API"
header = "Τεκμηρίωση API"
desc = "Προβάλετε και δοκιμάστε τα endpoints του Stirling PDF API"
tags = "api,documentation,swagger,endpoints,development"
tags = "api,τεκμηρίωση,swagger,τελικά σημεία,ανάπτυξη"
[cookieBanner.popUp]
title = "Πώς χρησιμοποιούμε τα cookies"
@@ -4482,7 +4567,7 @@ label = "Ενεργοποίηση προσκλήσεων μέσω email"
description = "Να επιτρέπεται στους διαχειριστές να προσκαλούν χρήστες μέσω email με αυτόματα παραγόμενους κωδικούς"
[admin.settings.mail.frontendUrl]
label = "Frontend URL"
label = "URL front-end"
description = "Βασικό URL για το frontend (π.χ. https://pdf.example.com). Χρησιμοποιείται για τη δημιουργία συνδέσμων πρόσκλησης στα email. Αφήστε κενό για χρήση του backend URL."
[admin.settings.legal]
@@ -5177,7 +5262,7 @@ upgrade = "Αναβάθμιση τώρα →"
freeTitle = "Άδεια διακομιστή"
overLimitTitle = "Απαιτείται άδεια διακομιστή"
overLimitBody = "Η αδειοδότηση μας επιτρέπει έως <strong>{{freeTierLimit}}</strong> χρήστες δωρεάν ανά διακομιστή. Έχετε <strong>{{overLimitUserCopy}}</strong> χρήστες Stirling. Για να συνεχίσετε χωρίς διακοπές, αναβαθμίστε στο πλάνο Stirling Server - <strong>απεριόριστες θέσεις</strong>, επεξεργασία κειμένου PDF και πλήρης έλεγχος διαχειριστή για $99/server/μήνα."
freeBody = "Η αδειοδότηση <strong>Open-Core</strong> μας επιτρέπει έως <strong>{{freeTierLimit}}</strong> χρήστες δωρεάν ανά διακομιστή. Για απρόσκοπτη κλιμάκωση και έγκαιρη πρόσβαση στο νέο <strong>εργαλείο επεξεργασίας κειμένου PDF</strong>, προτείνουμε το πλάνο Stirling Server - πλήρης επεξεργασία και <strong>απεριόριστες θέσεις</strong> για $99/server/μήνα."
freeBody = "Οι άδειες χρήσης <strong>Open-Core</strong> επιτρέπουν έως και <strong>{{freeTierLimit}}</strong> χρήστες δωρεάν ανά διακομιστή. Για απρόσκοπτη κλιμάκωση, προτείνουμε το πλάνο Stirling Server - <strong>απεριόριστες θέσεις</strong> και <strong>υποστήριξη SSO</strong> με $99/διακομιστή/μήνα."
[onboarding.desktopInstall]
title = "Λήψη"
@@ -5282,6 +5367,31 @@ error = "Αποτυχία ενημέρωσης κατάστασης χρήστη
success = "Ο χρήστης διαγράφηκε με επιτυχία"
error = "Αποτυχία διαγραφής χρήστη"
[workspace.people.changePassword]
action = "Αλλαγή κωδικού πρόσβασης"
title = "Αλλαγή κωδικού πρόσβασης"
subtitle = "Ενημέρωση κωδικού πρόσβασης για"
newPassword = "Νέος κωδικός πρόσβασης"
confirmPassword = "Επιβεβαίωση κωδικού πρόσβασης"
placeholder = "Εισαγάγετε νέο κωδικό πρόσβασης"
confirmPlaceholder = "Εισαγάγετε ξανά τον νέο κωδικό πρόσβασης"
passwordRequired = "Παρακαλούμε εισαγάγετε νέο κωδικό πρόσβασης"
passwordMismatch = "Οι κωδικοί πρόσβασης δεν ταιριάζουν"
generateRandom = "Δημιουργία ασφαλούς κωδικού πρόσβασης"
generatedPreview = "Δημιουργημένος κωδικός πρόσβασης:"
copyTooltip = "Αντιγραφή στο πρόχειρο"
copiedToClipboard = "Ο κωδικός πρόσβασης αντιγράφηκε στο πρόχειρο"
copyFailed = "Αποτυχία αντιγραφής κωδικού πρόσβασης"
sendEmail = "Αποστολή email στον χρήστη για αυτήν την αλλαγή"
includePassword = "Να συμπεριληφθεί ο νέος κωδικός πρόσβασης στο email"
forcePasswordChange = "Υποχρεωτική αλλαγή κωδικού πρόσβασης κατά την επόμενη σύνδεση"
emailUnavailable = "Το email αυτού του χρήστη δεν είναι έγκυρη διεύθυνση email. Οι ειδοποιήσεις είναι απενεργοποιημένες."
smtpDisabled = "Οι ειδοποιήσεις μέσω email απαιτούν την ενεργοποίηση του SMTP στις ρυθμίσεις."
notifyOnly = "Θα σταλεί email χωρίς τον κωδικό πρόσβασης, ενημερώνοντας τον χρήστη ότι ένας διαχειριστής τον άλλαξε."
submit = "Ενημέρωση κωδικού πρόσβασης"
success = "Ο κωδικός πρόσβασης ενημερώθηκε με επιτυχία"
error = "Αποτυχία ενημέρωσης κωδικού πρόσβασης"
[workspace.people.emailInvite]
tab = "Πρόσκληση μέσω Email"
description = "Πληκτρολογήστε ή επικολλήστε emails παρακάτω, χωρισμένα με κόμμα. Οι χρήστες θα λάβουν στοιχεία σύνδεσης μέσω email."
@@ -5815,6 +5925,7 @@ subtitle = "Συνδεθείτε με τον λογαριασμό Stirling"
[setup.selfhosted]
title = "Σύνδεση στον διακομιστή"
subtitle = "Εισαγάγετε τα διαπιστευτήρια του διακομιστή σας"
link = "ή συνδεθείτε σε έναν self-hosted λογαριασμό"
[setup.server]
title = "Σύνδεση σε διακομιστή"
@@ -5833,6 +5944,14 @@ description = "Εισαγάγετε το πλήρες URL του self-hosted δ
emptyUrl = "Εισαγάγετε URL διακομιστή"
unreachable = "Αδυναμία σύνδεσης με τον διακομιστή"
testFailed = "Αποτυχία ελέγχου σύνδεσης"
configFetch = "Αποτυχία ανάκτησης της διαμόρφωσης του διακομιστή. Ελέγξτε το URL και δοκιμάστε ξανά."
[setup.server.error.securityDisabled]
title = "Η σύνδεση δεν είναι ενεργοποιημένη"
body = "Σε αυτόν τον διακομιστή δεν είναι ενεργοποιημένη η σύνδεση. Για να συνδεθείτε σε αυτόν τον διακομιστή, πρέπει να ενεργοποιήσετε τον έλεγχο ταυτότητας:"
step1 = "Ορίστε το DOCKER_ENABLE_SECURITY=true στο περιβάλλον σας"
step2 = "Ή ορίστε security.enableLogin=true στο settings.yml"
step3 = "Επανεκκινήστε τον διακομιστή"
[setup.login]
title = "Σύνδεση"
@@ -5905,6 +6024,7 @@ earlyAccess = "Πρόωρη πρόσβαση"
reset = "Επαναφορά αλλαγών"
downloadJson = "Λήψη JSON"
generatePdf = "Δημιουργία PDF"
saveChanges = "Αποθήκευση αλλαγών"
[pdfTextEditor.options.autoScaleText]
title = "Αυτόματη προσαρμογή κειμένου στα πλαίσια"
@@ -5942,6 +6062,8 @@ alpha = "Αυτός ο προβολέας άλφα εξελίσσεται ακό
[pdfTextEditor.empty]
title = "Δεν φορτώθηκε έγγραφο"
subtitle = "Φορτώστε ένα αρχείο PDF ή JSON για να ξεκινήσετε την επεξεργασία κειμένου."
dropzone = "Σύρετε και αποθέστε εδώ ένα αρχείο PDF ή JSON, ή κάντε κλικ για περιήγηση"
dropzoneWithFiles = "Επιλέξτε ένα αρχείο από την καρτέλα Αρχεία, ή σύρετε και αποθέστε εδώ ένα αρχείο PDF ή JSON, ή κάντε κλικ για περιήγηση"
[pdfTextEditor.welcomeBanner]
title = "Καλώς ορίσατε στο PDF Text Editor (Πρώιμη πρόσβαση)"
+170 -3
View File
@@ -435,6 +435,24 @@ latestVersion = "Latest Version"
checkForUpdates = "Check for Updates"
viewDetails = "View Details"
[settings.security]
title = "Security"
description = "Update your password to keep your account secure."
[settings.security.password]
subtitle = "Change your password. You will be logged out after updating."
required = "All fields are required."
mismatch = "New passwords do not match."
error = "Unable to update password. Please verify your current password and try again."
success = "Password updated successfully. Please sign in again."
current = "Current password"
currentPlaceholder = "Enter your current password"
new = "New password"
newPlaceholder = "Enter a new password"
confirm = "Confirm new password"
confirmPlaceholder = "Re-enter your new password"
update = "Update password"
[settings.hotkeys]
title = "Keyboard Shortcuts"
description = "Customize keyboard shortcuts for quick tool access. Click \"Change shortcut\" and press a new key combination. Press Esc to cancel."
@@ -493,6 +511,10 @@ oldPassword = "Current Password"
newPassword = "New Password"
confirmNewPassword = "Confirm New Password"
submit = "Submit Changes"
credsUpdated = "Account updated"
description = "Changes saved. Please log in again."
error = "Unable to update username. Please verify your password and try again."
changeUsername = "Update your username. You will be logged out after updating."
[account]
title = "Account Settings"
@@ -3036,6 +3058,91 @@ title = "Get Info on PDF"
header = "Get Info on PDF"
submit = "Get Info"
downloadJson = "Download JSON"
processing = "Extracting information..."
results = "Results"
noResults = "Run the tool to generate a report."
downloads = "Downloads"
noneDetected = "None detected"
indexTitle = "Index"
[getPdfInfo.report]
entryLabel = "Full information summary"
shortTitle = "PDF Information"
[getPdfInfo.sections]
metadata = "Metadata"
formFields = "Form Fields"
basicInfo = "Basic Info"
documentInfo = "Document Info"
compliance = "Compliance"
encryption = "Encryption"
permissions = "Permissions"
other = "Other"
perPageInfo = "Per Page Info"
tableOfContents = "Table of Contents"
[getPdfInfo.other]
attachments = "Attachments"
embeddedFiles = "Embedded Files"
javaScript = "JavaScript"
layers = "Layers"
structureTree = "StructureTree"
xmp = "XMPMetadata"
[getPdfInfo.perPage]
size = "Size"
annotations = "Annotations"
images = "Images"
links = "Links"
fonts = "Fonts"
xobjects = "XObject Counts"
multimedia = "Multimedia"
[getPdfInfo.summary]
pages = "Pages"
fileSize = "File Size"
pdfVersion = "PDF Version"
language = "Language"
title = "PDF Summary"
author = "Author"
created = "Created"
modified = "Modified"
permsAll = "All Permissions Allowed"
permsRestricted = "{{count}} restrictions"
permsMixed = "Some permissions restricted"
hasCompliance = "Has compliance standards"
noCompliance = "No Compliance Standards"
basic = "Basic Information"
documentInfo = "Document Information"
securityTitle = "Security Status"
technical = "Technical"
overviewTitle = "PDF Overview"
[getPdfInfo.summary.security]
encrypted = "Encrypted PDF - Password protection present"
unencrypted = "Unencrypted PDF - No password protection"
[getPdfInfo.summary.tech]
images = "Images"
fonts = "Fonts"
formFields = "Form Fields"
embeddedFiles = "Embedded Files"
javaScript = "JavaScript"
layers = "Layers"
bookmarks = "Bookmarks"
multimedia = "Multimedia"
[getPdfInfo.summary.overview]
untitled = "an untitled document"
unknown = "Unknown Author"
text = "This is a {{pages}}-page PDF titled {{title}} created by {{author}} (PDF version {{version}})."
[getPdfInfo.error]
partial = "Some files could not be processed."
unexpected = "Unexpected error during extraction."
[getPdfInfo.status]
complete = "Extraction complete"
[extractPage]
tags = "extract"
@@ -3454,8 +3561,8 @@ signinTitle = "Please sign in"
ssoSignIn = "Login via Single Sign-on"
oAuth2AutoCreateDisabled = "OAUTH2 Auto-Create User Disabled"
oAuth2AdminBlockedUser = "Registration or logging in of non-registered users is currently blocked. Please contact the administrator."
oAuth2RequiresLicense = "OAuth/SSO login requires a paid license (Server or Enterprise). Please contact the administrator to upgrade your plan."
saml2RequiresLicense = "SAML login requires a paid license (Server or Enterprise). Please contact the administrator to upgrade your plan."
oAuth2RequiresLicense = "OAuth/SSO login requires a Server or Enterprise license. Please contact the administrator to upgrade your plan."
saml2RequiresLicense = "SAML login requires an Enterprise license. Please contact the administrator to upgrade your plan."
maxUsersReached = "Maximum number of users reached for your current license. Please contact the administrator to upgrade your plan or add more seats."
oauth2RequestNotFound = "Authorization request not found"
oauth2InvalidUserInfoResponse = "Invalid User Info Response"
@@ -4985,6 +5092,7 @@ loading = "Loading..."
back = "Back"
continue = "Continue"
error = "Error"
save = "Save"
[config.overview]
title = "Application Configuration"
@@ -5178,7 +5286,7 @@ upgrade = "Upgrade now →"
freeTitle = "Server License"
overLimitTitle = "Server License Needed"
overLimitBody = "Our licensing permits up to <strong>{{freeTierLimit}}</strong> users for free per server. You have <strong>{{overLimitUserCopy}}</strong> Stirling users. To continue uninterrupted, upgrade to the Stirling Server plan - <strong>unlimited seats</strong>, PDF text editing, and full admin control for $99/server/mo."
freeBody = "Our <strong>Open-Core</strong> licensing permits up to <strong>{{freeTierLimit}}</strong> users for free per server. To scale uninterrupted and get early access to our new <strong>PDF text editing tool</strong>, we recommend the Stirling Server plan - full editing and <strong>unlimited seats</strong> for $99/server/mo."
freeBody = "Our <strong>Open-Core</strong> licensing permits up to <strong>{{freeTierLimit}}</strong> users for free per server. To scale uninterrupted, we recommend the Stirling Server plan - <strong>unlimited seats</strong> and <strong>SSO support</strong> for $99/server/mo."
[onboarding.desktopInstall]
title = "Download"
@@ -5283,6 +5391,31 @@ error = "Failed to update user status"
success = "User deleted successfully"
error = "Failed to delete user"
[workspace.people.changePassword]
action = "Change password"
title = "Change password"
subtitle = "Update the password for"
newPassword = "New password"
confirmPassword = "Confirm password"
placeholder = "Enter a new password"
confirmPlaceholder = "Re-enter the new password"
passwordRequired = "Please enter a new password"
passwordMismatch = "Passwords do not match"
generateRandom = "Generate secure password"
generatedPreview = "Generated password:"
copyTooltip = "Copy to clipboard"
copiedToClipboard = "Password copied to clipboard"
copyFailed = "Failed to copy password"
sendEmail = "Email the user about this change"
includePassword = "Include the new password in the email"
forcePasswordChange = "Force user to change password on next login"
emailUnavailable = "This user's email is not a valid email address. Notifications are disabled."
smtpDisabled = "Email notifications require SMTP to be enabled in settings."
notifyOnly = "An email will be sent without the password, letting the user know an admin changed it."
submit = "Update password"
success = "Password updated successfully"
error = "Failed to update password"
[workspace.people.emailInvite]
tab = "Email Invite"
description = "Type or paste in emails below, separated by commas. Users will receive login credentials via email."
@@ -5459,6 +5592,28 @@ contactSales = "Contact Sales"
contactToUpgrade = "Contact us to upgrade or customize your plan"
maxUsers = "Max Users"
upTo = "Up to"
getLicense = "Get Server License"
upgradeToEnterprise = "Upgrade to Enterprise"
selectPeriod = "Select Billing Period"
monthlyBilling = "Monthly Billing"
yearlyBilling = "Yearly Billing"
checkoutOpened = "Checkout Opened"
checkoutInstructions = "Complete your purchase in the Stripe tab. After payment, return here and refresh the page to activate your license. You will also receive an email with your license key."
activateLicense = "Activate Your License"
[plan.static.licenseActivation]
checkoutOpened = "Checkout Opened in New Tab"
instructions = "Complete your purchase in the Stripe tab. Once your payment is complete, you will receive an email with your license key."
enterKey = "Enter your license key below to activate your plan:"
keyDescription = "Paste the license key from your email"
activate = "Activate License"
doLater = "I'll do this later"
success = "License Activated!"
successMessage = "Your license has been successfully activated. You can now close this window."
[plan.static.billingPortal]
title = "Email Verification Required"
message = "You will need to verify your email address in the Stripe billing portal. Check your email for a login link."
[plan.period]
month = "month"
@@ -5816,6 +5971,7 @@ subtitle = "Sign in with your Stirling account"
[setup.selfhosted]
title = "Sign in to Server"
subtitle = "Enter your server credentials"
link = "or connect to a self-hosted account"
[setup.server]
title = "Connect to Server"
@@ -5834,6 +5990,14 @@ description = "Enter the full URL of your self-hosted Stirling PDF server"
emptyUrl = "Please enter a server URL"
unreachable = "Could not connect to server"
testFailed = "Connection test failed"
configFetch = "Failed to fetch server configuration. Please check the URL and try again."
[setup.server.error.securityDisabled]
title = "Login Not Enabled"
body = "This server does not have login enabled. To connect to this server, you must enable authentication:"
step1 = "Set DOCKER_ENABLE_SECURITY=true in your environment"
step2 = "Or set security.enableLogin=true in settings.yml"
step3 = "Restart the server"
[setup.login]
title = "Sign In"
@@ -5906,6 +6070,7 @@ earlyAccess = "Early Access"
reset = "Reset Changes"
downloadJson = "Download JSON"
generatePdf = "Generate PDF"
saveChanges = "Save Changes"
[pdfTextEditor.options.autoScaleText]
title = "Auto-scale text to fit boxes"
@@ -5943,6 +6108,8 @@ alpha = "This alpha viewer is still evolving—certain fonts, colours, transpare
[pdfTextEditor.empty]
title = "No document loaded"
subtitle = "Load a PDF or JSON file to begin editing text content."
dropzone = "Drag and drop a PDF or JSON file here, or click to browse"
dropzoneWithFiles = "Select a file from the Files tab, or drag and drop a PDF or JSON file here, or click to browse"
[pdfTextEditor.welcomeBanner]
title = "Welcome to PDF Text Editor (Early Access)"
+123 -1
View File
@@ -3036,6 +3036,91 @@ title = "Obtener Información del PDF"
header = "Obtener Información del PDF"
submit = "Obtener Información"
downloadJson = "Descargar JSON"
processing = "Extrayendo información..."
results = "Resultados"
noResults = "Ejecute la herramienta para generar un informe."
downloads = "Descargas"
noneDetected = "Ninguno detectado"
indexTitle = "Índice"
[getPdfInfo.report]
entryLabel = "Resumen completo de información"
shortTitle = "Información del PDF"
[getPdfInfo.sections]
metadata = "Metadatos"
formFields = "Campos de formulario"
basicInfo = "Información básica"
documentInfo = "Información del documento"
compliance = "Conformidad"
encryption = "Cifrado"
permissions = "Permisos"
other = "Otros"
perPageInfo = "Información por página"
tableOfContents = "Tabla de contenidos"
[getPdfInfo.other]
attachments = "Adjuntos"
embeddedFiles = "Archivos incrustados"
javaScript = "JavaScript"
layers = "Capas"
structureTree = "Árbol de estructura"
xmp = "Metadatos XMP"
[getPdfInfo.perPage]
size = "Tamaño"
annotations = "Anotaciones"
images = "Imágenes"
links = "Enlaces"
fonts = "Fuentes"
xobjects = "Recuento de XObject"
multimedia = "Multimedia"
[getPdfInfo.summary]
pages = "Páginas"
fileSize = "Tamaño del archivo"
pdfVersion = "Versión de PDF"
language = "Idioma"
title = "Resumen del PDF"
author = "Autor"
created = "Creado"
modified = "Modificado"
permsAll = "Todos los permisos permitidos"
permsRestricted = "{{count}} restricciones"
permsMixed = "Algunos permisos restringidos"
hasCompliance = "Cumple con estándares"
noCompliance = "Sin estándares de conformidad"
basic = "Información básica"
documentInfo = "Información del documento"
securityTitle = "Estado de seguridad"
technical = "Técnico"
overviewTitle = "Vista general del PDF"
[getPdfInfo.summary.security]
encrypted = "PDF cifrado: protección con contraseña presente"
unencrypted = "PDF sin cifrar: sin protección con contraseña"
[getPdfInfo.summary.tech]
images = "Imágenes"
fonts = "Fuentes"
formFields = "Campos de formulario"
embeddedFiles = "Archivos incrustados"
javaScript = "JavaScript"
layers = "Capas"
bookmarks = "Marcadores"
multimedia = "Multimedia"
[getPdfInfo.summary.overview]
untitled = "un documento sin título"
unknown = "Autor desconocido"
text = "Este es un PDF de {{pages}} páginas titulado {{title}} creado por {{author}} (versión de PDF {{version}})."
[getPdfInfo.error]
partial = "Algunos archivos no se pudieron procesar."
unexpected = "Error inesperado durante la extracción."
[getPdfInfo.status]
complete = "Extracción completada"
[extractPage]
tags = "extraer"
@@ -5177,7 +5262,7 @@ upgrade = "Actualizar ahora →"
freeTitle = "Licencia del servidor"
overLimitTitle = "Se necesita licencia de servidor"
overLimitBody = "Nuestra licencia permite hasta <strong>{{freeTierLimit}}</strong> usuarios gratis por servidor. Tiene <strong>{{overLimitUserCopy}}</strong> usuarios de Stirling. Para continuar sin interrupciones, actualice al plan Stirling Server: <strong>plazas ilimitadas</strong>, edición de texto PDF y control total de administración por 99 $/servidor/mes."
freeBody = "Nuestra licencia <strong>Open-Core</strong> permite hasta <strong>{{freeTierLimit}}</strong> usuarios gratis por servidor. Para escalar sin interrupciones y obtener acceso anticipado a nuestra nueva <strong>herramienta de edición de texto PDF</strong>, recomendamos el plan Stirling Server: edición completa y <strong>plazas ilimitadas</strong> por 99 $/servidor/mes."
freeBody = "Nuestra licencia <strong>Open-Core</strong> permite hasta <strong>{{freeTierLimit}}</strong> usuarios gratis por servidor. Para escalar sin interrupciones, recomendamos el plan Stirling Server - <strong>plazas ilimitadas</strong> y <strong>soporte SSO</strong> por $99/servidor/mes."
[onboarding.desktopInstall]
title = "Descargar"
@@ -5282,6 +5367,31 @@ error = "No se pudo actualizar el estado del usuario"
success = "Usuario eliminado correctamente"
error = "No se pudo eliminar el usuario"
[workspace.people.changePassword]
action = "Cambiar contraseña"
title = "Cambiar contraseña"
subtitle = "Actualizar la contraseña de"
newPassword = "Nueva contraseña"
confirmPassword = "Confirmar contraseña"
placeholder = "Introduzca una nueva contraseña"
confirmPlaceholder = "Vuelva a introducir la nueva contraseña"
passwordRequired = "Introduzca una nueva contraseña"
passwordMismatch = "Las contraseñas no coinciden"
generateRandom = "Generar contraseña segura"
generatedPreview = "Contraseña generada:"
copyTooltip = "Copiar al portapapeles"
copiedToClipboard = "Contraseña copiada al portapapeles"
copyFailed = "Error al copiar la contraseña"
sendEmail = "Enviar un correo al usuario sobre este cambio"
includePassword = "Incluir la nueva contraseña en el correo"
forcePasswordChange = "Obligar al usuario a cambiar la contraseña en el próximo inicio de sesión"
emailUnavailable = "El correo de este usuario no es una dirección válida. Las notificaciones están desactivadas."
smtpDisabled = "Las notificaciones por correo requieren que SMTP esté habilitado en la configuración."
notifyOnly = "Se enviará un correo sin la contraseña, informando al usuario de que un administrador la cambió."
submit = "Actualizar contraseña"
success = "Contraseña actualizada correctamente"
error = "No se pudo actualizar la contraseña"
[workspace.people.emailInvite]
tab = "Invitación por correo electrónico"
description = "Escribe o pega correos a continuación, separados por comas. Los usuarios recibirán credenciales de inicio de sesión por correo electrónico."
@@ -5815,6 +5925,7 @@ subtitle = "Inicie sesión con su cuenta de Stirling"
[setup.selfhosted]
title = "Inicie sesión en el servidor"
subtitle = "Introduzca las credenciales de su servidor"
link = "o conectarse a una cuenta autoalojada"
[setup.server]
title = "Conectar con el servidor"
@@ -5833,6 +5944,14 @@ description = "Introduzca la URL completa de su servidor autoalojado de Stirling
emptyUrl = "Introduzca una URL de servidor"
unreachable = "No se pudo conectar con el servidor"
testFailed = "Falló la prueba de conexión"
configFetch = "No se pudo obtener la configuración del servidor. Compruebe la URL e inténtelo de nuevo."
[setup.server.error.securityDisabled]
title = "Inicio de sesión no habilitado"
body = "Este servidor no tiene habilitado el inicio de sesión. Para conectarse a este servidor, debe habilitar la autenticación:"
step1 = "Establezca DOCKER_ENABLE_SECURITY=true en su entorno"
step2 = "O establezca security.enableLogin=true en settings.yml"
step3 = "Reinicie el servidor"
[setup.login]
title = "Iniciar sesión"
@@ -5905,6 +6024,7 @@ earlyAccess = "Acceso anticipado"
reset = "Restablecer cambios"
downloadJson = "Descargar JSON"
generatePdf = "Generar PDF"
saveChanges = "Guardar cambios"
[pdfTextEditor.options.autoScaleText]
title = "Escalar texto automáticamente para ajustar a las cajas"
@@ -5942,6 +6062,8 @@ alpha = "Este visor alfa sigue evolucionando: ciertas fuentes, colores, efectos
[pdfTextEditor.empty]
title = "Ningún documento cargado"
subtitle = "Carga un archivo PDF o JSON para empezar a editar el contenido de texto."
dropzone = "Arrastre y suelte un archivo PDF o JSON aquí, o haga clic para explorar"
dropzoneWithFiles = "Seleccione un archivo de la pestaña Archivos, o arrastre y suelte un archivo PDF o JSON aquí, o haga clic para explorar"
[pdfTextEditor.welcomeBanner]
title = "Bienvenido a PDF Text Editor (Acceso anticipado)"
+123 -1
View File
@@ -3036,6 +3036,91 @@ title = "Lortu informazioa PDFn"
header = "Lortu informazioa PDFn"
submit = "Lortu informazioa"
downloadJson = "Deskargatu JSON"
processing = "Informazioa erauzten..."
results = "Emaitzak"
noResults = "Exekutatu tresna txosten bat sortzeko."
downloads = "Deskargak"
noneDetected = "Ez da ezer detektatu"
indexTitle = "Indizea"
[getPdfInfo.report]
entryLabel = "Informazio osoaren laburpena"
shortTitle = "PDFren informazioa"
[getPdfInfo.sections]
metadata = "Metadatuak"
formFields = "Inprimaki-eremuak"
basicInfo = "Oinarrizko informazioa"
documentInfo = "Dokumentuaren informazioa"
compliance = "Arauen betetzea"
encryption = "Zifratzea"
permissions = "Baimenak"
other = "Bestelakoak"
perPageInfo = "Orrialdeko informazioa"
tableOfContents = "Aurkibidea"
[getPdfInfo.other]
attachments = "Eranskinak"
embeddedFiles = "Txertatutako fitxategiak"
javaScript = "JavaScript"
layers = "Geruzak"
structureTree = "StructureTree"
xmp = "XMPMetadata"
[getPdfInfo.perPage]
size = "Tamaina"
annotations = "Anotazioak"
images = "Irudiak"
links = "Estekak"
fonts = "Letra-tipoak"
xobjects = "XObject kopuruak"
multimedia = "Multimedia"
[getPdfInfo.summary]
pages = "Orriak"
fileSize = "Fitxategi-tamaina"
pdfVersion = "PDF bertsioa"
language = "Hizkuntza"
title = "PDFren laburpena"
author = "Egilea"
created = "Sortua"
modified = "Aldatua"
permsAll = "Baimen guztiak baimenduta"
permsRestricted = "{{count}} murrizketa"
permsMixed = "Zenbait baimen murriztuta"
hasCompliance = "Betetze-estandarrak ditu"
noCompliance = "Ez dago betetze-estandarrik"
basic = "Oinarrizko informazioa"
documentInfo = "Dokumentuaren informazioa"
securityTitle = "Segurtasun-egoera"
technical = "Teknikoa"
overviewTitle = "PDFren ikuspegi orokorra"
[getPdfInfo.summary.security]
encrypted = "Zifratutako PDFa - Pasahitz-babesa dago"
unencrypted = "Zifratu gabeko PDFa - Ez dago pasahitz-babesik"
[getPdfInfo.summary.tech]
images = "Irudiak"
fonts = "Letra-tipoak"
formFields = "Inprimaki-eremuak"
embeddedFiles = "Txertatutako fitxategiak"
javaScript = "JavaScript"
layers = "Geruzak"
bookmarks = "Laster-markak"
multimedia = "Multimedia"
[getPdfInfo.summary.overview]
untitled = "izenbururik gabeko dokumentu bat"
unknown = "Egile ezezaguna"
text = "Hau {{pages}} orrialdeko PDF bat da; izenburua: {{title}}, egilea: {{author}} (PDF bertsioa: {{version}})."
[getPdfInfo.error]
partial = "Fitxategi batzuk ezin izan dira prozesatu."
unexpected = "Ustekabeko errorea erauzketan zehar."
[getPdfInfo.status]
complete = "Erau zketa amaituta"
[extractPage]
tags = "erauzi"
@@ -5177,7 +5262,7 @@ upgrade = "Eguneratu orain →"
freeTitle = "Zerbitzari-lizentzia"
overLimitTitle = "Beharrezkoa da zerbitzari-lizentzia"
overLimitBody = "Gure lizentziak baimentzen ditu <strong>{{freeTierLimit}}</strong> erabiltzaile doan zerbitzari bakoitzeko. <strong>{{overLimitUserCopy}}</strong> Stirling erabiltzaile dituzu. Jarraitzeko etenik gabe, eguneratu Stirling Server planera - <strong>eserleku mugagabeak</strong>, PDF testu-edizioa, eta admin kontrol osoa $99/zerbitzari/hilean."
freeBody = "Gure <strong>Open-Core</strong> lizentziak <strong>{{freeTierLimit}}</strong> erabiltzaile arte baimentzen ditu doan zerbitzari bakoitzeko. Etenik gabe eskalatzeko eta gure <strong>PDF testu-edizio tresna</strong> berrirako sarbide goiztiarra lortzeko, gomendatzen dugu Stirling Server plana - edizio osoa eta <strong>eserleku mugagabeak</strong> $99/zerbitzari/hilean."
freeBody = "Gure <strong>Open-Core</strong> lizentziak zerbitzari bakoitzeko doan gehienez <strong>{{freeTierLimit}}</strong> erabiltzaile baimentzen ditu. Etenik gabe eskalatzeko, Stirling Server plana gomendatzen dugu - <strong>eserleku mugagabeak</strong> eta <strong>SSO euskarria</strong> $99/server/mo."
[onboarding.desktopInstall]
title = "Deskargatu"
@@ -5282,6 +5367,31 @@ error = "Ezin izan da erabiltzailearen egoera eguneratu"
success = "Erabiltzailea ongi ezabatu da"
error = "Ezin izan da erabiltzailea ezabatu"
[workspace.people.changePassword]
action = "Pasahitza aldatu"
title = "Pasahitza aldatu"
subtitle = "Honetarako pasahitza eguneratu"
newPassword = "Pasahitz berria"
confirmPassword = "Berretsi pasahitza"
placeholder = "Sartu pasahitz berria"
confirmPlaceholder = "Sartu berriro pasahitz berria"
passwordRequired = "Sartu pasahitz berria"
passwordMismatch = "Pasahitzak ez datoz bat"
generateRandom = "Sortu pasahitz segurua"
generatedPreview = "Sortutako pasahitza:"
copyTooltip = "Kopiatu arbelera"
copiedToClipboard = "Pasahitza arbelera kopiatu da"
copyFailed = "Pasahitza kopiatzeak huts egin du"
sendEmail = "Bidali mezu elektronikoa erabiltzaileari aldaketa honi buruz"
includePassword = "Sartu pasahitz berria mezu elektronikoan"
forcePasswordChange = "Behartu erabiltzailea hurrengo saio-hasieran pasahitza aldatzera"
emailUnavailable = "Erabiltzaile honen helbide elektronikoa ez da baliozkoa. Jakinarazpenak desgaituta daude."
smtpDisabled = "Posta elektroniko bidezko jakinarazpenek SMTP gaituta egotea eskatzen dute ezarpenetan."
notifyOnly = "Pasahitzik gabe bidaliko da mezu elektronikoa; erabiltzaileari jakinaraziko zaio administratzaile batek aldatu duela."
submit = "Eguneratu pasahitza"
success = "Pasahitza ongi eguneratu da"
error = "Pasahitza eguneratzeak huts egin du"
[workspace.people.emailInvite]
tab = "E-posta bidezko gonbidapena"
description = "Idatzi edo itsatsi behean helbide elektronikoak, komaz bereizita. Erabiltzaileek saio-hasierako kredentzialak e-postaz jasoko dituzte."
@@ -5815,6 +5925,7 @@ subtitle = "Hasi saioa zure Stirling kontuarekin"
[setup.selfhosted]
title = "Hasi saioa zerbitzarian"
subtitle = "Sartu zure zerbitzariaren kredentzialak"
link = "edo konektatu autoostatutako kontu batera"
[setup.server]
title = "Konektatu zerbitzarira"
@@ -5833,6 +5944,14 @@ description = "Sartu zure auto-ostatuko Stirling PDF zerbitzariaren URLa osoa"
emptyUrl = "Sartu zerbitzari baten URLa"
unreachable = "Ezin izan da zerbitzarira konektatu"
testFailed = "Konexio proba huts egin du"
configFetch = "Ezin izan da zerbitzariaren konfigurazioa eskuratu. Egiaztatu URLa eta saiatu berriro."
[setup.server.error.securityDisabled]
title = "Saio-hasiera ez dago gaituta"
body = "Zerbitzari honek ez du saio-hasiera gaituta. Zerbitzari honekin konektatzeko, autentifikazioa gaitu behar duzu:"
step1 = "Ezarri DOCKER_ENABLE_SECURITY=true zure ingurunean"
step2 = "Edo ezarri security.enableLogin=true settings.yml fitxategian"
step3 = "Berrabiarazi zerbitzaria"
[setup.login]
title = "Hasi saioa"
@@ -5905,6 +6024,7 @@ earlyAccess = "Sarbide goiztiarra"
reset = "Aldaketak berrezarri"
downloadJson = "JSON deskargatu"
generatePdf = "PDF sortu"
saveChanges = "Gorde aldaketak"
[pdfTextEditor.options.autoScaleText]
title = "Testua automatikoki eskalatu kutxetara egokitzeko"
@@ -5942,6 +6062,8 @@ alpha = "Ikusle alfa hau oraindik eboluzioan dago—zenbait letra-tipo, kolore,
[pdfTextEditor.empty]
title = "Ez da dokumenturik kargatu"
subtitle = "Kargatu PDF edo JSON fitxategi bat testu-edukia editatzen hasteko."
dropzone = "Arrastatu eta jaregin PDF edo JSON fitxategi bat hemen, edo egin klik arakatzeko"
dropzoneWithFiles = "Hautatu fitxategi bat Fitxategiak fitxatik, edo arrastatu eta jaregin PDF edo JSON fitxategi bat hemen, edo egin klik arakatzeko"
[pdfTextEditor.welcomeBanner]
title = "Ongi etorri PDF Text Editor-era (Sarbide goiztiarra)"

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