mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-01 20:10:35 +01:00
fix(api): return JSON responses for admin settings + API key endpoints to prevent Tauri client parse errors (#5437)
# Description of Changes
```console
index-DsORDqQQ.js:124 \n [TauriHttpClient] Network error: \n{url: 'http://localhost:8080/api/v1/admin/settings', method: 'PUT', errorType: 'ERR_NETWORK', originalMessage: `Failed to execute 'close' on 'ReadableStreamDefaul…cted token 'S', "Successful"... is not valid JSON`, stack: `SyntaxError: Unexpected token 'S', "Successful"...…lback (<anonymous>:284:7)\n at <anonymous>:1:28`}\nerrorType\n: \n"ERR_NETWORK"\nmethod\n: \n"PUT"\noriginalMessage\n: \n"Failed to execute 'close' on 'ReadableStreamDefaultController': Unexpected token 'S', \"Successful\"... is not valid JSON"\nstack\n: \n"SyntaxError: Unexpected token 'S', \"Successful\"... is not valid JSON\n at A.onmessage (http://tauri.localhost/assets/index-DsORDqQQ.js:124:22714)\n at http://tauri.localhost/assets/index-DsORDqQQ.js:124:20748\n at <anonymous>:272:26\n at Object.runCallback (<anonymous>:284:7)\n at <anonymous>:1:28"\nurl\n: \n"http://localhost:8080/api/v1/admin/settings"
index-DXbk7lbS.js:124 \n [TauriHttpClient] Network error: \n{url: 'http://localhost:8080/api/v1/user/get-api-key', method: 'POST', errorType: 'ERR_NETWORK', originalMessage: `Failed to execute 'close' on 'ReadableStreamDefaul…cted token 'a', "a72f6b26-1"... is not valid JSON`, stack: `SyntaxError: Unexpected token 'a', "a72f6b26-1"...…lback (<anonymous>:284:7)\n at <anonymous>:1:28`}\nerrorType\n: \n"ERR_NETWORK"\nmethod\n: \n"POST"\noriginalMessage\n: \n"Failed to execute 'close' on 'ReadableStreamDefaultController': Unexpected token 'a', \"a72f6b26-1\"... is not valid JSON"\nstack\n: \n"SyntaxError: Unexpected token 'a', \"a72f6b26-1\"... is not valid JSON\n at A.onmessage (http://tauri.localhost/assets/index-DXbk7lbS.js:124:22714)\n at http://tauri.localhost/assets/index-DXbk7lbS.js:124:20748\n at <anonymous>:272:26\n at Object.runCallback (<anonymous>:284:7)\n at <anonymous>:1:28"\nurl\n: \n"http://localhost:8080/api/v1/user/get-api-key"
```
This pull request fixes a self-hosting issue where the Tauri HTTP client
fails with `Unexpected token ... is not valid JSON` because certain API
endpoints returned plain text responses.
## What was changed
- Updated `AdminSettingsController`:
- Changed `updateSettings` and `updateSettingsSection` to return
structured JSON objects instead of raw strings.
- Standardized success and error payloads using a `Map<String, Object>`
with keys like `message` and `error`.
- Updated `UserController`:
- Changed `/api/v1/user/get-api-key` and `/api/v1/user/update-api-key`
to return JSON objects (`{ "apiKey": "..." }`) and JSON error objects
instead of plain text.
## Why the change was made
- The Tauri client expects JSON responses and attempts to parse them.
Returning plain strings like `"Successful..."` or an API key string
causes JSON parsing to fail, resulting in network errors on self-hosted
setups.
---
## 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.
---------
Co-authored-by: James Brunton <jbrunton96@gmail.com>
This commit is contained in:
parent
84ed1d7ecb
commit
b266b50bef
@ -32,16 +32,19 @@ public class SettingsController {
|
||||
|
||||
@AutoJobPostMapping("/update-enable-analytics")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updateApiKey(@RequestParam Boolean enabled) throws IOException {
|
||||
public ResponseEntity<Map<String, Object>> updateApiKey(@RequestParam Boolean enabled)
|
||||
throws IOException {
|
||||
if (applicationProperties.getSystem().getEnableAnalytics() != null) {
|
||||
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
|
||||
.body(
|
||||
"Setting has already been set, To adjust please edit "
|
||||
+ InstallationPathConfig.getSettingsPath());
|
||||
Map.of(
|
||||
"message",
|
||||
"Setting has already been set, To adjust please edit "
|
||||
+ InstallationPathConfig.getSettingsPath()));
|
||||
}
|
||||
GeneralUtils.saveKeyToSettings("system.enableAnalytics", enabled);
|
||||
applicationProperties.getSystem().setEnableAnalytics(enabled);
|
||||
return ResponseEntity.ok("Updated");
|
||||
return ResponseEntity.ok(Map.of("message", "Updated"));
|
||||
}
|
||||
|
||||
@GetMapping("/get-endpoints-status")
|
||||
|
||||
@ -163,12 +163,13 @@ public class AdminSettingsController {
|
||||
responseCode = "500",
|
||||
description = "Failed to save settings to configuration file")
|
||||
})
|
||||
public ResponseEntity<String> updateSettings(
|
||||
public ResponseEntity<Map<String, Object>> updateSettings(
|
||||
@Valid @RequestBody UpdateSettingsRequest request) {
|
||||
try {
|
||||
Map<String, Object> settings = request.getSettings();
|
||||
if (settings == null || settings.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body("No settings provided to update");
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "No settings provided to update"));
|
||||
}
|
||||
|
||||
int updatedCount = 0;
|
||||
@ -178,7 +179,11 @@ public class AdminSettingsController {
|
||||
|
||||
if (!isValidSettingKey(key)) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("Invalid setting key format: " + HtmlUtils.htmlEscape(key));
|
||||
.body(
|
||||
Map.of(
|
||||
"error",
|
||||
"Invalid setting key format: "
|
||||
+ HtmlUtils.htmlEscape(key)));
|
||||
}
|
||||
|
||||
log.info("Admin updating setting: {} = {}", key, value);
|
||||
@ -191,22 +196,26 @@ public class AdminSettingsController {
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(
|
||||
String.format(
|
||||
"Successfully updated %d setting(s). Changes will take effect on"
|
||||
+ " application restart.",
|
||||
updatedCount));
|
||||
Map.of(
|
||||
"message",
|
||||
String.format(
|
||||
"Successfully updated %d setting(s). Changes will take effect on"
|
||||
+ " application restart.",
|
||||
updatedCount)));
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to save settings to file: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(GENERIC_FILE_ERROR);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("error", GENERIC_FILE_ERROR));
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Invalid setting key or value: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(GENERIC_INVALID_SETTING);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(Map.of("error", GENERIC_INVALID_SETTING));
|
||||
} catch (Exception e) {
|
||||
log.error("Unexpected error while updating settings: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(GENERIC_SERVER_ERROR);
|
||||
.body(Map.of("error", GENERIC_SERVER_ERROR));
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,20 +292,23 @@ public class AdminSettingsController {
|
||||
description = "Access denied - Admin role required"),
|
||||
@ApiResponse(responseCode = "500", description = "Failed to save settings")
|
||||
})
|
||||
public ResponseEntity<String> updateSettingsSection(
|
||||
public ResponseEntity<Map<String, Object>> updateSettingsSection(
|
||||
@PathVariable String sectionName, @Valid @RequestBody Map<String, Object> sectionData) {
|
||||
try {
|
||||
if (sectionData == null || sectionData.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body("No section data provided to update");
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "No section data provided to update"));
|
||||
}
|
||||
|
||||
if (!isValidSectionName(sectionName)) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(
|
||||
"Invalid section name: "
|
||||
+ HtmlUtils.htmlEscape(sectionName)
|
||||
+ ". Valid sections: "
|
||||
+ String.join(", ", VALID_SECTION_NAMES));
|
||||
Map.of(
|
||||
"error",
|
||||
"Invalid section name: "
|
||||
+ HtmlUtils.htmlEscape(sectionName)
|
||||
+ ". Valid sections: "
|
||||
+ String.join(", ", VALID_SECTION_NAMES)));
|
||||
}
|
||||
|
||||
// Auto-enable premium features if license key is provided
|
||||
@ -317,7 +329,11 @@ public class AdminSettingsController {
|
||||
|
||||
if (!isValidSettingKey(fullKey)) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("Invalid setting key format: " + HtmlUtils.htmlEscape(fullKey));
|
||||
.body(
|
||||
Map.of(
|
||||
"error",
|
||||
"Invalid setting key format: "
|
||||
+ HtmlUtils.htmlEscape(fullKey)));
|
||||
}
|
||||
|
||||
log.info("Admin updating section setting: {} = {}", fullKey, value);
|
||||
@ -331,21 +347,25 @@ public class AdminSettingsController {
|
||||
|
||||
String escapedSectionName = HtmlUtils.htmlEscape(sectionName);
|
||||
return ResponseEntity.ok(
|
||||
String.format(
|
||||
"Successfully updated %d setting(s) in section '%s'. Changes will take"
|
||||
+ " effect on application restart.",
|
||||
updatedCount, escapedSectionName));
|
||||
Map.of(
|
||||
"message",
|
||||
String.format(
|
||||
"Successfully updated %d setting(s) in section '%s'. Changes will take"
|
||||
+ " effect on application restart.",
|
||||
updatedCount, escapedSectionName)));
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to save section settings to file: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(GENERIC_FILE_ERROR);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("error", GENERIC_FILE_ERROR));
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Invalid section data: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(GENERIC_INVALID_SECTION);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(Map.of("error", GENERIC_INVALID_SECTION));
|
||||
} catch (Exception e) {
|
||||
log.error("Unexpected error while updating section settings: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(GENERIC_SERVER_ERROR);
|
||||
.body(Map.of("error", GENERIC_SERVER_ERROR));
|
||||
}
|
||||
}
|
||||
|
||||
@ -453,7 +473,7 @@ public class AdminSettingsController {
|
||||
description = "Access denied - Admin role required"),
|
||||
@ApiResponse(responseCode = "500", description = "Failed to initiate restart")
|
||||
})
|
||||
public ResponseEntity<String> restartApplication() {
|
||||
public ResponseEntity<Map<String, Object>> restartApplication() {
|
||||
try {
|
||||
log.warn("Admin initiated application restart");
|
||||
|
||||
@ -465,13 +485,18 @@ public class AdminSettingsController {
|
||||
log.error("Cannot restart: not running from JAR (likely development mode)");
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(
|
||||
"Restart not available in development mode. Please restart the application manually.");
|
||||
Map.of(
|
||||
"error",
|
||||
"Restart not available in development mode. Please restart the application manually."));
|
||||
}
|
||||
|
||||
if (helperJar == null || !Files.isRegularFile(helperJar)) {
|
||||
log.error("Cannot restart: restart-helper.jar not found at expected location");
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body("Restart helper not found. Please restart the application manually.");
|
||||
.body(
|
||||
Map.of(
|
||||
"error",
|
||||
"Restart helper not found. Cannot perform application restart."));
|
||||
}
|
||||
|
||||
// Get current application arguments
|
||||
@ -526,12 +551,17 @@ public class AdminSettingsController {
|
||||
.start();
|
||||
|
||||
return ResponseEntity.ok(
|
||||
"Application restart initiated. The server will be back online shortly.");
|
||||
Map.of(
|
||||
"message",
|
||||
"Application restart initiated. The server will be back online shortly."));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to initiate restart: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("Failed to initiate application restart: " + e.getMessage());
|
||||
.body(
|
||||
Map.of(
|
||||
"error",
|
||||
"Failed to initiate application restart: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -741,31 +741,35 @@ public class UserController {
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PostMapping("/get-api-key")
|
||||
public ResponseEntity<String> getApiKey(Principal principal) {
|
||||
public ResponseEntity<Map<String, String>> getApiKey(Principal principal) {
|
||||
if (principal == null) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(Map.of("error", "User not authenticated."));
|
||||
}
|
||||
String username = principal.getName();
|
||||
String apiKey = userService.getApiKeyForUser(username);
|
||||
if (apiKey == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(Map.of("error", "API key not found for user."));
|
||||
}
|
||||
return ResponseEntity.ok(apiKey);
|
||||
return ResponseEntity.ok(Map.of("apiKey", apiKey));
|
||||
}
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PostMapping("/update-api-key")
|
||||
public ResponseEntity<String> updateApiKey(Principal principal) {
|
||||
public ResponseEntity<Map<String, String>> updateApiKey(Principal principal) {
|
||||
if (principal == null) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(Map.of("error", "User not authenticated."));
|
||||
}
|
||||
String username = principal.getName();
|
||||
User user = userService.refreshApiKeyForUser(username);
|
||||
String apiKey = user.getApiKey();
|
||||
if (apiKey == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(Map.of("error", "API key not found for user."));
|
||||
}
|
||||
return ResponseEntity.ok(apiKey);
|
||||
return ResponseEntity.ok(Map.of("apiKey", apiKey));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -128,6 +128,9 @@ undoQuotaError = "Rückgängig nicht möglich: unzureichender Speicherplatz"
|
||||
undoStorageError = "Rückgängig abgeschlossen, aber einige Dateien konnten nicht im Speicher gespeichert werden"
|
||||
undoSuccess = "Vorgang erfolgreich rückgängig gemacht"
|
||||
unsupported = "Nicht unterstützt"
|
||||
discardRedactions = "Discard & Leave"
|
||||
pendingRedactions = "You have unapplied redactions that will be lost."
|
||||
pendingRedactionsTitle = "Unapplied Redactions"
|
||||
|
||||
[toolPanel]
|
||||
placeholder = "Wählen Sie ein Tool, um zu starten"
|
||||
@ -434,6 +437,13 @@ currentVersion = "Aktuelle Version"
|
||||
latestVersion = "Neueste Version"
|
||||
checkForUpdates = "Nach Updates suchen"
|
||||
viewDetails = "Details anzeigen"
|
||||
serverNeedsUpdate = "Server needs to be updated by administrator"
|
||||
|
||||
[settings.general.versionInfo]
|
||||
description = "Desktop and server version details"
|
||||
desktop = "Desktop Version"
|
||||
server = "Server Version"
|
||||
title = "Version Information"
|
||||
|
||||
[settings.hotkeys]
|
||||
title = "Tastenkürzel"
|
||||
@ -453,6 +463,25 @@ noShortcut = "Kein Kürzel festgelegt"
|
||||
mac = "Fügen Sie ⌘ (Command), ⌥ (Option) oder einen anderen Modifikator in Ihr Kürzel ein."
|
||||
windows = "Fügen Sie Strg, Alt oder einen anderen Modifikator in Ihr Kürzel ein."
|
||||
|
||||
[settings.security]
|
||||
description = "Update your password to keep your account secure."
|
||||
title = "Security"
|
||||
|
||||
[settings.security.password]
|
||||
confirm = "Confirm new password"
|
||||
confirmPlaceholder = "Re-enter your new password"
|
||||
current = "Current password"
|
||||
currentPlaceholder = "Enter your current password"
|
||||
error = "Unable to update password. Please verify your current password and try again."
|
||||
mismatch = "New passwords do not match."
|
||||
new = "New password"
|
||||
newPlaceholder = "Enter a new password"
|
||||
required = "All fields are required."
|
||||
ssoDisabled = "Password changes are managed by your identity provider."
|
||||
subtitle = "Change your password. You will be logged out after updating."
|
||||
success = "Password updated successfully. Please sign in again."
|
||||
update = "Update password"
|
||||
|
||||
[update]
|
||||
modalTitle = "Update verfügbar"
|
||||
current = "Aktuelle Version"
|
||||
@ -493,6 +522,11 @@ oldPassword = "Aktuelles Passwort"
|
||||
newPassword = "Neues Passwort"
|
||||
confirmNewPassword = "Neues Passwort bestätigen"
|
||||
submit = "Änderung speichern"
|
||||
changeUsername = "Update your username. You will be logged out after updating."
|
||||
credsUpdated = "Account updated"
|
||||
description = "Changes saved. Please log in again."
|
||||
error = "Unable to update username. Please verify your password and try again."
|
||||
ssoManaged = "Your account is managed by your identity provider."
|
||||
|
||||
[account]
|
||||
title = "Kontoeinstellungen"
|
||||
@ -514,6 +548,8 @@ property = "Eigenschaft"
|
||||
webBrowserSettings = "Webbrowser-Einstellung"
|
||||
syncToBrowser = "Synchronisiere Konto -> Browser"
|
||||
syncToAccount = "Synchronisiere Konto <- Browser"
|
||||
changeUsernameDescription = "Update your username. You will be logged out after updating."
|
||||
newUsernamePlaceholder = "Enter your new username"
|
||||
|
||||
[adminUserSettings]
|
||||
title = "Benutzerkontrolle"
|
||||
@ -926,10 +962,16 @@ tags = "text,anmerkung,beschriftung"
|
||||
title = "Text hinzufügen"
|
||||
desc = "Beliebigen Text überall in Ihrem PDF hinzufügen"
|
||||
|
||||
[home.annotate]
|
||||
desc = "Highlight, draw, add notes and shapes in the viewer"
|
||||
tags = "annotate,highlight,draw"
|
||||
title = "Annotate"
|
||||
|
||||
[landing]
|
||||
addFiles = "Dateien hinzufügen"
|
||||
uploadFromComputer = "Vom Computer hochladen"
|
||||
openFromComputer = "Vom Computer öffnen"
|
||||
mobileUpload = "Upload from Mobile"
|
||||
|
||||
[viewPdf]
|
||||
tags = "anzeigen,lesen,kommentieren,text,bild"
|
||||
@ -1245,6 +1287,21 @@ cbzOptions = "Optionen: CBZ zu PDF"
|
||||
optimizeForEbook = "PDF für E-Book-Reader optimieren (verwendet Ghostscript)"
|
||||
cbzOutputOptions = "Optionen: PDF zu CBZ"
|
||||
cbzDpi = "DPI für Bildrendering"
|
||||
cbrDpi = "DPI for image rendering"
|
||||
cbrOptions = "CBR Options"
|
||||
cbrOutputOptions = "PDF to CBR Options"
|
||||
|
||||
[convert.ebookOptions]
|
||||
ebookOptions = "eBook to PDF Options"
|
||||
ebookOptionsDesc = "Options for converting eBooks to PDF"
|
||||
embedAllFonts = "Embed all fonts"
|
||||
embedAllFontsDesc = "Embed all fonts from the eBook into the generated PDF"
|
||||
includePageNumbers = "Include page numbers"
|
||||
includePageNumbersDesc = "Add page numbers to the generated PDF"
|
||||
includeTableOfContents = "Include table of contents"
|
||||
includeTableOfContentsDesc = "Add a generated table of contents to the resulting PDF"
|
||||
optimizeForEbookPdf = "Optimize for ebook readers"
|
||||
optimizeForEbookPdfDesc = "Optimize the PDF for eBook reading (smaller file size, better rendering on eInk devices)"
|
||||
|
||||
[imageToPdf]
|
||||
tags = "konvertierung,img,jpg,bild,foto"
|
||||
@ -1362,6 +1419,11 @@ add = "Anhang hinzufügen"
|
||||
remove = "Anhang entfernen"
|
||||
embed = "Anhang einbetten"
|
||||
submit = "Anhänge hinzufügen"
|
||||
convertToPdfA3b = "Convert to PDF/A-3b"
|
||||
convertToPdfA3bDescription = "Creates an archival PDF with embedded attachments"
|
||||
convertToPdfA3bTooltip = "PDF/A-3b is an archival format ensuring long-term preservation. It allows embedding arbitrary file formats as attachments. Conversion requires Ghostscript and may take longer for large files."
|
||||
convertToPdfA3bTooltipHeader = "About PDF/A-3b Conversion"
|
||||
convertToPdfA3bTooltipTitle = "What it does"
|
||||
|
||||
[watermark]
|
||||
title = "Wasserzeichen hinzufügen"
|
||||
@ -2306,6 +2368,10 @@ saved = "Gespeichert"
|
||||
label = "Unterschriftsbild hochladen"
|
||||
placeholder = "Bilddatei auswählen"
|
||||
hint = "Laden Sie ein PNG- oder JPG-Bild Ihrer Unterschrift hoch"
|
||||
backgroundRemovalFailedMessage = "Could not remove the background from the image. Using original image instead."
|
||||
backgroundRemovalFailedTitle = "Background removal failed"
|
||||
processing = "Processing image..."
|
||||
removeBackground = "Remove white background (make transparent)"
|
||||
|
||||
[sign.instructions]
|
||||
title = "So fügen Sie eine Unterschrift hinzu"
|
||||
@ -2376,6 +2442,11 @@ bullet2 = "Links funktionieren beim Anklicken weiterhin"
|
||||
bullet3 = "Kommentare und Notizen bleiben sichtbar"
|
||||
bullet4 = "Lesezeichen helfen weiterhin bei der Navigation"
|
||||
|
||||
[flatten.renderDpi]
|
||||
help = "Leave blank to use the system default. Higher DPI sharpens output but increases processing time and file size."
|
||||
label = "Rendering DPI (optional, recommended 150 DPI)"
|
||||
placeholder = "e.g. 150"
|
||||
|
||||
[repair]
|
||||
tags = "reparieren,wiederherstellen,korrigieren,wiederherstellen"
|
||||
title = "Reparieren"
|
||||
@ -2925,6 +2996,7 @@ header = "PDF zuschneiden"
|
||||
submit = "Abschicken"
|
||||
noFileSelected = "Wählen Sie eine PDF-Datei aus, um mit dem Zuschneiden zu beginnen"
|
||||
reset = "Auf vollständiges PDF zurücksetzen"
|
||||
autoCrop = "Auto-crop whitespace"
|
||||
|
||||
[crop.preview]
|
||||
title = "Zuschneidebereich-Auswahl"
|
||||
@ -3158,6 +3230,7 @@ automaticDesc = "Text basierend auf Suchbegriffen schwärzen"
|
||||
manual = "Manuell"
|
||||
manualDesc = "Klicken und ziehen zum Schwärzen bestimmter Bereiche"
|
||||
manualComingSoon = "Manuelle Schwärzung kommt bald"
|
||||
automaticDisabledTooltip = "Select files in the file manager to redact multiple files at once"
|
||||
|
||||
[redact.auto]
|
||||
header = "Auto-Schwärzung"
|
||||
@ -3225,6 +3298,24 @@ text = "Nur vollständige Wörter abgleichen, keine Teilübereinstimmungen. 'Joh
|
||||
title = "In PDF-Bild konvertieren"
|
||||
text = "Konvertiert das PDF nach der Schwärzung in ein bildbasiertes PDF. Dies stellt sicher, dass Text hinter Schwärzungskästen vollständig entfernt und nicht wiederherstellbar ist."
|
||||
|
||||
[redact.tooltip.manual.apply]
|
||||
bullet1 = "Mark as many areas as needed before applying"
|
||||
bullet2 = "All pending redactions are applied at once"
|
||||
bullet3 = "Redactions cannot be undone after applying"
|
||||
text = "After marking content, click 'Apply' to permanently redact all marked areas. The pending count shows how many redactions are ready to be applied."
|
||||
title = "Apply Redactions"
|
||||
|
||||
[redact.tooltip.manual.header]
|
||||
title = "Manual Redaction Controls"
|
||||
|
||||
[redact.tooltip.manual.markArea]
|
||||
text = "Draw rectangular areas on the PDF to mark regions for redaction. Useful for redacting images, signatures, or irregular shapes."
|
||||
title = "Mark Area Tool"
|
||||
|
||||
[redact.tooltip.manual.markText]
|
||||
text = "Select text directly on the PDF to mark it for redaction. Click and drag to highlight specific text that you want to redact."
|
||||
title = "Mark Text Tool"
|
||||
|
||||
[redact.manual]
|
||||
header = "Manuelle Schwärzung"
|
||||
textBasedRedaction = "Textbasierte Schwärzung"
|
||||
@ -3246,6 +3337,15 @@ showLayers = "Ebenen anzeigen (Doppelklick, um alle Ebenen auf den Standardzusta
|
||||
colourPicker = "Farbwähler"
|
||||
findCurrentOutlineItem = "Aktuelles Gliederungselement finden"
|
||||
applyChanges = "Änderungen anwenden"
|
||||
apply = "Apply"
|
||||
applyWarning = "⚠️ Permanent application, cannot be undone and the data underneath will be deleted"
|
||||
controlsTitle = "Manual Redaction Controls"
|
||||
instructions = "Select text or draw areas on the PDF to mark content for redaction."
|
||||
markArea = "Mark Area"
|
||||
markText = "Mark Text"
|
||||
noMarks = "No redaction marks. Use the tools above to mark content for redaction."
|
||||
pendingLabel = "Pending:"
|
||||
title = "Redaction Tools"
|
||||
|
||||
[redact.manual.pageRedactionNumbers]
|
||||
title = "Seiten"
|
||||
@ -3342,6 +3442,19 @@ placeholder = "Anzahl horizontaler Teiler eingeben"
|
||||
label = "Vertikale Teiler"
|
||||
placeholder = "Anzahl vertikaler Teiler eingeben"
|
||||
|
||||
[split-by-sections.customPages]
|
||||
label = "Custom Page Numbers"
|
||||
placeholder = "e.g. 2,4,6"
|
||||
|
||||
[split-by-sections.splitMode]
|
||||
custom = "Custom pages"
|
||||
description = "Choose how to split the pages"
|
||||
label = "Split Mode"
|
||||
splitAll = "Split all pages"
|
||||
splitAllExceptFirst = "Split all except first"
|
||||
splitAllExceptFirstAndLast = "Split all except first and last"
|
||||
splitAllExceptLast = "Split all except last"
|
||||
|
||||
[AddStampRequest]
|
||||
tags = "stempeln,bild hinzufügen,bild zentrieren,wasserzeichen,pdf,einbetten,anpassen"
|
||||
header = "PDF Stempel"
|
||||
@ -3720,6 +3833,10 @@ bullet2 = "Höhere Werte reduzieren die Dateigröße"
|
||||
title = "Graustufen"
|
||||
text = "Wählen Sie diese Option, um alle Bilder in Schwarz-Weiß zu konvertieren, was die Dateigröße erheblich reduzieren kann, insbesondere bei gescannten PDFs oder bildreichen Dokumenten."
|
||||
|
||||
[compress.tooltip.lineArt]
|
||||
text = "Convert pages to high-contrast black and white using ImageMagick. Use detail level to control how much content becomes black, and edge emphasis to control how aggressively edges are detected."
|
||||
title = "Line Art"
|
||||
|
||||
[compress.error]
|
||||
failed = "Ein Fehler ist beim Komprimieren der PDF aufgetreten."
|
||||
|
||||
@ -3732,6 +3849,24 @@ failed = "Ein Fehler ist beim Komprimieren der PDF aufgetreten."
|
||||
_value = "Kompressionseinstellungen"
|
||||
1 = "1-3 PDF-Komprimierung, </br> 4-6 Leichte Bildkomprimierung, </br> 7-9 Intensive Bildkomprimierung verringert die Bildqualität dramatisch"
|
||||
|
||||
[compress.compressionLevel]
|
||||
range1to3 = "Lower values preserve quality but result in larger files"
|
||||
range4to6 = "Medium compression with moderate quality reduction"
|
||||
range7to9 = "Higher values reduce file size significantly but may reduce image clarity"
|
||||
|
||||
[compress.lineArt]
|
||||
description = "Uses ImageMagick to reduce pages to high-contrast black and white for maximum size reduction."
|
||||
detailLevel = "Detail level"
|
||||
edgeEmphasis = "Edge emphasis"
|
||||
edgeHigh = "Strong"
|
||||
edgeLow = "Gentle"
|
||||
edgeMedium = "Balanced"
|
||||
label = "Convert images to line art"
|
||||
unavailable = "ImageMagick is not installed or enabled on this server"
|
||||
|
||||
[compress.linearize]
|
||||
label = "Linearize PDF for fast web viewing"
|
||||
|
||||
[decrypt]
|
||||
passwordPrompt = "Diese Datei ist passwortgeschützt. Bitte geben Sie das Passwort ein:"
|
||||
cancelled = "Vorgang für PDF abgebrochen: {0}"
|
||||
@ -3983,11 +4118,14 @@ rotateRight = "Nach rechts drehen"
|
||||
toggleSidebar = "Seitenleiste umschalten"
|
||||
exportSelected = "Ausgewählte Seiten exportieren"
|
||||
toggleAnnotations = "Anmerkungen ein-/ausblenden"
|
||||
annotationMode = "Anmerkungsmodus umschalten"
|
||||
print = "PDF drucken"
|
||||
draw = "Zeichnen"
|
||||
save = "Speichern"
|
||||
saveChanges = "Änderungen speichern"
|
||||
annotations = "Annotations"
|
||||
applyRedactionsFirst = "Apply redactions first"
|
||||
exitRedaction = "Exit Redaction Mode"
|
||||
redact = "Redact"
|
||||
|
||||
[search]
|
||||
title = "PDF durchsuchen"
|
||||
@ -4038,12 +4176,20 @@ settings = "Optionen"
|
||||
adminSettings = "Admin Optionen"
|
||||
allTools = "Werkzeuge"
|
||||
reader = "Reader"
|
||||
showMeAround = "Show me around"
|
||||
tours = "Tours"
|
||||
|
||||
[quickAccess.helpMenu]
|
||||
toolsTour = "Tool-Tour"
|
||||
toolsTourDesc = "Erfahren Sie, was die Tools können"
|
||||
adminTour = "Admin-Tour"
|
||||
adminTourDesc = "Entdecken Sie Admin-Einstellungen & Funktionen"
|
||||
whatsNewTour = "See what's new in V2"
|
||||
whatsNewTourDesc = "Tour the updated layout"
|
||||
|
||||
[quickAccess.toursTooltip]
|
||||
admin = "Watch walkthroughs here: Tools tour, New V2 layout tour, and the Admin tour."
|
||||
user = "Watch walkthroughs here: Tools tour and the New V2 layout tour."
|
||||
|
||||
[admin]
|
||||
error = "Fehler"
|
||||
@ -4069,6 +4215,8 @@ loginRequired = "Der Anmeldemodus muss aktiviert sein, um Admin-Einstellungen zu
|
||||
restarting = "Server wird neu gestartet"
|
||||
restartingMessage = "Der Server wird neu gestartet. Bitte einen Moment warten..."
|
||||
restartError = "Server konnte nicht neu gestartet werden. Bitte manuell neu starten."
|
||||
error = "Failed to save settings"
|
||||
success = "Settings saved successfully"
|
||||
|
||||
[admin.settings.unsavedChanges]
|
||||
title = "Ungespeicherte Änderungen"
|
||||
@ -4185,6 +4333,10 @@ description = "Pfad zur WeasyPrint-Ausführungsdatei für HTML-zu-PDF-Konvertier
|
||||
label = "Unoconvert-Ausführbare Datei"
|
||||
description = "Pfad zu LibreOffice unoconvert für Dokumentkonvertierungen (leer lassen für Standard: /opt/venv/bin/unoconvert)"
|
||||
|
||||
[admin.settings.general.frontendUrl]
|
||||
description = "Base URL for frontend (e.g., https://pdf.example.com). Used for email invite links and mobile QR code uploads. Leave empty to use backend URL."
|
||||
label = "Frontend URL"
|
||||
|
||||
[admin.settings.security]
|
||||
title = "Sicherheit"
|
||||
description = "Authentifizierung, Anmeldeverhalten und Sicherheitsrichtlinien konfigurieren."
|
||||
@ -4321,6 +4473,19 @@ connect = "Verbinden"
|
||||
disconnect = "Trennen"
|
||||
disconnected = "Anbieter erfolgreich getrennt"
|
||||
disconnectError = "Anbieter konnte nicht getrennt werden"
|
||||
imageResolutionFull = "Full (Original Size)"
|
||||
imageResolutionReduced = "Reduced (Max 1200px)"
|
||||
mobileScannerConvertToPdf = "Convert Images to PDF"
|
||||
mobileScannerConvertToPdfDesc = "Automatically convert uploaded images to PDF format. If disabled, images will be kept as-is."
|
||||
mobileScannerImageResolution = "Image Resolution"
|
||||
mobileScannerImageResolutionDesc = "Resolution of uploaded images. \"Reduced\" scales images to max 1200px to reduce file size."
|
||||
mobileScannerPageFormat = "Page Format"
|
||||
mobileScannerPageFormatDesc = "PDF page size for converted images. \"Keep\" uses original image dimensions."
|
||||
mobileScannerStretchToFit = "Stretch to Fit"
|
||||
mobileScannerStretchToFitDesc = "Stretch images to fill the entire page. If disabled, images are centered with preserved aspect ratio."
|
||||
pageFormatA4 = "A4 (210×297mm)"
|
||||
pageFormatKeep = "Keep (Original Dimensions)"
|
||||
pageFormatLetter = "Letter (8.5×11in)"
|
||||
|
||||
[admin.settings.connections.ssoAutoLogin]
|
||||
label = "SSO-Autoanmeldung"
|
||||
@ -4389,6 +4554,26 @@ description = "Benutzerkonten bei der ersten SAML2-Anmeldung automatisch erstell
|
||||
label = "Registrierung blockieren"
|
||||
description = "Neue Benutzerregistrierung über SAML2 verhindern"
|
||||
|
||||
[admin.settings.connections.mobileScanner]
|
||||
description = "Allow users to upload files from mobile devices by scanning a QR code"
|
||||
enable = "Enable QR Code Upload"
|
||||
imageResolutionFull = "Full (Original Size)"
|
||||
imageResolutionReduced = "Reduced (Max 1200px)"
|
||||
label = "Mobile Phone Upload"
|
||||
link = "Configure in System Settings"
|
||||
mobileScannerConvertToPdf = "Convert Images to PDF"
|
||||
mobileScannerConvertToPdfDesc = "Automatically convert uploaded images to PDF format. If disabled, images will be kept as-is."
|
||||
mobileScannerImageResolution = "Image Resolution"
|
||||
mobileScannerImageResolutionDesc = "Resolution of uploaded images. \"Reduced\" scales images to max 1200px to reduce file size."
|
||||
mobileScannerPageFormat = "Page Format"
|
||||
mobileScannerPageFormatDesc = "PDF page size for converted images. \"Keep\" uses original image dimensions."
|
||||
mobileScannerStretchToFit = "Stretch to Fit"
|
||||
mobileScannerStretchToFitDesc = "Stretch images to fill the entire page. If disabled, images are centered with preserved aspect ratio."
|
||||
note = "Note: Requires Frontend URL to be configured. "
|
||||
pageFormatA4 = "A4 (210×297mm)"
|
||||
pageFormatKeep = "Keep (Original Dimensions)"
|
||||
pageFormatLetter = "Letter (8.5×11in)"
|
||||
|
||||
[admin.settings.database]
|
||||
title = "Datenbank"
|
||||
description = "Benutzerdefinierte Datenbankverbindungseinstellungen für Enterprise-Bereitstellungen konfigurieren."
|
||||
@ -4570,6 +4755,10 @@ description = "Admins erlauben, Benutzer per E-Mail mit automatisch generierten
|
||||
label = "Frontend-URL"
|
||||
description = "Basis-URL für das Frontend (z. B. https://pdf.example.com). Wird zum Erzeugen von Einladungslinks in E-Mails verwendet. Leer lassen, um Backend-URL zu verwenden."
|
||||
|
||||
[admin.settings.mail.frontendUrlNote]
|
||||
link = "Configure in System Settings"
|
||||
note = "Note: Requires Frontend URL to be configured. "
|
||||
|
||||
[admin.settings.legal]
|
||||
title = "Rechtliche Dokumente"
|
||||
description = "Links zu rechtlichen Dokumenten und Richtlinien konfigurieren."
|
||||
@ -4685,6 +4874,9 @@ description = "Einzelne zu deaktivierende Endpunkte auswählen"
|
||||
label = "Deaktivierte Endpunktgruppen"
|
||||
description = "Zu deaktivierende Endpunktgruppen auswählen"
|
||||
|
||||
[admin.settings.badge]
|
||||
clickToUpgrade = "Click to view plan details"
|
||||
|
||||
[fileUpload]
|
||||
selectFile = "Datei auswählen"
|
||||
selectFiles = "Dateien auswählen"
|
||||
@ -4784,6 +4976,9 @@ showAll = "Alle anzeigen"
|
||||
sortByDate = "Nach Datum sortieren"
|
||||
sortByName = "Nach Name sortieren"
|
||||
sortBySize = "Nach Größe sortieren"
|
||||
mobileShort = "Mobile"
|
||||
mobileUpload = "Mobile Upload"
|
||||
mobileUploadNotAvailable = "Mobile upload not enabled"
|
||||
|
||||
[storage]
|
||||
temporaryNotice = "Dateien werden temporär in Ihrem Browser gespeichert und können automatisch gelöscht werden"
|
||||
@ -5069,6 +5264,7 @@ loading = "Laden..."
|
||||
back = "Zurück"
|
||||
continue = "Weiter"
|
||||
error = "Fehler"
|
||||
save = "Save"
|
||||
|
||||
[config.overview]
|
||||
title = "Anwendungskonfiguration"
|
||||
@ -5131,6 +5327,15 @@ impact = "Alle Anwendungen oder Dienste, die derzeit diese Schlüssel verwenden,
|
||||
confirmPrompt = "Sind Sie sicher, dass Sie fortfahren möchten?"
|
||||
confirmCta = "Schlüssel aktualisieren"
|
||||
|
||||
[config.apiKeys.alert]
|
||||
apiKeyErrorTitle = "API-Schlüssel-Fehler"
|
||||
failedToCreateApiKey = "API-Schlüssel konnte nicht erstellt werden."
|
||||
failedToRetrieveApiKey = "API-Schlüssel konnte nicht aus der Antwort abgerufen werden."
|
||||
failedToFetchApiKey = "API-Schlüssel konnte nicht abgerufen werden."
|
||||
apiKeyRefreshed = "API-Schlüssel aktualisiert"
|
||||
apiKeyRefreshedBody = "Ihr API-Schlüssel wurde erfolgreich aktualisiert."
|
||||
failedToRefreshApiKey = "API-Schlüssel konnte nicht aktualisiert werden."
|
||||
|
||||
[AddAttachmentsRequest]
|
||||
attachments = "Anhänge auswählen"
|
||||
info = "Wählen Sie Dateien aus, die Sie Ihrer PDF anhängen möchten. Diese Dateien werden eingebettet und über das Anhangs-Panel der PDF zugänglich sein."
|
||||
@ -5279,6 +5484,20 @@ userBody = "Laden Sie Teammitglieder ein, weisen Sie Rollen zu und halten Sie Ih
|
||||
[onboarding.securityCheck]
|
||||
message = "Die Anwendung hat kürzlich bedeutende Änderungen erfahren. Möglicherweise ist Aufmerksamkeit Ihres Server-Admins erforderlich. Bitte bestätigen Sie Ihre Rolle, um fortzufahren."
|
||||
|
||||
[onboarding.tourOverview]
|
||||
body = "Stirling PDF V2 ships with dozens of tools and a refreshed layout. Take a quick tour to see what changed and where to find the features you need."
|
||||
title = "Tour Overview"
|
||||
|
||||
[onboarding.whatsNew]
|
||||
activeFilesView = "Use Active Files to see everything you have open and pick what to work on."
|
||||
fileUpload = "Use the <strong>Files</strong> button to upload or pick a recent PDF. We will load a sample so you can see the workspace."
|
||||
leftPanel = "The left <strong>Tools</strong> panel lists everything you can do. Browse categories or search to find a tool quickly."
|
||||
pageEditorView = "Switch to the Page Editor to reorder, rotate, or delete pages."
|
||||
quickAccess = "Start at the <strong>Quick Access</strong> rail to jump between Reader, Automate, your files, and all the tours."
|
||||
rightRail = "The <strong>Right Rail</strong> holds quick actions to select files, change theme or language, and download results."
|
||||
topBar = "The top bar lets you swap between <strong>Viewer</strong>, <strong>Page Editor</strong>, and <strong>Active Files</strong>."
|
||||
wrapUp = "That is what is new in V2. Open the <strong>Tours</strong> menu anytime to replay this, the Tools tour, or the Admin tour."
|
||||
|
||||
[adminOnboarding]
|
||||
welcome = "Willkommen zur <strong>Admin-Tour</strong>! Entdecken wir die leistungsstarken Enterprise-Funktionen und Einstellungen für Systemadministratoren."
|
||||
configButton = "Klicken Sie auf die Schaltfläche <strong>Config</strong>, um alle Systemeinstellungen und Administrationskontrollen aufzurufen."
|
||||
@ -5568,6 +5787,28 @@ contactSales = "Vertrieb kontaktieren"
|
||||
contactToUpgrade = "Kontaktieren Sie uns, um Ihren Plan zu upgraden oder anzupassen"
|
||||
maxUsers = "Max. Benutzer"
|
||||
upTo = "Bis zu"
|
||||
activateLicense = "Activate Your License"
|
||||
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."
|
||||
checkoutOpened = "Checkout Opened"
|
||||
getLicense = "Get Server License"
|
||||
monthlyBilling = "Monthly Billing"
|
||||
selectPeriod = "Select Billing Period"
|
||||
upgradeToEnterprise = "Upgrade to Enterprise"
|
||||
yearlyBilling = "Yearly Billing"
|
||||
|
||||
[plan.static.billingPortal]
|
||||
message = "You will need to verify your email address in the Stripe billing portal. Check your email for a login link."
|
||||
title = "Email Verification Required"
|
||||
|
||||
[plan.static.licenseActivation]
|
||||
activate = "Activate License"
|
||||
checkoutOpened = "Checkout Opened in New Tab"
|
||||
doLater = "I'll do this later"
|
||||
enterKey = "Enter your license key below to activate your plan:"
|
||||
instructions = "Complete your purchase in the Stripe tab. Once your payment is complete, you will receive an email with your license key."
|
||||
keyDescription = "Paste the license key from your email"
|
||||
success = "License Activated!"
|
||||
successMessage = "Your license has been successfully activated. You can now close this window."
|
||||
|
||||
[plan.period]
|
||||
month = "Monat"
|
||||
@ -5771,6 +6012,8 @@ notAvailable = "Audit-System nicht verfügbar"
|
||||
notAvailableMessage = "Das Audit-System ist nicht konfiguriert oder nicht verfügbar."
|
||||
disabled = "Audit-Protokollierung ist deaktiviert"
|
||||
disabledMessage = "Aktivieren Sie die Audit-Protokollierung in Ihrer Anwendungskonfiguration, um Systemereignisse nachzuverfolgen."
|
||||
enterpriseRequired = "Enterprise License Required"
|
||||
enterpriseRequiredMessage = "The audit logging system is an enterprise feature. Please upgrade to an enterprise license to access audit logs and analytics."
|
||||
|
||||
[audit.error]
|
||||
title = "Fehler beim Laden des Audit-Systems"
|
||||
@ -5945,6 +6188,7 @@ 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."
|
||||
invalidUrl = "Invalid URL format. Please enter a valid URL like https://your-server.com"
|
||||
|
||||
[setup.server.error.securityDisabled]
|
||||
title = "Anmeldung nicht aktiviert"
|
||||
@ -5968,6 +6212,7 @@ instructions = "So aktivieren Sie die Anmeldung auf Ihrem Stirling PDF-Server:"
|
||||
instructionsEnvVar = "Setzen Sie die Umgebungsvariable:"
|
||||
instructionsOrYml = "Oder in der settings.yml:"
|
||||
instructionsRestart = "Starten Sie anschließend Ihren Server neu, damit die Änderungen wirksam werden."
|
||||
sso = "Single Sign-On"
|
||||
|
||||
[setup.login.username]
|
||||
label = "Benutzername"
|
||||
@ -6025,6 +6270,8 @@ reset = "Änderungen zurücksetzen"
|
||||
downloadJson = "JSON herunterladen"
|
||||
generatePdf = "PDF generieren"
|
||||
saveChanges = "Änderungen speichern"
|
||||
applyChanges = "Apply Changes"
|
||||
downloadCopy = "Download Copy"
|
||||
|
||||
[pdfTextEditor.options.autoScaleText]
|
||||
title = "Text automatisch in Rahmen einpassen"
|
||||
@ -6043,6 +6290,9 @@ descriptionInline = "Tipp: Halten Sie Strg (Cmd) oder Umschalt, um mehrere Textf
|
||||
title = "Bearbeiteten Text auf ein einzelnes PDF-Element fixieren"
|
||||
description = "Wenn aktiviert, exportiert der Editor jedes bearbeitete Textfeld als ein PDF-Textelement, um überlappende Glyphen oder gemischte Schriften zu vermeiden."
|
||||
|
||||
[pdfTextEditor.options.advanced]
|
||||
title = "Advanced Settings"
|
||||
|
||||
[pdfTextEditor.manual]
|
||||
mergeTooltip = "Ausgewählte Felder zusammenführen"
|
||||
merge = "Auswahl zusammenführen"
|
||||
@ -6118,6 +6368,21 @@ subset = "Subset"
|
||||
invalidJson = "JSON-Datei kann nicht gelesen werden. Stellen Sie sicher, dass sie vom PDF-zu-JSON-Tool erzeugt wurde."
|
||||
pdfConversion = "Das bearbeitete JSON kann nicht zurück in ein PDF konvertiert werden."
|
||||
|
||||
[pdfTextEditor.tooltip.alpha]
|
||||
text = "This alpha viewer is still evolving—certain fonts, colours, transparency effects, and layout details may shift slightly. Please double-check the generated PDF before sharing."
|
||||
title = "Alpha Viewer"
|
||||
|
||||
[pdfTextEditor.tooltip.header]
|
||||
title = "Preview Limitations"
|
||||
|
||||
[pdfTextEditor.tooltip.previewVariance]
|
||||
text = "Some visuals (such as table borders, shapes, or annotation appearances) may not display exactly in the preview. The exported PDF keeps the original drawing commands whenever possible."
|
||||
title = "Preview Variance"
|
||||
|
||||
[pdfTextEditor.tooltip.textFocus]
|
||||
text = "This workspace focuses on editing text and repositioning embedded images. Complex page artwork, form widgets, and layered graphics are preserved for export but are not fully editable here."
|
||||
title = "Text and Image Focus"
|
||||
|
||||
[auth]
|
||||
sessionExpired = "Sitzung abgelaufen"
|
||||
pleaseLoginAgain = "Bitte melden Sie sich erneut an."
|
||||
@ -6164,3 +6429,134 @@ title = "Ergebnisse: Text hinzufügen"
|
||||
|
||||
[addText.error]
|
||||
failed = "Beim Hinzufügen von Text zum PDF ist ein Fehler aufgetreten."
|
||||
|
||||
[annotation]
|
||||
applyChanges = "Apply Changes"
|
||||
backgroundColor = "Background colour"
|
||||
borderOff = "Border: Off"
|
||||
borderOn = "Border: On"
|
||||
chooseColor = "Choose colour"
|
||||
circle = "Circle"
|
||||
clearBackground = "Remove background"
|
||||
color = "Colour"
|
||||
contents = "Text"
|
||||
desc = "Use highlight, pen, text, and notes. Changes stay live—no flattening required."
|
||||
drawing = "Drawing"
|
||||
editCircle = "Edit Circle"
|
||||
editInk = "Edit Pen"
|
||||
editLine = "Edit Line"
|
||||
editNote = "Edit Note"
|
||||
editPolygon = "Edit Polygon"
|
||||
editSelectDescription = "Click an existing annotation to edit its colour, opacity, text, or size."
|
||||
editSelected = "Edit Annotation"
|
||||
editSquare = "Edit Square"
|
||||
editStampHint = "To change the image, delete this stamp and add a new one."
|
||||
editSwitchToSelect = "Switch to Select & Edit to edit this annotation."
|
||||
editText = "Edit Text Box"
|
||||
editTextMarkup = "Edit Text Markup"
|
||||
ellipse = "Ellipse"
|
||||
exit = "Exit annotation mode"
|
||||
fillColor = "Fill Colour"
|
||||
fillOpacity = "Fill Opacity"
|
||||
fontSize = "Font size"
|
||||
freehandHighlighter = "Freehand Highlighter"
|
||||
highlight = "Highlight"
|
||||
imagePreview = "Preview"
|
||||
inkHighlighter = "Freehand Highlighter"
|
||||
line = "Line"
|
||||
noBackground = "No background"
|
||||
note = "Note"
|
||||
noteIcon = "Note Icon"
|
||||
notesStamps = "Notes & Stamps"
|
||||
opacity = "Opacity"
|
||||
pen = "Pen"
|
||||
polygon = "Polygon"
|
||||
rectangle = "Rectangle"
|
||||
redo = "Redo"
|
||||
saveChanges = "Save Changes"
|
||||
saveFailed = "Unable to save copy"
|
||||
saveReady = "Download ready"
|
||||
savingCopy = "Preparing download..."
|
||||
select = "Select"
|
||||
selectAndMove = "Select and Edit"
|
||||
settings = "Settings"
|
||||
shapes = "Shapes"
|
||||
square = "Square"
|
||||
squiggly = "Squiggly"
|
||||
stamp = "Add Image"
|
||||
stampSettings = "Stamp Settings"
|
||||
strikeout = "Strikeout"
|
||||
strokeColor = "Stroke Colour"
|
||||
strokeOpacity = "Stroke Opacity"
|
||||
strokeWidth = "Width"
|
||||
text = "Text box"
|
||||
textAlignment = "Text Alignment"
|
||||
textMarkup = "Text Markup"
|
||||
title = "Annotate"
|
||||
underline = "Underline"
|
||||
undo = "Undo"
|
||||
unsupportedType = "This annotation type is not fully supported for editing."
|
||||
|
||||
[footer]
|
||||
discord = "Discord"
|
||||
issues = "GitHub"
|
||||
|
||||
[mobileScanner]
|
||||
addToBatch = "Add to Batch"
|
||||
back = "Back"
|
||||
batchImages = "Batch"
|
||||
camera = "Camera"
|
||||
cameraAccessDenied = "Camera access denied. Please enable camera access."
|
||||
cameraDescription = "Scan documents using your device camera with automatic edge detection"
|
||||
capture = "Capture Photo"
|
||||
chooseMethod = "Choose Upload Method"
|
||||
chooseMethodDescription = "Select how you want to scan and upload documents"
|
||||
clearBatch = "Clear"
|
||||
connected = "Connected"
|
||||
connecting = "Connecting..."
|
||||
edgeDetection = "Edge Detection"
|
||||
fileDescription = "Upload existing photos or documents from your device"
|
||||
fileUpload = "File Upload"
|
||||
flash = "Flash"
|
||||
flashlight = "Flashlight"
|
||||
httpsRequired = "Camera access requires HTTPS or localhost. Please use HTTPS or access via localhost."
|
||||
noSession = "Invalid Session"
|
||||
noSessionMessage = "Please scan a valid QR code to access this page."
|
||||
preview = "Preview"
|
||||
processing = "Processing..."
|
||||
retake = "Retake"
|
||||
selectFilesPrompt = "Select files to upload"
|
||||
selectImage = "Select Image"
|
||||
sessionExpired = "This session has expired. Please refresh and try again."
|
||||
sessionInvalid = "Session Error"
|
||||
sessionNotFound = "Session not found. Please refresh and try again."
|
||||
sessionValidationError = "Unable to verify session. Please try again."
|
||||
settings = "Settings"
|
||||
title = "Mobile Scanner"
|
||||
upload = "Upload"
|
||||
uploadAll = "Upload All"
|
||||
uploadFailed = "Upload failed. Please try again."
|
||||
uploadSuccess = "Upload Successful!"
|
||||
uploadSuccessMessage = "Your images have been transferred."
|
||||
uploading = "Uploading..."
|
||||
validating = "Validating session..."
|
||||
|
||||
[mobileUpload]
|
||||
connected = "Mobile device connected"
|
||||
description = "Scan to upload photos. Images auto-convert to PDF."
|
||||
descriptionNoConvert = "Scan to upload photos from your mobile device."
|
||||
error = "Connection Error"
|
||||
expiryWarning = "Session Expiring Soon"
|
||||
expiryWarningMessage = "This QR code will expire in {{seconds}} seconds. A new code will be generated automatically."
|
||||
filesReceived = "{{count}} file(s) received"
|
||||
instructions = "Scan with your phone camera. Images convert to PDF automatically."
|
||||
instructionsNoConvert = "Scan with your phone camera to upload files."
|
||||
pollingError = "Error checking for files"
|
||||
sessionCreateError = "Failed to create session"
|
||||
sessionId = "Session ID"
|
||||
title = "Upload from Mobile"
|
||||
|
||||
[textAlign]
|
||||
center = "Center"
|
||||
left = "Left"
|
||||
right = "Right"
|
||||
|
||||
@ -5404,6 +5404,15 @@ impact = "Any applications or services currently using these keys will stop work
|
||||
confirmPrompt = "Are you sure you want to continue?"
|
||||
confirmCta = "Refresh Keys"
|
||||
|
||||
[config.apiKeys.alert]
|
||||
apiKeyErrorTitle = "API Key Error"
|
||||
failedToCreateApiKey = "Failed to create API key."
|
||||
failedToRetrieveApiKey = "Failed to retrieve API key from response."
|
||||
failedToFetchApiKey = "Failed to fetch API key."
|
||||
apiKeyRefreshed = "API Key Refreshed"
|
||||
apiKeyRefreshedBody = "Your API key has been successfully refreshed."
|
||||
failedToRefreshApiKey = "Failed to refresh API key."
|
||||
|
||||
[AddAttachmentsRequest]
|
||||
attachments = "Select Attachments"
|
||||
info = "Select files to attach to your PDF. These files will be embedded and accessible through the PDF's attachment panel."
|
||||
|
||||
@ -16,9 +16,12 @@ export function useRestartServer() {
|
||||
};
|
||||
|
||||
const restartServer = async () => {
|
||||
try {
|
||||
setRestartModalOpened(false);
|
||||
setRestartModalOpened(false);
|
||||
|
||||
await apiClient.post('/api/v1/admin/settings/restart', undefined, {
|
||||
suppressErrorToast: true,
|
||||
})
|
||||
.then(() => {
|
||||
alert({
|
||||
alertType: 'neutral',
|
||||
title: t('admin.settings.restarting', 'Restarting Server'),
|
||||
@ -27,14 +30,12 @@ export function useRestartServer() {
|
||||
'The server is restarting. Please wait a moment...'
|
||||
),
|
||||
});
|
||||
|
||||
await apiClient.post('/api/v1/admin/settings/restart');
|
||||
|
||||
// Wait a moment then reload the page
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 3000);
|
||||
} catch (_error) {
|
||||
})
|
||||
.catch(async (_error) => {
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('admin.error', 'Error'),
|
||||
@ -43,7 +44,7 @@ export function useRestartServer() {
|
||||
'Failed to restart server. Please restart manually.'
|
||||
),
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import apiClient from "@app/services/apiClient";
|
||||
import { alert } from "@app/components/toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function useApiKey() {
|
||||
const [apiKey, setApiKey] = useState<string | null>(null);
|
||||
@ -7,49 +9,107 @@ export function useApiKey() {
|
||||
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [hasAttempted, setHasAttempted] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
function failedToCreateAlert() {
|
||||
alert({
|
||||
alertType: "error",
|
||||
title: t("config.apiKeys.alert.apiKeyErrorTitle", "API Key Error"),
|
||||
body: t("config.apiKeys.alert.failedToCreateApiKey", "Failed to create API key."),
|
||||
isPersistentPopup: false,
|
||||
});
|
||||
}
|
||||
|
||||
const fetchKey = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
// Backend is POST for get and update
|
||||
const res = await apiClient.post("/api/v1/user/get-api-key");
|
||||
const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
|
||||
if (typeof value === "string") setApiKey(value);
|
||||
} catch (e: any) {
|
||||
// If not found, try to create one by calling update endpoint
|
||||
if (e?.response?.status === 404) {
|
||||
try {
|
||||
const createRes = await apiClient.post("/api/v1/user/update-api-key");
|
||||
const created =
|
||||
typeof createRes.data === "string"
|
||||
? createRes.data
|
||||
: createRes.data?.apiKey;
|
||||
if (typeof created === "string") setApiKey(created);
|
||||
} catch (createErr: any) {
|
||||
setError(createErr);
|
||||
// Backend is POST for get and update
|
||||
await apiClient
|
||||
.post("/api/v1/user/get-api-key", undefined, {
|
||||
responseType: "json",
|
||||
})
|
||||
.then((response) => {
|
||||
const data = response.data;
|
||||
const apiKeyValue = typeof data === "string" ? data : data?.apiKey;
|
||||
if (typeof apiKeyValue === "string") {
|
||||
setApiKey(apiKeyValue);
|
||||
} else {
|
||||
alert({
|
||||
alertType: "error",
|
||||
title: t("config.apiKeys.alert.apiKeyErrorTitle", "API Key Error"),
|
||||
body: t("config.apiKeys.alert.failedToRetrieveApiKey", "Failed to retrieve API key from response."),
|
||||
isPersistentPopup: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setError(e);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setHasAttempted(true);
|
||||
}
|
||||
})
|
||||
.catch(async (e) => {
|
||||
// If not found, try to create one by calling update endpoint
|
||||
if (e?.response?.status === 404) {
|
||||
await apiClient
|
||||
.post("/api/v1/user/update-api-key")
|
||||
.then((createRes) => {
|
||||
const created = typeof createRes.data === "string" ? createRes.data : createRes.data?.apiKey;
|
||||
if (typeof created === "string") {
|
||||
setApiKey(created);
|
||||
} else {
|
||||
failedToCreateAlert();
|
||||
}
|
||||
})
|
||||
.catch((createErr) => {
|
||||
failedToCreateAlert();
|
||||
setError(createErr);
|
||||
});
|
||||
} else {
|
||||
alert({
|
||||
alertType: "error",
|
||||
title: t("config.apiKeys.alert.apiKeyErrorTitle", "API Key Error"),
|
||||
body: t("config.apiKeys.alert.failedToFetchApiKey", "Failed to fetch API key."),
|
||||
isPersistentPopup: false,
|
||||
});
|
||||
setError(e);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
setHasAttempted(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setIsRefreshing(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await apiClient.post("/api/v1/user/update-api-key");
|
||||
await apiClient.post("/api/v1/user/update-api-key", undefined, {
|
||||
responseType: "json",
|
||||
suppressErrorToast: true,
|
||||
}).then((res) => {
|
||||
const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
|
||||
if (typeof value === "string") setApiKey(value);
|
||||
} catch (e: any) {
|
||||
if (typeof value === "string") {
|
||||
alert({
|
||||
alertType: "success",
|
||||
title: t("config.apiKeys.alert.apiKeyRefreshed", "API Key Refreshed"),
|
||||
body: t("config.apiKeys.alert.apiKeyRefreshedBody", "Your API key has been successfully refreshed."),
|
||||
isPersistentPopup: false,
|
||||
});
|
||||
setApiKey(value);
|
||||
} else {
|
||||
alert({
|
||||
alertType: "error",
|
||||
title: t("config.apiKeys.alert.apiKeyErrorTitle", "API Key Error"),
|
||||
body: t("config.apiKeys.alert.failedToRefreshApiKey", "Failed to refresh API key."),
|
||||
isPersistentPopup: false,
|
||||
});
|
||||
}
|
||||
}).catch((e) => {
|
||||
alert({
|
||||
alertType: "error",
|
||||
title: t("config.apiKeys.alert.apiKeyErrorTitle", "API Key Error"),
|
||||
body: t("config.apiKeys.alert.failedToRefreshApiKey", "Failed to refresh API key."),
|
||||
isPersistentPopup: false,
|
||||
});
|
||||
setError(e);
|
||||
} finally {
|
||||
}).finally(() => {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user