mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 13:20:08 +03:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78964f1e4e | ||
|
|
be13028209 | ||
|
|
d4b1862654 | ||
|
|
51835a7b5e | ||
|
|
97c0ccf582 | ||
|
|
4e46ba3b5a | ||
|
|
cbe3ef8f69 | ||
|
|
5b5e922069 | ||
|
|
a215c30068 | ||
|
|
897c72e9d9 | ||
|
|
732ef18ae5 | ||
|
|
f7a2c626c9 | ||
|
|
caeca0b88a | ||
|
|
c93feb5dfc | ||
|
|
f945cc7dc6 | ||
|
|
72b7892312 | ||
|
|
353df7a647 | ||
|
|
0d75715af2 |
@@ -8,11 +8,6 @@ updates:
|
||||
- package-ecosystem: "gradle" # See documentation for possible values
|
||||
directories:
|
||||
- "/" # Location of package manifests
|
||||
- "/app/common"
|
||||
- "/app/core"
|
||||
- "/app/proprietary"
|
||||
- "/app/saas"
|
||||
- "/buildSrc"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
|
||||
@@ -67,6 +67,7 @@ labels:
|
||||
- 'frontend/**'
|
||||
- 'frontend/.*'
|
||||
- 'frontend/**/.*'
|
||||
- '.taskfiles/frontend.yml'
|
||||
|
||||
- label: 'Tauri'
|
||||
files:
|
||||
|
||||
+62
-3
@@ -40,12 +40,15 @@ tasks:
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED | default "false"}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS | default "120"}}'
|
||||
SECURITY_ENABLELOGIN: '{{.SECURITY_ENABLELOGIN | default ""}}'
|
||||
# Set by dev:linked. Inline rather than in `env:` so an empty value emits nothing
|
||||
# and cannot blank the committed default.
|
||||
ACCOUNT_LINK_SAAS_BASE_URL: '{{.ACCOUNT_LINK_SAAS_BASE_URL | default ""}}'
|
||||
env:
|
||||
SERVER_PORT: '{{.PORT}}'
|
||||
cmds:
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}cmd /c ".\gradlew.bat :stirling-pdf:bootRun"'
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{if .ACCOUNT_LINK_SAAS_BASE_URL}}STIRLING_BILLING_ACCOUNT_LINK_ENABLED=true STIRLING_BILLING_ACCOUNT_LINK_SAAS_BASE_URL={{.ACCOUNT_LINK_SAAS_BASE_URL}} {{end}}cmd /c ".\gradlew.bat :stirling-pdf:bootRun"'
|
||||
platforms: [windows]
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}./gradlew :stirling-pdf:bootRun'
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED={{.AIENGINE_ENABLED}} AIENGINE_TIMEOUTSECONDS={{.AIENGINE_TIMEOUTSECONDS}} {{end}}{{if .SECURITY_ENABLELOGIN}}SECURITY_ENABLELOGIN={{.SECURITY_ENABLELOGIN}} {{end}}{{if .ACCOUNT_LINK_SAAS_BASE_URL}}STIRLING_BILLING_ACCOUNT_LINK_ENABLED=true STIRLING_BILLING_ACCOUNT_LINK_SAAS_BASE_URL={{.ACCOUNT_LINK_SAAS_BASE_URL}} {{end}}./gradlew :stirling-pdf:bootRun'
|
||||
platforms: [linux, darwin]
|
||||
|
||||
dev:bundled:
|
||||
@@ -84,6 +87,8 @@ tasks:
|
||||
AIENGINE_URL: '{{.AIENGINE_URL}}'
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
|
||||
APP_BASE_URL: '{{.APP_BASE_URL}}'
|
||||
BASE_PATH: '{{.BASE_PATH}}'
|
||||
|
||||
staging:saas:
|
||||
desc: "Start SaaS backend against the shared v3 staging project"
|
||||
@@ -95,10 +100,47 @@ tasks:
|
||||
AIENGINE_URL: '{{.AIENGINE_URL}}'
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
|
||||
APP_BASE_URL: '{{.APP_BASE_URL}}'
|
||||
BASE_PATH: '{{.BASE_PATH}}'
|
||||
|
||||
dev:linked:
|
||||
desc: "Self-hosted backend linked to a locally running SaaS backend (see task linked:*)"
|
||||
ignore_error: true
|
||||
vars:
|
||||
PORT: '{{.PORT | default "8080"}}'
|
||||
SAAS_BASE_URL: '{{.SAAS_BASE_URL | default "http://localhost:8081"}}'
|
||||
cmds:
|
||||
- 'echo ">> self-hosted :{{.PORT}} linking to SaaS at {{.SAAS_BASE_URL}}"'
|
||||
# The two backends run different STIRLING_FLAVOURs, which are different Gradle
|
||||
# project graphs sharing one build/ tree. Waiting avoids overlapping builds; it
|
||||
# does not make the sharing safe, so avoid rebuilding one while the other runs.
|
||||
- cmd: |
|
||||
n=0
|
||||
while [ "$n" -lt 150 ]; do
|
||||
if curl -s -m 2 "{{.SAAS_BASE_URL}}" >/dev/null 2>&1; then
|
||||
echo ">> SaaS backend is up, starting self-hosted"
|
||||
break
|
||||
fi
|
||||
n=$((n + 1))
|
||||
{{if eq OS "windows"}}powershell -NoProfile -Command "Start-Sleep -Seconds 2"{{else}}sleep 2{{end}}
|
||||
done
|
||||
if [ "$n" -ge 150 ]; then
|
||||
echo ">> SaaS backend never answered; starting anyway"
|
||||
fi
|
||||
- task: dev:proprietary
|
||||
vars:
|
||||
PORT: '{{.PORT}}'
|
||||
ACCOUNT_LINK_SAAS_BASE_URL: '{{.SAAS_BASE_URL}}'
|
||||
|
||||
_run:saas:
|
||||
internal: true
|
||||
dotenv: ['app/.env.saas.local', 'app/.env.saas']
|
||||
# The frontend files are here only for RUN_SUBPATH, which the authorize URL needs.
|
||||
# Last, because dotenv is set-if-absent: app/* still decides everything else.
|
||||
dotenv:
|
||||
- 'app/.env.saas.local'
|
||||
- 'app/.env.saas'
|
||||
- 'frontend/editor/.env.saas.local'
|
||||
- 'frontend/editor/.env.saas'
|
||||
ignore_error: true
|
||||
vars:
|
||||
PORT: '{{.PORT | default "8080"}}'
|
||||
@@ -111,12 +153,29 @@ tasks:
|
||||
AIENGINE_URL: '{{.AIENGINE_URL | default ""}}'
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED | default "false"}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS | default "120"}}'
|
||||
# Empty is the same as unset: the property defaults to empty and is blank-checked.
|
||||
APP_BASE_URL: '{{.APP_BASE_URL | default ""}}'
|
||||
# Relocates configs/pipeline/logs, for a second backend in the same directory.
|
||||
# Empty is the same as unset: the reader blank-checks it.
|
||||
BASE_PATH: '{{.BASE_PATH | default ""}}'
|
||||
env:
|
||||
SERVER_PORT: '{{.PORT}}'
|
||||
STIRLING_FLAVOR: saas
|
||||
STIRLING_BASE_PATH: '{{.BASE_PATH}}'
|
||||
AIENGINE_URL: '{{.AIENGINE_URL}}'
|
||||
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
|
||||
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
|
||||
# Appends RUN_SUBPATH: the approval page is at <base>/link, so a subpath build
|
||||
# serves it at <base>/app/link. An explicit value still wins.
|
||||
SYSTEM_FRONTENDURL:
|
||||
sh: |
|
||||
if [ -n "${SYSTEM_FRONTENDURL:-}" ]; then
|
||||
echo "${SYSTEM_FRONTENDURL}"
|
||||
elif [ -n "{{.APP_BASE_URL}}" ] && [ -n "${RUN_SUBPATH:-}" ]; then
|
||||
echo "{{.APP_BASE_URL}}/${RUN_SUBPATH}"
|
||||
else
|
||||
echo "{{.APP_BASE_URL}}"
|
||||
fi
|
||||
cmds:
|
||||
# PROFILE_ARGS is empty when PROFILES=none, i.e. the bare `saas` profile
|
||||
# against SAAS_DB_* (production).
|
||||
|
||||
+14
-4
@@ -23,7 +23,7 @@ tasks:
|
||||
- package-lock.json
|
||||
- package.json
|
||||
status:
|
||||
- test -d node_modules
|
||||
- npm ls --depth=0
|
||||
env:
|
||||
CI: '{{ .CI | default "false" }}'
|
||||
|
||||
@@ -121,17 +121,17 @@ tasks:
|
||||
sh: |
|
||||
case "${SAAS_ENV:-dev}" in
|
||||
staging) ref="${SAAS_STAGING_PROJECT_REF:?set it in app/.env.saas.local}" ;;
|
||||
*) ref="${SAAS_DEV_PROJECT_REF:?set it in app/.env.saas.local, or run task staging:saas}" ;;
|
||||
*) ref="${SAAS_DEV_PROJECT_REF:?set it in app/.env.saas.local, or pass SAAS_ENV=staging}" ;;
|
||||
esac
|
||||
echo "https://${ref}.supabase.co"
|
||||
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY:
|
||||
sh: |
|
||||
case "${SAAS_ENV:-dev}" in
|
||||
staging) echo "${SAAS_STAGING_PUBLISHABLE_KEY:?set it in app/.env.saas.local}" ;;
|
||||
*) echo "${SAAS_DEV_PUBLISHABLE_KEY:?set it in app/.env.saas.local}" ;;
|
||||
*) echo "${SAAS_DEV_PUBLISHABLE_KEY:?set it in app/.env.saas.local, or pass SAAS_ENV=staging}" ;;
|
||||
esac
|
||||
cmds:
|
||||
- 'echo ">> frontend Supabase target: $VITE_SUPABASE_URL"'
|
||||
- 'echo ">> frontend {{.SAAS_ENV}}: Supabase $VITE_SUPABASE_URL, backend $BACKEND_URL"'
|
||||
- npx vite editor --mode saas --port {{.PORT}}{{if .OPEN}} --open{{end}}
|
||||
|
||||
dev:
|
||||
@@ -173,6 +173,16 @@ tasks:
|
||||
OPEN: '{{.OPEN}}'
|
||||
SAAS_ENV: '{{.SAAS_ENV}}'
|
||||
|
||||
staging:saas:
|
||||
desc: "Start frontend dev server against the shared v3 staging project"
|
||||
cmds:
|
||||
- task: dev:saas
|
||||
vars:
|
||||
SAAS_ENV: staging
|
||||
PORT: '{{.PORT}}'
|
||||
BACKEND_URL: '{{.BACKEND_URL}}'
|
||||
OPEN: '{{.OPEN}}'
|
||||
|
||||
dev:desktop:
|
||||
desc: "Start frontend dev server in desktop mode"
|
||||
deps:
|
||||
|
||||
@@ -121,6 +121,92 @@ tasks:
|
||||
cmds:
|
||||
- task: dev:_all
|
||||
|
||||
# No engine: linking never calls it.
|
||||
linked:staging:
|
||||
desc: "SaaS on the shared v3 project + a self-hosted instance linked to it"
|
||||
cmds:
|
||||
- task: linked:_all
|
||||
vars: { SAAS_ENV: staging }
|
||||
|
||||
linked:dev:
|
||||
desc: "SaaS on the current PR's preview branch + a self-hosted instance linked to it"
|
||||
cmds:
|
||||
- task: linked:_all
|
||||
vars: { SAAS_ENV: dev }
|
||||
|
||||
linked:_all:
|
||||
internal: true
|
||||
vars:
|
||||
SAAS_ENV: '{{.SAAS_ENV | default "staging"}}'
|
||||
PORTS:
|
||||
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8081 5174 8080 5173{{else}}{{.FIND_FREE_PORT_SH}} 8081 5174 8080 5173{{end}}'
|
||||
SAAS_BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
|
||||
SAAS_FRONTEND_PORT: '{{index (splitList "\n" .PORTS) 1}}'
|
||||
APP_BACKEND_PORT: '{{index (splitList "\n" .PORTS) 2}}'
|
||||
APP_FRONTEND_PORT: '{{index (splitList "\n" .PORTS) 3}}'
|
||||
deps:
|
||||
# APP_BASE_URL is the SaaS *frontend*: the approval page is served by vite, not
|
||||
# by the API. BASE_PATH moves this backend's configs/pipeline aside so it does not
|
||||
# race the self-hosted one, which keeps ./configs and its existing database.
|
||||
- task: 'backend:{{.SAAS_ENV}}:saas'
|
||||
vars:
|
||||
PORT: '{{.SAAS_BACKEND_PORT}}'
|
||||
APP_BASE_URL: 'http://localhost:{{.SAAS_FRONTEND_PORT}}'
|
||||
BASE_PATH: 'tmp/linked-saas'
|
||||
- task: frontend:dev:saas
|
||||
vars:
|
||||
PORT: '{{.SAAS_FRONTEND_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.SAAS_BACKEND_PORT}}'
|
||||
SAAS_ENV: '{{.SAAS_ENV}}'
|
||||
- task: backend:dev:linked
|
||||
vars:
|
||||
PORT: '{{.APP_BACKEND_PORT}}'
|
||||
SAAS_BASE_URL: 'http://localhost:{{.SAAS_BACKEND_PORT}}'
|
||||
- task: frontend:dev:proprietary
|
||||
vars:
|
||||
PORT: '{{.APP_FRONTEND_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.APP_BACKEND_PORT}}'
|
||||
OPEN: "true"
|
||||
- task: linked:_ready
|
||||
vars:
|
||||
SAAS_BACKEND_PORT: '{{.SAAS_BACKEND_PORT}}'
|
||||
SAAS_FRONTEND_PORT: '{{.SAAS_FRONTEND_PORT}}'
|
||||
APP_BACKEND_PORT: '{{.APP_BACKEND_PORT}}'
|
||||
APP_FRONTEND_PORT: '{{.APP_FRONTEND_PORT}}'
|
||||
|
||||
# Waits for all four to answer, then prints where they landed.
|
||||
linked:_ready:
|
||||
internal: true
|
||||
cmds:
|
||||
- cmd: |
|
||||
n=0
|
||||
ok=0
|
||||
while [ "$n" -lt 150 ]; do
|
||||
ok=1
|
||||
for u in "http://localhost:{{.SAAS_BACKEND_PORT}}" \
|
||||
"http://localhost:{{.SAAS_FRONTEND_PORT}}" \
|
||||
"http://localhost:{{.APP_BACKEND_PORT}}" \
|
||||
"http://localhost:{{.APP_FRONTEND_PORT}}"; do
|
||||
# Not -o /dev/null: Windows curl.exe treats it as a real path and exits 23.
|
||||
curl -s -m 2 "$u" >/dev/null 2>&1 || ok=0
|
||||
done
|
||||
if [ "$ok" = 1 ]; then break; fi
|
||||
n=$((n + 1))
|
||||
# `sleep` is a binary, not a builtin, and Windows has none.
|
||||
{{if eq OS "windows"}}powershell -NoProfile -Command "Start-Sleep -Seconds 2"{{else}}sleep 2{{end}}
|
||||
done
|
||||
echo ""
|
||||
if [ "$ok" = 1 ]; then
|
||||
echo ">> all four answering"
|
||||
else
|
||||
echo ">> still waiting on one or more after 5 minutes; addresses below anyway"
|
||||
fi
|
||||
echo ">> self-hosted UI http://localhost:{{.APP_FRONTEND_PORT}}/processor"
|
||||
echo ">> self-hosted api http://localhost:{{.APP_BACKEND_PORT}}"
|
||||
echo ">> saas UI http://localhost:{{.SAAS_FRONTEND_PORT}}"
|
||||
echo ">> saas api http://localhost:{{.SAAS_BACKEND_PORT}}"
|
||||
echo ""
|
||||
|
||||
dev:_all:
|
||||
internal: true
|
||||
vars:
|
||||
|
||||
+32
-7
@@ -3,6 +3,10 @@ bootRun {
|
||||
enabled = false
|
||||
}
|
||||
dependencies {
|
||||
// Security-hardening utilities (zip-slip, SSRF, filename sanitization, command injection).
|
||||
// Declared as api here so core + proprietary (which depend on common) get it transitively,
|
||||
// keeping it off modules that don't need it (e.g. saas).
|
||||
api 'io.github.pixee:java-security-toolkit:1.2.3'
|
||||
api "com.google.guava:guava:${guavaVersion}"
|
||||
api 'org.springframework.boot:spring-boot-starter-webmvc'
|
||||
api 'org.springframework.boot:spring-boot-starter-aspectj'
|
||||
@@ -22,7 +26,10 @@ dependencies {
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3"
|
||||
// Simple Java Mail for EML/MSG parsing (replaces direct Angus Mail usage)
|
||||
api 'org.simplejavamail:simple-java-mail:9.3.2'
|
||||
api 'org.simplejavamail:outlook-module:9.3.2' // MSG file support
|
||||
// MSG file support; exclude commons-math3 (only HSSF/formula needs it, MSG parsing doesn't)
|
||||
api('org.simplejavamail:outlook-module:9.3.2') {
|
||||
exclude group: 'org.apache.commons', module: 'commons-math3'
|
||||
}
|
||||
api 'jakarta.mail:jakarta.mail-api:2.1.5'
|
||||
runtimeOnly 'org.eclipse.angus:angus-mail:2.0.5'
|
||||
|
||||
@@ -36,12 +43,30 @@ dependencies {
|
||||
|
||||
api "com.stirling:jpdfium:${jpdfiumVersion}"
|
||||
|
||||
// -PjpdfiumPlatforms=all|none|<csv of linux-x64,linux-arm64,darwin-x64,darwin-arm64,windows-x64>
|
||||
// 'none' skips natives entirely (windows-arm64 builds, until JPDFium ships that platform).
|
||||
def jpdfiumPlatformsProp = (project.findProperty('jpdfiumPlatforms') ?: 'all').toString().trim()
|
||||
def jpdfiumAllPlatforms = ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64', 'windows-x64']
|
||||
// -PjpdfiumPlatforms=auto|all|none|<csv of linux-x64,linux-arm64,linux-musl-x64,linux-musl-arm64,darwin-x64,darwin-arm64,windows-x64> (windows-arm64 natives not published yet)
|
||||
def jpdfiumPlatformsProp = (project.findProperty('jpdfiumPlatforms') ?: 'auto').toString().trim()
|
||||
def jpdfiumAllPlatforms = ['linux-x64', 'linux-arm64', 'linux-musl-x64', 'linux-musl-arm64', 'darwin-x64', 'darwin-arm64', 'windows-x64']
|
||||
def jpdfiumPlatforms
|
||||
if (jpdfiumPlatformsProp == 'all') {
|
||||
if (jpdfiumPlatformsProp == 'auto') {
|
||||
def osName = System.getProperty('os.name').toLowerCase()
|
||||
def osArch = System.getProperty('os.arch').toLowerCase()
|
||||
def isArm64 = osArch.contains('aarch64') || osArch.contains('arm64')
|
||||
if (osName.contains('linux')) {
|
||||
jpdfiumPlatforms = isArm64 ? ['linux-arm64'] : ['linux-x64']
|
||||
} else if (osName.contains('mac')) {
|
||||
jpdfiumPlatforms = isArm64 ? ['darwin-arm64'] : ['darwin-x64']
|
||||
} else if (osName.contains('win')) {
|
||||
if (isArm64) {
|
||||
logger.lifecycle("JPDFium natives are not available for windows-arm64; set -PjpdfiumPlatforms=none to skip bundling natives.")
|
||||
jpdfiumPlatforms = []
|
||||
} else {
|
||||
jpdfiumPlatforms = ['windows-x64']
|
||||
}
|
||||
} else {
|
||||
// Fallback: bundle all platforms when host can't be determined
|
||||
jpdfiumPlatforms = jpdfiumAllPlatforms
|
||||
}
|
||||
} else if (jpdfiumPlatformsProp == 'all') {
|
||||
jpdfiumPlatforms = jpdfiumAllPlatforms
|
||||
} else if (jpdfiumPlatformsProp == 'none') {
|
||||
jpdfiumPlatforms = []
|
||||
@@ -51,7 +76,7 @@ dependencies {
|
||||
def jpdfiumInvalid = jpdfiumPlatforms.findAll { !jpdfiumAllPlatforms.contains(it) }
|
||||
if (jpdfiumInvalid) {
|
||||
throw new GradleException("Unknown jpdfiumPlatforms value(s): ${jpdfiumInvalid.join(', ')}. " +
|
||||
"Valid: ${jpdfiumAllPlatforms.join(', ')}, 'all' or 'none'.")
|
||||
"Valid: ${jpdfiumAllPlatforms.join(', ')}, 'auto', 'all' or 'none'.")
|
||||
}
|
||||
logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms ? jpdfiumPlatforms.join(', ') : 'none'}")
|
||||
jpdfiumPlatforms.each { platform ->
|
||||
|
||||
@@ -106,6 +106,7 @@ SwaggerDoc.json
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
*.log.gz
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
+14
-2
@@ -62,8 +62,16 @@ dependencies {
|
||||
// CVE-2022-25647: Explicit gson to prevent unsafe deserialization (tabula would pull 2.8.7)
|
||||
implementation "com.google.code.gson:gson:${gsonVersion}"
|
||||
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.5'
|
||||
implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv
|
||||
implementation 'org.apache.poi:poi-ooxml:5.5.1'
|
||||
// OpenCSV: Stirling-PDF only uses CSVWriter, not the opencsv-bean module.
|
||||
// Exclude commons-beanutils + commons-collections.
|
||||
implementation('com.opencsv:opencsv:5.12.0') {
|
||||
exclude group: 'commons-beanutils', module: 'commons-beanutils'
|
||||
exclude group: 'commons-collections', module: 'commons-collections'
|
||||
}
|
||||
// POI: only XSSF (modern Excel) is used, not HSSF/FormulaEvaluator which need commons-math3.
|
||||
implementation('org.apache.poi:poi-ooxml:5.5.1') {
|
||||
exclude group: 'org.apache.commons', module: 'commons-math3'
|
||||
}
|
||||
|
||||
// Batik only bridge module needed (transitively pulls anim, gvt, util, css, dom, svg-dom)
|
||||
// Replaces batik-all which included unused codec, svggen, transcoder, script modules
|
||||
@@ -129,6 +137,10 @@ bootJar {
|
||||
exclude 'META-INF/*.RSA'
|
||||
exclude 'META-INF/*.EC'
|
||||
|
||||
// Exclude source maps from production JAR, dev-only debugging artifacts, not needed at runtime
|
||||
exclude 'static/pdfjs-legacy/**/*.map'
|
||||
exclude 'static/**/*.map'
|
||||
|
||||
manifest {
|
||||
attributes(
|
||||
'Implementation-Title': 'Stirling-PDF',
|
||||
|
||||
@@ -114,6 +114,7 @@ public class OCRController {
|
||||
List<String> selectedLanguages = request.getLanguages();
|
||||
boolean sidecar = request.isSidecar();
|
||||
Boolean deskew = request.isDeskew();
|
||||
Boolean rotatePages = request.isRotatePages();
|
||||
Boolean clean = request.isClean();
|
||||
Boolean cleanFinal = request.isCleanFinal();
|
||||
String ocrType = request.getOcrType();
|
||||
@@ -154,6 +155,7 @@ public class OCRController {
|
||||
selectedLanguages,
|
||||
sidecar,
|
||||
deskew,
|
||||
rotatePages,
|
||||
clean,
|
||||
cleanFinal,
|
||||
ocrType,
|
||||
@@ -236,6 +238,7 @@ public class OCRController {
|
||||
List<String> selectedLanguages,
|
||||
Boolean sidecar,
|
||||
Boolean deskew,
|
||||
Boolean rotatePages,
|
||||
Boolean clean,
|
||||
Boolean cleanFinal,
|
||||
String ocrType,
|
||||
@@ -268,6 +271,10 @@ public class OCRController {
|
||||
if (deskew != null && deskew) {
|
||||
command.add("--deskew");
|
||||
}
|
||||
if (rotatePages != null && rotatePages) {
|
||||
// Tesseract OSD-based automatic page orientation correction (90/180/270)
|
||||
command.add("--rotate-pages");
|
||||
}
|
||||
if (clean != null && clean) {
|
||||
command.add("--clean");
|
||||
}
|
||||
|
||||
+4
@@ -221,6 +221,10 @@ public class RedactController {
|
||||
.normalizeFonts(false)
|
||||
.fixToUnicode(false)
|
||||
.glyphAware(true)
|
||||
.ligatureAware(true)
|
||||
.bidiAware(true)
|
||||
.graphemeSafe(true)
|
||||
.sanitizeStructure(false) // WIP/Experimental API
|
||||
.redactMetadata(true)
|
||||
.build();
|
||||
|
||||
|
||||
+4
@@ -110,6 +110,10 @@ class TextRedactionService {
|
||||
.fixToUnicode(false)
|
||||
.repairWidths(false)
|
||||
.glyphAware(true)
|
||||
.ligatureAware(true)
|
||||
.bidiAware(true)
|
||||
.graphemeSafe(true)
|
||||
.sanitizeStructure(false)
|
||||
.build();
|
||||
|
||||
try (PdfDocument checkDoc = PdfDocument.open(tempIn.toPath())) {
|
||||
|
||||
+5
@@ -25,6 +25,11 @@ public class ProcessPdfWithOcrRequest extends PDFFile {
|
||||
@Schema(description = "Deskew the input file if set to true")
|
||||
private boolean deskew;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"Auto-correct page orientation (90/180/270) using Tesseract OSD if set to true")
|
||||
private boolean rotatePages;
|
||||
|
||||
@Schema(description = "Clean the input file if set to true")
|
||||
private boolean clean;
|
||||
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
<pattern>%d %p %c{1} [%thread] %m%n</pattern>
|
||||
</encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_PATH}/auth-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>1</maxHistory>
|
||||
<fileNamePattern>${LOG_PATH}/auth-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
|
||||
<maxHistory>7</maxHistory>
|
||||
<totalSizeCap>64MB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
@@ -28,8 +29,9 @@
|
||||
<pattern>%d %p %c{1} [%thread] %m%n</pattern>
|
||||
</encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_PATH}/info-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>1</maxHistory>
|
||||
<fileNamePattern>${LOG_PATH}/info-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
|
||||
<maxHistory>7</maxHistory>
|
||||
<totalSizeCap>256MB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ system:
|
||||
maxDPI: 500 # Maximum allowed DPI for PDF to image conversion
|
||||
corsAllowedOrigins: [] # List of allowed origins for CORS (e.g. ['http://localhost:5173', 'https://app.example.com']). WARNING: leaving this empty falls back to allowing ALL origins (with credentials), it does NOT disable CORS. Set explicit origins to lock it down.
|
||||
backendUrl: "" # Backend base URL for SAML/OAuth/API callbacks (e.g. 'http://localhost:8080' for dev, 'https://api.example.com' for production). REQUIRED for SSO authentication to work correctly. This is where your IdP will send SAML responses and OAuth callbacks. Leave empty to default to 'http://localhost:8080' in development.
|
||||
frontendUrl: "" # Frontend URL for invite email links (e.g. 'https://app.example.com'). Optional - if not set, will use backendUrl. This is the URL users click in invite emails.
|
||||
frontendUrl: "" # Base URL of the web app, as a browser reaches it (e.g. 'https://app.example.com', or 'https://example.com/app' if served under a base path). Optional - if not set, will use backendUrl. Used for any link handed to a browser: invite emails, share links, mobile QR codes, and the account-link handshake.
|
||||
enableMobileScanner: true # Enable mobile phone QR code upload feature. Requires frontendUrl to be configured.
|
||||
enableMobileSignature: true # Enable drawing signatures on a phone via QR code from the Sign tool. Requires frontendUrl to be configured.
|
||||
mobileScannerSettings:
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
Bag Attributes
|
||||
friendlyName: alias
|
||||
localKeyID: 43 4A B0 2D D5 03 52 9F 5B 78 50 64 54 22 AB F7 C8 0B 1F 2B
|
||||
subject=C = US, ST = CA, L = SF, O = Test, OU = Test, CN = Test
|
||||
issuer=C = US, ST = CA, L = SF, O = Test, OU = Test, CN = Test
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDiTCCAnGgAwIBAgIUdWDUiSWDll+owMQEzypIuChp+bcwDQYJKoZIhvcNAQEL
|
||||
MIIDizCCAnOgAwIBAgIUZMcjBbPlADpy5PssUsPlPM/h7dcwDQYJKoZIhvcNAQEL
|
||||
BQAwVDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjENMAsG
|
||||
A1UECgwEVGVzdDENMAsGA1UECwwEVGVzdDENMAsGA1UEAwwEVGVzdDAeFw0yNTA4
|
||||
MjYwNzQxMTBaFw0yNjA4MjYwNzQxMTBaMFQxCzAJBgNVBAYTAlVTMQswCQYDVQQI
|
||||
DAJDQTELMAkGA1UEBwwCU0YxDTALBgNVBAoMBFRlc3QxDTALBgNVBAsMBFRlc3Qx
|
||||
DTALBgNVBAMMBFRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDM
|
||||
SfspXLx1WAKSo3AfDYIJAyeSrqFcTsPoNBEvT2U1b8w+SCTw4xR5sC3pNenbiEQ7
|
||||
4sI60hgURtOMOAt+iKvfI0A/9N8/wYadXUyis4qGZPkM/F6H5cBF9VaYisGptY2w
|
||||
ad9X8XcZgZFABYA5O50Jb5nbUM8fPwDYz2fISIejIpW36y+ApFsotJQCaISe4UWb
|
||||
K7bwW4UycghYh7AqfH/1OvgR35gGeL7S+SC0F+CZqGECgansFOh/yYL6VoatoggV
|
||||
oZxjIQblmuSrLtfwN1S7ngn85k3NFMBHm1ehMOHabx5G58Wg05/0mBK8bIrwjrNp
|
||||
Wzomit8BQJ7eIYUikZfVAgMBAAGjUzBRMB0GA1UdDgQWBBRm6hGFGnC1dxipumf/
|
||||
6ROdNE6/YDAfBgNVHSMEGDAWgBRm6hGFGnC1dxipumf/6ROdNE6/YDAPBgNVHRMB
|
||||
Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB66MPy5kZlSlBgsK4HtB1LSr3M
|
||||
dmBWbnQQMq9rmD9AIBQV/shiIjMXGRGnt9zaB0Gg9M39iEvISE6ByMpaDQqV0Md5
|
||||
9y4XJu0rg/aMXLaHOGDAWJsb7nCGDt12cWdgn1Ni2mmXUHv4SJCRXNQF7mSgIr+p
|
||||
Fvd1ljyvzu/iig8qxrcuWoZvY677p3yen4dN8ocgi8Df3KjduGbsTjFAESYqqNQC
|
||||
f+bvypQfhHjxdvz5W3Lpk2swUufqOvhO2b6+cshYJX98qLU8mhai/rOnYkHE7haq
|
||||
WDH6XEthnVGtk2VJ4XFDbz+FID440DPzy5u/1OZw2Mcoyp6y7rZDKC/D0Uvh
|
||||
A1UECgwEVGVzdDENMAsGA1UECwwEVGVzdDENMAsGA1UEAwwEVGVzdDAgFw0yNTAx
|
||||
MDEwMDAwMDBaGA8yMTI1MDEwMTAwMDAwMFowVDELMAkGA1UEBhMCVVMxCzAJBgNV
|
||||
BAgMAkNBMQswCQYDVQQHDAJTRjENMAsGA1UECgwEVGVzdDENMAsGA1UECwwEVGVz
|
||||
dDENMAsGA1UEAwwEVGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
|
||||
AMmEw2bpAoPnrUWkydKGmvDl0bHaCMoSY9yK2b/JL1xaTRwGT4H7DOuO/0Uu6/BV
|
||||
c93UEO0eHf1mt1kkYaRSPyOQLXF2QzRDkiW78c/xvqX+DgmfB7BFNrFSP+CRabdH
|
||||
wvepLUFtXJ6WwvWXjyvDXn8wEAirfETMdU8OXlPaJwS6cbQawYuB6GG5Z1ulxw6k
|
||||
GRi5hnq7PFJBxz1pg6Xx6pwKnaaNemW4Gp2B90St9N5yu7yV6V6XON84ZdohfXSQ
|
||||
livH3UTdkrpe+MO2m3CaAA19zlIxM6OIhkuo8r5GEPxoXCykPbgGMRFWPt0GNC3/
|
||||
AxZofAJg8atqy4peR8XL060CAwEAAaNTMFEwHQYDVR0OBBYEFDULr71QH24KKgNi
|
||||
2fyM6W9qJnzsMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUNQuvvVAfbgoq
|
||||
A2LZ/Izpb2omfOwwDQYJKoZIhvcNAQELBQADggEBAASZENsvvyxygLjE913BBLd2
|
||||
73unxWNSDXakT4xssDEysf//4nGPGo57KjUGFU6M64IVUmhPRTT3aYq7PyA9pb5Q
|
||||
Ijritg5PdxfUMqd+H3CT++JvJmXxmIrFtKD9fZbmBgRg2wU7yvcp7MP+w36CYmqe
|
||||
MH1YF29VcBcF+GfI8k00y83qYHuoHpzPrNcL/Gu6MtcC+1Hy96bs+NYIEFH67dy8
|
||||
IJrN3Vvr4VXSF9qA+Vp5RatPv+hEKZssEFK2fNpGMFPGc1r+HBQ/HEAL4M2betEo
|
||||
Ne4DigJ3CkTIYAd+cZ2m4tdtzbqkeXZJ7SL+/d/5MIXKTnYITw+NrpiMZ6I72Zk=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
Bag Attributes
|
||||
friendlyName: alias
|
||||
localKeyID: 43 4A B0 2D D5 03 52 9F 5B 78 50 64 54 22 AB F7 C8 0B 1F 2B
|
||||
subject=C = US, ST = CA, L = SF, O = Test, OU = Test, CN = Test
|
||||
issuer=C = US, ST = CA, L = SF, O = Test, OU = Test, CN = Test
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDiTCCAnGgAwIBAgIUdWDUiSWDll+owMQEzypIuChp+bcwDQYJKoZIhvcNAQEL
|
||||
MIIDizCCAnOgAwIBAgIUZMcjBbPlADpy5PssUsPlPM/h7dcwDQYJKoZIhvcNAQEL
|
||||
BQAwVDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjENMAsG
|
||||
A1UECgwEVGVzdDENMAsGA1UECwwEVGVzdDENMAsGA1UEAwwEVGVzdDAeFw0yNTA4
|
||||
MjYwNzQxMTBaFw0yNjA4MjYwNzQxMTBaMFQxCzAJBgNVBAYTAlVTMQswCQYDVQQI
|
||||
DAJDQTELMAkGA1UEBwwCU0YxDTALBgNVBAoMBFRlc3QxDTALBgNVBAsMBFRlc3Qx
|
||||
DTALBgNVBAMMBFRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDM
|
||||
SfspXLx1WAKSo3AfDYIJAyeSrqFcTsPoNBEvT2U1b8w+SCTw4xR5sC3pNenbiEQ7
|
||||
4sI60hgURtOMOAt+iKvfI0A/9N8/wYadXUyis4qGZPkM/F6H5cBF9VaYisGptY2w
|
||||
ad9X8XcZgZFABYA5O50Jb5nbUM8fPwDYz2fISIejIpW36y+ApFsotJQCaISe4UWb
|
||||
K7bwW4UycghYh7AqfH/1OvgR35gGeL7S+SC0F+CZqGECgansFOh/yYL6VoatoggV
|
||||
oZxjIQblmuSrLtfwN1S7ngn85k3NFMBHm1ehMOHabx5G58Wg05/0mBK8bIrwjrNp
|
||||
Wzomit8BQJ7eIYUikZfVAgMBAAGjUzBRMB0GA1UdDgQWBBRm6hGFGnC1dxipumf/
|
||||
6ROdNE6/YDAfBgNVHSMEGDAWgBRm6hGFGnC1dxipumf/6ROdNE6/YDAPBgNVHRMB
|
||||
Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB66MPy5kZlSlBgsK4HtB1LSr3M
|
||||
dmBWbnQQMq9rmD9AIBQV/shiIjMXGRGnt9zaB0Gg9M39iEvISE6ByMpaDQqV0Md5
|
||||
9y4XJu0rg/aMXLaHOGDAWJsb7nCGDt12cWdgn1Ni2mmXUHv4SJCRXNQF7mSgIr+p
|
||||
Fvd1ljyvzu/iig8qxrcuWoZvY677p3yen4dN8ocgi8Df3KjduGbsTjFAESYqqNQC
|
||||
f+bvypQfhHjxdvz5W3Lpk2swUufqOvhO2b6+cshYJX98qLU8mhai/rOnYkHE7haq
|
||||
WDH6XEthnVGtk2VJ4XFDbz+FID440DPzy5u/1OZw2Mcoyp6y7rZDKC/D0Uvh
|
||||
A1UECgwEVGVzdDENMAsGA1UECwwEVGVzdDENMAsGA1UEAwwEVGVzdDAgFw0yNTAx
|
||||
MDEwMDAwMDBaGA8yMTI1MDEwMTAwMDAwMFowVDELMAkGA1UEBhMCVVMxCzAJBgNV
|
||||
BAgMAkNBMQswCQYDVQQHDAJTRjENMAsGA1UECgwEVGVzdDENMAsGA1UECwwEVGVz
|
||||
dDENMAsGA1UEAwwEVGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
|
||||
AMmEw2bpAoPnrUWkydKGmvDl0bHaCMoSY9yK2b/JL1xaTRwGT4H7DOuO/0Uu6/BV
|
||||
c93UEO0eHf1mt1kkYaRSPyOQLXF2QzRDkiW78c/xvqX+DgmfB7BFNrFSP+CRabdH
|
||||
wvepLUFtXJ6WwvWXjyvDXn8wEAirfETMdU8OXlPaJwS6cbQawYuB6GG5Z1ulxw6k
|
||||
GRi5hnq7PFJBxz1pg6Xx6pwKnaaNemW4Gp2B90St9N5yu7yV6V6XON84ZdohfXSQ
|
||||
livH3UTdkrpe+MO2m3CaAA19zlIxM6OIhkuo8r5GEPxoXCykPbgGMRFWPt0GNC3/
|
||||
AxZofAJg8atqy4peR8XL060CAwEAAaNTMFEwHQYDVR0OBBYEFDULr71QH24KKgNi
|
||||
2fyM6W9qJnzsMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUNQuvvVAfbgoq
|
||||
A2LZ/Izpb2omfOwwDQYJKoZIhvcNAQELBQADggEBAASZENsvvyxygLjE913BBLd2
|
||||
73unxWNSDXakT4xssDEysf//4nGPGo57KjUGFU6M64IVUmhPRTT3aYq7PyA9pb5Q
|
||||
Ijritg5PdxfUMqd+H3CT++JvJmXxmIrFtKD9fZbmBgRg2wU7yvcp7MP+w36CYmqe
|
||||
MH1YF29VcBcF+GfI8k00y83qYHuoHpzPrNcL/Gu6MtcC+1Hy96bs+NYIEFH67dy8
|
||||
IJrN3Vvr4VXSF9qA+Vp5RatPv+hEKZssEFK2fNpGMFPGc1r+HBQ/HEAL4M2betEo
|
||||
Ne4DigJ3CkTIYAd+cZ2m4tdtzbqkeXZJ7SL+/d/5MIXKTnYITw+NrpiMZ6I72Zk=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,26 +1,21 @@
|
||||
Bag Attributes
|
||||
friendlyName: alias
|
||||
localKeyID: 43 4A B0 2D D5 03 52 9F 5B 78 50 64 54 22 AB F7 C8 0B 1F 2B
|
||||
subject=C = US, ST = CA, L = SF, O = Test, OU = Test, CN = Test
|
||||
issuer=C = US, ST = CA, L = SF, O = Test, OU = Test, CN = Test
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDiTCCAnGgAwIBAgIUdWDUiSWDll+owMQEzypIuChp+bcwDQYJKoZIhvcNAQEL
|
||||
MIIDizCCAnOgAwIBAgIUZMcjBbPlADpy5PssUsPlPM/h7dcwDQYJKoZIhvcNAQEL
|
||||
BQAwVDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjENMAsG
|
||||
A1UECgwEVGVzdDENMAsGA1UECwwEVGVzdDENMAsGA1UEAwwEVGVzdDAeFw0yNTA4
|
||||
MjYwNzQxMTBaFw0yNjA4MjYwNzQxMTBaMFQxCzAJBgNVBAYTAlVTMQswCQYDVQQI
|
||||
DAJDQTELMAkGA1UEBwwCU0YxDTALBgNVBAoMBFRlc3QxDTALBgNVBAsMBFRlc3Qx
|
||||
DTALBgNVBAMMBFRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDM
|
||||
SfspXLx1WAKSo3AfDYIJAyeSrqFcTsPoNBEvT2U1b8w+SCTw4xR5sC3pNenbiEQ7
|
||||
4sI60hgURtOMOAt+iKvfI0A/9N8/wYadXUyis4qGZPkM/F6H5cBF9VaYisGptY2w
|
||||
ad9X8XcZgZFABYA5O50Jb5nbUM8fPwDYz2fISIejIpW36y+ApFsotJQCaISe4UWb
|
||||
K7bwW4UycghYh7AqfH/1OvgR35gGeL7S+SC0F+CZqGECgansFOh/yYL6VoatoggV
|
||||
oZxjIQblmuSrLtfwN1S7ngn85k3NFMBHm1ehMOHabx5G58Wg05/0mBK8bIrwjrNp
|
||||
Wzomit8BQJ7eIYUikZfVAgMBAAGjUzBRMB0GA1UdDgQWBBRm6hGFGnC1dxipumf/
|
||||
6ROdNE6/YDAfBgNVHSMEGDAWgBRm6hGFGnC1dxipumf/6ROdNE6/YDAPBgNVHRMB
|
||||
Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB66MPy5kZlSlBgsK4HtB1LSr3M
|
||||
dmBWbnQQMq9rmD9AIBQV/shiIjMXGRGnt9zaB0Gg9M39iEvISE6ByMpaDQqV0Md5
|
||||
9y4XJu0rg/aMXLaHOGDAWJsb7nCGDt12cWdgn1Ni2mmXUHv4SJCRXNQF7mSgIr+p
|
||||
Fvd1ljyvzu/iig8qxrcuWoZvY677p3yen4dN8ocgi8Df3KjduGbsTjFAESYqqNQC
|
||||
f+bvypQfhHjxdvz5W3Lpk2swUufqOvhO2b6+cshYJX98qLU8mhai/rOnYkHE7haq
|
||||
WDH6XEthnVGtk2VJ4XFDbz+FID440DPzy5u/1OZw2Mcoyp6y7rZDKC/D0Uvh
|
||||
A1UECgwEVGVzdDENMAsGA1UECwwEVGVzdDENMAsGA1UEAwwEVGVzdDAgFw0yNTAx
|
||||
MDEwMDAwMDBaGA8yMTI1MDEwMTAwMDAwMFowVDELMAkGA1UEBhMCVVMxCzAJBgNV
|
||||
BAgMAkNBMQswCQYDVQQHDAJTRjENMAsGA1UECgwEVGVzdDENMAsGA1UECwwEVGVz
|
||||
dDENMAsGA1UEAwwEVGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
|
||||
AMmEw2bpAoPnrUWkydKGmvDl0bHaCMoSY9yK2b/JL1xaTRwGT4H7DOuO/0Uu6/BV
|
||||
c93UEO0eHf1mt1kkYaRSPyOQLXF2QzRDkiW78c/xvqX+DgmfB7BFNrFSP+CRabdH
|
||||
wvepLUFtXJ6WwvWXjyvDXn8wEAirfETMdU8OXlPaJwS6cbQawYuB6GG5Z1ulxw6k
|
||||
GRi5hnq7PFJBxz1pg6Xx6pwKnaaNemW4Gp2B90St9N5yu7yV6V6XON84ZdohfXSQ
|
||||
livH3UTdkrpe+MO2m3CaAA19zlIxM6OIhkuo8r5GEPxoXCykPbgGMRFWPt0GNC3/
|
||||
AxZofAJg8atqy4peR8XL060CAwEAAaNTMFEwHQYDVR0OBBYEFDULr71QH24KKgNi
|
||||
2fyM6W9qJnzsMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUNQuvvVAfbgoq
|
||||
A2LZ/Izpb2omfOwwDQYJKoZIhvcNAQELBQADggEBAASZENsvvyxygLjE913BBLd2
|
||||
73unxWNSDXakT4xssDEysf//4nGPGo57KjUGFU6M64IVUmhPRTT3aYq7PyA9pb5Q
|
||||
Ijritg5PdxfUMqd+H3CT++JvJmXxmIrFtKD9fZbmBgRg2wU7yvcp7MP+w36CYmqe
|
||||
MH1YF29VcBcF+GfI8k00y83qYHuoHpzPrNcL/Gu6MtcC+1Hy96bs+NYIEFH67dy8
|
||||
IJrN3Vvr4VXSF9qA+Vp5RatPv+hEKZssEFK2fNpGMFPGc1r+HBQ/HEAL4M2betEo
|
||||
Ne4DigJ3CkTIYAd+cZ2m4tdtzbqkeXZJ7SL+/d/5MIXKTnYITw+NrpiMZ6I72Zk=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
Binary file not shown.
@@ -1,34 +1,34 @@
|
||||
Bag Attributes
|
||||
friendlyName: alias
|
||||
localKeyID: 43 4A B0 2D D5 03 52 9F 5B 78 50 64 54 22 AB F7 C8 0B 1F 2B
|
||||
localKeyID: C0 76 69 F4 6E D7 E6 03 D1 EB AD F1 A4 66 C4 14 3A 9B CB D4
|
||||
Key Attributes: <No Attributes>
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIB/3nui1td5QCAggA
|
||||
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBDY04ug+QgB6t2TdOWPgdtIBIIE
|
||||
0IaMRXXtpzLzSjlpyQpLMWLX9Lu+MauINVQMpan8qspC3RGkGcCQUzTkliM3Ls5Q
|
||||
Pwv02iFlKAzUYg/Z5V/kONfDkuxjeZvLFjmzomtWNy6yIxp4ShZinH8AGon16J6E
|
||||
s1+xlQBBLZYrRXX7WCpnHKE2OKquOoFWpYcb23py6FlD7Uq6XB0LEHR+C35tgnTQ
|
||||
WkTFK/La+cbJ+zmWA11Nrnz5XzuWTrNoNB4ygVON78T9o25Hf4V8rWhSZj2N79+B
|
||||
QuCAvuqZyAO12aUI9sxZZyis00JOnX7xbAeOkJk8Hhk4iQRMUUudKb5rqLrh/lcm
|
||||
F9zZjpu6PxJh22ztnRik3L3LyZLdEhMJJGWk4Z/3tKO87K4EiluzwZhAfMLpqfxx
|
||||
qfRKu6By97pbfJFBKqBTzmli2eeJLOwhERlovIaDiublFU8o8RE92PxUPOr7kqL7
|
||||
3cx8Qx5AF2Mnu7ftcLIGgg/lN+haoxpACDkC5ZvTFCrGr7jD1DlkswSMoai9gknx
|
||||
IMjID9nq6pVWyBm+wt9cALeK2wNa5RsE9fFvF/DBathV/WNmBwjnTKCeX3uPP1nw
|
||||
CUE6d+zicrz79kRWRnmscE3phTTu3/O9TokCMe3rLzC0f+gOpIE7vXDSeRuek/xs
|
||||
7uahAAWm94cHdz8QIBR/Ub+fFyrz/VHStAGlZhs0SoVnCl+VnZ9D9OqiyqslOihg
|
||||
LMcNwH8QjEv4zRAU/Sf1OdVJItXyKfII5zSUCW/TpD/vWPlG80Ib/bc+H9uZDZsg
|
||||
OADQYSyWjxA6OUThbCi6Wr+OxFUuDwVaMXxKjz1xH3HjmjpWZeTJy6BAuqe/OLDg
|
||||
VxDdEyL8fgz+QaaM/uqFarVMTir2A5VYNJzTXh02rUn3mXXHbH7uZYSwSg7fJ/hU
|
||||
ycSUkr/TFe9ZfqKOg1+ZKDu7Q97/tkL7gBTQbPqitUSinGvBgtMZKTHBznEn8foq
|
||||
NL/VaFSR4MxTOxFyE2e+9riNJmR0tavZCSgA7LcJtcT9l62cbmwmMj8DvEw8fiSD
|
||||
AYpgwovMtDoVDVQGb7ixLMz8/ta1BB7zPpr2aK8x5pVz5c+9rW/NiWQ68LCpEiAc
|
||||
HxExUVR0b9thC5YvG4VepUtmZ768yTYyus9jDiDNwRH/qttmAosn4pq5gGK+IVao
|
||||
oJX5jcroYaQnvXDBwve2XXXKSkIWe62r8h7Jv6mxR9yBQdVeWNtCGQ5AYNJNxI0i
|
||||
ZbCmCcQJnIuMHLYddaIEmUuUBFOquQC9y/pVbMbmdWOMw5Nama+/q6bke/XGk81I
|
||||
/Ov2gNN4Eu2V9N9MzlF0GiAmk1784qITj9iDIiYXPESnQfybFyhi2DaUM+KmeHpB
|
||||
I2KHL2KA0EGVhBjvCd7FVAqDJL7Dy3nCiLxNiDKChCP9+DDXB2mEfZafltSWai6p
|
||||
FPfGZJImQ6NO4/I/2aeXIwr4urJVFt3mr2b6w+gGRjr4qur0ZcqpvvcA3Es+tMX1
|
||||
eY5Or9V8iw/wj0x+CrHvvsRBfvCTSN/yqweMr5p1xSZm3Hfz906/q8HSaHb/sNne
|
||||
HCjUiKWJ6WTrjDjf9ewYnXb6Qxs3P0zjuHwSrpbq0Pr3HQveQvO5Tfrwr5+ikK1k
|
||||
FyqiU4e4vjpLujkIj2dmH0CkJ6ase1j/rWU8nLr1XZSR
|
||||
MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQnH1/C+tgQtDL2ETF
|
||||
DVH1SQICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEG7VLFdF6M627msk
|
||||
RRRS94wEggTQEOPfMCPRwnTb88nNFAGHr586zkrtG0MUftf4Lgfwns0D5l8qErV2
|
||||
oQZqla9XWqzwc1tM6SyeCbP+86vMBLNl4NXN/F/8j+P2njyahBumx9tym0Fs8KSW
|
||||
P6/GSmBESJWNJ2vT4lGAsuQyPf+iHvd+RAJbhKCtxWHXMY2OK7j2suCaTJSB5Jz1
|
||||
yyPazN/PZSFtDKhMJJRWcQ1pGGsJYaRoJ1v6/05yWtPGGrYGmnDBZ2eKxVm5dncv
|
||||
iYfqaIJ2HXmYZLvmDWy9AkHQSF+mNIMEN8jHXw9l1wGPx3GYtqcRr3r/cPDTZLd6
|
||||
SAjNY/U2YZUBqPqxgFy8sc1kHX6dJAXgBSeR4Rb8GNB8Ry14tMgJRsdsHi1bpMQ/
|
||||
hoqi2mUzYs9I/nz1ncGUB44jtwpN1OgkN9EgQN6i/pN1IJtMkFCnjQ+Ejgi/FRgQ
|
||||
R4fpqDxab2NkFGNE8hWiS0nsjvRyAtnqMwf6+flYAUYumeRbUkkYMelYOQelyJVb
|
||||
OxvfBUr6XBdTVwBR1B5S1MtFtHyw32i6+RCx0S5jRvA7jdX3CVfbTMnk5xLJOrP4
|
||||
7vIckCJaac0NfRQUe812sYWe68LSec3bzz0E4cytyuN7c5u2s1X7i6qs5ITjE7A8
|
||||
1Z2m0m+PDH1XjVvbQpzoLmbv4Spzus1fMQ7bGUjjGJw2PyfT9uD4ukEF12VI+S/n
|
||||
T6ckOkbUha6t5A47KXPpN4VpCnPFvvsJ4ej/ijzVoo5UbZ358tvCBE2D4uu9/TMq
|
||||
hAhWPMnM64JfYRvz96axKy2xgCRGDfYIpTSqBRvCwX3j1MyVKKfjvzIsraHCMb9g
|
||||
+7ELpbBFB8rRSqV/8VRypWSxmSWhLlgTLgH1iPVd7riSzsxcnBAON2iUmgcE0IEV
|
||||
fPcD2uFGTtiNiXu8iZ0xgNZ0nrhquuiUO1hmO/tBquDia7IvyXMHedaugvxdOgu7
|
||||
sZ5YD0DJCGOKTPWvBAF3UZPBJ3kbv2zBl/zEQD5e2wcCo2Flubdwz1/Gf9TGehce
|
||||
TLz0csUdNXjGmu1wpzwBFdBECPUQ7xoLnwc/1K2AiPcktWdLSPjzTkw6ERsYP9NA
|
||||
5w1zi4KmgX2iG78mc/fqHUhppPnL0acLLGFWFKTjYK7mCnPSW5taoRl2EIW+BezK
|
||||
kQYrGz1aONC5ol9e9pmK6YHt7fkHiYqPs/pE44a2tuM80EZsfsz0Mn5RKUgAIOOL
|
||||
cLvK/zmaZ5pf24b8p9vD7kdlFqzEq+H2t5RGuyCGvanS5Z4LL/fDBjcsCh2E3N+i
|
||||
hTsLRPZmKVqeDBIHoyBtSpe5OhzNZTitd6k1JoLFECzHckJflLVEDR7lLvPTI5ko
|
||||
/xxDMxi9InTA62zoSokvFIfN95Rd2tXPqmj14gsZlrKT/3cUNmdva0YmgI2gluS0
|
||||
qT7zozaKHQDDDMzTjhVRheccZOoPuXgQNvnVaXUDBDNyxRSuy3BWnt5YVQRZBzPw
|
||||
HN71h6DxNar/eckRQ03inVn6tGlgwVan5w/JdS7fp1+ET0HF2N93T9f4ZzxHVbEV
|
||||
aam9K+1Vn3hZvL5L06Yq5MjNlIaH/RhMY6zlh5CHR7v+vjYIC02ctbZIrbGL3k2u
|
||||
JKOKDp2QMhTQQ6QQdzoR6BbRgFDGWz8bzOjtVsW2pY3ketp/7/tpfc4=
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
Bag Attributes
|
||||
friendlyName: alias
|
||||
localKeyID: 43 4A B0 2D D5 03 52 9F 5B 78 50 64 54 22 AB F7 C8 0B 1F 2B
|
||||
localKeyID: C0 76 69 F4 6E D7 E6 03 D1 EB AD F1 A4 66 C4 14 3A 9B CB D4
|
||||
Key Attributes: <No Attributes>
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIXl98lJJ1MUsCAggA
|
||||
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBAcT6pXTGm0w+LUzlVH0GpJBIIE
|
||||
0NfOk8+haqEuGskrV8+JJVQLgqpKiOmXBjkiSHGReF4UTocKiUAwrHbvLj+j1VLM
|
||||
TNM/G68+SzGuWxI7gxpzA9u7p4Is5+2Sji9KsMuAh2CQlEuzkFsVaD9KXF2rje7g
|
||||
0G+4+ExZtsjlt/UqG2plFuWzJwji4J82Cy5dir1MQOOAweq5zG5/nzVpMmNoc1lo
|
||||
B9PO18R3SpY6qIp8Q0+d1QJC8zsXi/KKQ3ODiS83x5BL4KkQfjYDK/Lfr9yk5a3t
|
||||
JN8wE5jkDyGCLGGWgwy7Xq5N7m+kvcdeIEqKP9g5k5uZ7LppsDFe9dpHVymTHZGu
|
||||
tGrB74vi4D28YNhuG5qkTjp6CEehSjMwgWEo0Y6ZGu4WQvoTmkne88zly5vUFNrw
|
||||
JFM57YqE8U0Gzy7c/zeGtPq8U7y/Pd4z3muZe9sLpFoFAC7Aoq5yw662mPEBZRVb
|
||||
MDw8fK1OY9fnj9qHwQbYAD5AT9GmpwEP4tWkB6qNiDJBR8Jn3VmQ1uwR7oH+BiwX
|
||||
Y0xWjgl39JcpMORhzJim7K788FEjDrxR1ptepowC4EKjSeq92BGpO+Flf+lY/xYS
|
||||
3QR64h/wJEx7M3FrD7qxSHguW3h8rSMPHQg3YThyBUYsCc1tNpgmhQXNHXlE6G7o
|
||||
vdlDawf0Oybq6KzhdU25/kJyTaM7suiDkwyZf8SIElSD8R2VdYmL2AeowJsi26Qc
|
||||
0f7l/cL/Pws0j4vxYY+6DD5uw+bCBvsjE5Y8Fw6t0xgYwnMCALjfKr2p3CW/Ifa/
|
||||
uynI7Hd548orqkddc834DO6gcPuXMUgZ75RFYglpnD+DDvOzvqh7mrgDiCURZuXd
|
||||
eZkF3sr4Wfn4YsQfM0XdfB0/dmzLnGGIzbW9cuB4VQUswDZ9KCnZVMZOC8AMKvSQ
|
||||
eZn8VEYSr+qT5m8yKSmeUUQga6G/jN6yHj2mV8ura3o1NHvQpy82lHX3M+2d+cs1
|
||||
PWTcYM3AwPpHAM2HyisPYOeNNiEKvo3mtyw2SgV4P6kavdNXFk/xA7mzDWr0QnNX
|
||||
/j4ZZFynhUz46joCC6bew0yyRfL1Jqy+XDvtEOmjhy96nJvUDb5IqsMY5ZHRmGkc
|
||||
yO3uVQu7kexLcA8mYA5OK1llWuyHxffTyGuL5C0q7+8mBvPrkCakUjsLGAgIWYTE
|
||||
ftJ6q8u8xyDghXhRM0lvcoVLjzzjCIDaGVqeXl6HtgJ4grUaNCjESIfsURFylVxk
|
||||
3jNFojsxHPtv+zYAG0otqedSKjZaG0uNivjBt/v21luSs+lqEKbv4122yzC8H6pG
|
||||
zrS6OGkKb8fIqz3D5nAezMFuMjd+ORiGf/IUJToCeluqVGwXMXExdDSCDf0hFJny
|
||||
6y/eKmA88lu6uHYe4TB7ZR2wPyIGl1HPN3xj7Dc/T3wEhCDycKLN4/fY9ZNw5U6E
|
||||
F5yVnZFdcaA6qHiY99xvtOPX/EmxibcV6C84QV3HDmdXgjEIH52I9oK0WEjRb2hd
|
||||
U2lCnZDNqthn3zn0DZ/aSe4HDe5SfLnzFFGyD1wvCTRcM25901Op4kgVD/BPwWH+
|
||||
4E7KiBh91UueWn7m5h1B8cEnpsHwpQLxq2ZdNYzp3ZFyzvzSUXe3QvPveehAgr0M
|
||||
lEXzn1/fJpmRPP5hvt6uYqZ+y90BkiT6UlANFHpoA6x0
|
||||
MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQnH1/C+tgQtDL2ETF
|
||||
DVH1SQICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEG7VLFdF6M627msk
|
||||
RRRS94wEggTQEOPfMCPRwnTb88nNFAGHr586zkrtG0MUftf4Lgfwns0D5l8qErV2
|
||||
oQZqla9XWqzwc1tM6SyeCbP+86vMBLNl4NXN/F/8j+P2njyahBumx9tym0Fs8KSW
|
||||
P6/GSmBESJWNJ2vT4lGAsuQyPf+iHvd+RAJbhKCtxWHXMY2OK7j2suCaTJSB5Jz1
|
||||
yyPazN/PZSFtDKhMJJRWcQ1pGGsJYaRoJ1v6/05yWtPGGrYGmnDBZ2eKxVm5dncv
|
||||
iYfqaIJ2HXmYZLvmDWy9AkHQSF+mNIMEN8jHXw9l1wGPx3GYtqcRr3r/cPDTZLd6
|
||||
SAjNY/U2YZUBqPqxgFy8sc1kHX6dJAXgBSeR4Rb8GNB8Ry14tMgJRsdsHi1bpMQ/
|
||||
hoqi2mUzYs9I/nz1ncGUB44jtwpN1OgkN9EgQN6i/pN1IJtMkFCnjQ+Ejgi/FRgQ
|
||||
R4fpqDxab2NkFGNE8hWiS0nsjvRyAtnqMwf6+flYAUYumeRbUkkYMelYOQelyJVb
|
||||
OxvfBUr6XBdTVwBR1B5S1MtFtHyw32i6+RCx0S5jRvA7jdX3CVfbTMnk5xLJOrP4
|
||||
7vIckCJaac0NfRQUe812sYWe68LSec3bzz0E4cytyuN7c5u2s1X7i6qs5ITjE7A8
|
||||
1Z2m0m+PDH1XjVvbQpzoLmbv4Spzus1fMQ7bGUjjGJw2PyfT9uD4ukEF12VI+S/n
|
||||
T6ckOkbUha6t5A47KXPpN4VpCnPFvvsJ4ej/ijzVoo5UbZ358tvCBE2D4uu9/TMq
|
||||
hAhWPMnM64JfYRvz96axKy2xgCRGDfYIpTSqBRvCwX3j1MyVKKfjvzIsraHCMb9g
|
||||
+7ELpbBFB8rRSqV/8VRypWSxmSWhLlgTLgH1iPVd7riSzsxcnBAON2iUmgcE0IEV
|
||||
fPcD2uFGTtiNiXu8iZ0xgNZ0nrhquuiUO1hmO/tBquDia7IvyXMHedaugvxdOgu7
|
||||
sZ5YD0DJCGOKTPWvBAF3UZPBJ3kbv2zBl/zEQD5e2wcCo2Flubdwz1/Gf9TGehce
|
||||
TLz0csUdNXjGmu1wpzwBFdBECPUQ7xoLnwc/1K2AiPcktWdLSPjzTkw6ERsYP9NA
|
||||
5w1zi4KmgX2iG78mc/fqHUhppPnL0acLLGFWFKTjYK7mCnPSW5taoRl2EIW+BezK
|
||||
kQYrGz1aONC5ol9e9pmK6YHt7fkHiYqPs/pE44a2tuM80EZsfsz0Mn5RKUgAIOOL
|
||||
cLvK/zmaZ5pf24b8p9vD7kdlFqzEq+H2t5RGuyCGvanS5Z4LL/fDBjcsCh2E3N+i
|
||||
hTsLRPZmKVqeDBIHoyBtSpe5OhzNZTitd6k1JoLFECzHckJflLVEDR7lLvPTI5ko
|
||||
/xxDMxi9InTA62zoSokvFIfN95Rd2tXPqmj14gsZlrKT/3cUNmdva0YmgI2gluS0
|
||||
qT7zozaKHQDDDMzTjhVRheccZOoPuXgQNvnVaXUDBDNyxRSuy3BWnt5YVQRZBzPw
|
||||
HN71h6DxNar/eckRQ03inVn6tGlgwVan5w/JdS7fp1+ET0HF2N93T9f4ZzxHVbEV
|
||||
aam9K+1Vn3hZvL5L06Yq5MjNlIaH/RhMY6zlh5CHR7v+vjYIC02ctbZIrbGL3k2u
|
||||
JKOKDp2QMhTQQ6QQdzoR6BbRgFDGWz8bzOjtVsW2pY3ketp/7/tpfc4=
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
|
||||
+139
-67
@@ -24,22 +24,6 @@ import tools.jackson.databind.node.ObjectNode;
|
||||
/**
|
||||
* Outbound calls from a self-hosted instance to its linked SaaS backend (combined-billing "Mode
|
||||
* A").
|
||||
*
|
||||
* <p>Calls:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #register} — relays the admin's short-lived Supabase JWT to {@code POST
|
||||
* /api/v1/account-link/register}; the SaaS side mints + returns a device credential.
|
||||
* <li>{@link #fetchEntitlement} — authenticates with the stored device credential against {@code
|
||||
* GET /api/v1/instance/entitlement}; what the local gate consults.
|
||||
* <li>{@link #reportUsage} — daily usage sync ({@code POST /api/v1/instance/sync}); reports
|
||||
* cumulative units and returns the refreshed entitlement.
|
||||
* <li>{@link #revokeSelf} — self-revokes the credential on local unlink ({@code POST
|
||||
* /api/v1/instance/revoke-self}).
|
||||
* </ul>
|
||||
*
|
||||
* <p>Uses {@code java.net.http.HttpClient} (the established self-hosted outbound pattern; see
|
||||
* {@code AiEngineClient}); base URL + client are injectable so tests can stub SaaS.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -72,13 +56,7 @@ public class AccountLinkClient {
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
/** The device credential a successful {@link #register} returns. */
|
||||
public record RegisterResult(String deviceId, String deviceSecret, Long teamId) {}
|
||||
|
||||
/**
|
||||
* A non-2xx reply from the SaaS account-link API. Carries the upstream status so the caller can
|
||||
* map auth failures (401/403) through rather than masking everything as a 502.
|
||||
*/
|
||||
/** A non-2xx reply from the SaaS account-link API. */
|
||||
public static class UpstreamException extends IOException {
|
||||
private final int status;
|
||||
|
||||
@@ -92,11 +70,7 @@ public class AccountLinkClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authoritative deny (401/403) — the device credential is revoked or invalid. Unlike a
|
||||
* transport/server failure (which returns {@code null} and fails open), the cache must BLOCK on
|
||||
* this. Unchecked so it propagates through {@link #fetchEntitlement}'s transport try/catch.
|
||||
*/
|
||||
/** Authoritative deny (401/403) — the device credential is revoked or invalid. */
|
||||
public static final class RevokedException extends RuntimeException {
|
||||
private final int status;
|
||||
|
||||
@@ -110,46 +84,142 @@ public class AccountLinkClient {
|
||||
}
|
||||
}
|
||||
|
||||
/** What the SaaS side hands back when it records a connect handshake. */
|
||||
public record ConnectRequestResult(
|
||||
String requestId, int expiresInSeconds, String authorizeUrl) {}
|
||||
|
||||
public enum ConnectClaimOutcome {
|
||||
/** Approved and collected; the credential fields are populated. */
|
||||
GRANTED,
|
||||
/** A re-authentication was approved. */
|
||||
CONFIRMED,
|
||||
/** No human decision yet. */
|
||||
PENDING,
|
||||
/** Declined, expired or already used. */
|
||||
REJECTED,
|
||||
/** SaaS unreachable or erroring. */
|
||||
UNAVAILABLE
|
||||
}
|
||||
|
||||
public record ConnectClaimResult(
|
||||
ConnectClaimOutcome outcome, String deviceId, String deviceSecret, Long teamId) {
|
||||
static ConnectClaimResult of(ConnectClaimOutcome outcome) {
|
||||
return new ConnectClaimResult(outcome, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens a connect handshake. */
|
||||
public ConnectRequestResult connectRequest(
|
||||
String name, String callbackUrl, String nonce, String claimSecret) throws IOException {
|
||||
return connectRequest(name, callbackUrl, nonce, claimSecret, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relays the admin Supabase JWT to the SaaS register endpoint and returns the minted
|
||||
* credential.
|
||||
*
|
||||
* @throws IOException on transport failure or a non-2xx response (caller surfaces to the
|
||||
* admin).
|
||||
* As {@link #connectRequest}, but presenting an existing device credential so the SaaS side
|
||||
* treats this as a re-authentication and pins the handshake to the team we already belong to.
|
||||
*/
|
||||
public RegisterResult register(String supabaseJwt, String instanceName) throws IOException {
|
||||
String body =
|
||||
instanceName == null || instanceName.isBlank()
|
||||
? "{}"
|
||||
: "{\"name\":" + mapper.writeValueAsString(instanceName) + "}";
|
||||
HttpRequest request =
|
||||
public ConnectRequestResult connectRequest(
|
||||
String name,
|
||||
String callbackUrl,
|
||||
String nonce,
|
||||
String claimSecret,
|
||||
DeviceCredential credential)
|
||||
throws IOException {
|
||||
ObjectNode root = mapper.createObjectNode();
|
||||
if (name != null && !name.isBlank()) {
|
||||
root.put("name", name);
|
||||
}
|
||||
root.put("callbackUrl", callbackUrl);
|
||||
root.put("nonce", nonce);
|
||||
root.put("claimSecret", claimSecret);
|
||||
|
||||
HttpRequest.Builder builder =
|
||||
HttpRequest.newBuilder()
|
||||
.uri(uri("/api/v1/account-link/register"))
|
||||
.header("Authorization", "Bearer " + supabaseJwt)
|
||||
.uri(uri("/api/v1/account-link/connect/request"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.timeout(timeout())
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||
.build();
|
||||
.POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(root)));
|
||||
if (credential != null) {
|
||||
builder.header(HEADER_DEVICE_ID, credential.getDeviceId())
|
||||
.header(HEADER_DEVICE_SECRET, credential.getDeviceSecret());
|
||||
}
|
||||
|
||||
HttpResponse<String> response = send(request);
|
||||
HttpResponse<String> response = send(builder.build());
|
||||
if (response.statusCode() / 100 != 2) {
|
||||
throw new UpstreamException(response.statusCode(), response.body());
|
||||
}
|
||||
JsonNode root = mapper.readTree(response.body());
|
||||
String deviceId = text(root, "deviceId");
|
||||
String deviceSecret = text(root, "deviceSecret");
|
||||
if (deviceId == null || deviceSecret == null) {
|
||||
throw new IOException("SaaS register response missing deviceId/deviceSecret");
|
||||
JsonNode body = mapper.readTree(response.body());
|
||||
String requestId = text(body, "requestId");
|
||||
if (requestId == null) {
|
||||
throw new IOException("SaaS connect response missing requestId");
|
||||
}
|
||||
String authorizeUrl = text(body, "authorizeUrl");
|
||||
if (authorizeUrl == null || !isAbsoluteHttpUrl(authorizeUrl)) {
|
||||
throw new IOException("SaaS connect response carried no usable authorizeUrl");
|
||||
}
|
||||
return new ConnectRequestResult(requestId, body.path("expiresIn").asInt(0), authorizeUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the device credential for an approved handshake, proving possession of the claim
|
||||
* secret.
|
||||
*/
|
||||
public ConnectClaimResult connectClaim(String requestId, String claimSecret) {
|
||||
HttpResponse<String> response;
|
||||
try {
|
||||
ObjectNode root = mapper.createObjectNode();
|
||||
root.put("requestId", requestId);
|
||||
root.put("claimSecret", claimSecret);
|
||||
HttpRequest request =
|
||||
HttpRequest.newBuilder()
|
||||
.uri(uri("/api/v1/account-link/connect/claim"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.timeout(timeout())
|
||||
.POST(
|
||||
HttpRequest.BodyPublishers.ofString(
|
||||
mapper.writeValueAsString(root)))
|
||||
.build();
|
||||
response = send(request);
|
||||
} catch (Exception e) {
|
||||
log.debug("Connect claim failed (transport): {}", e.getMessage());
|
||||
return ConnectClaimResult.of(ConnectClaimOutcome.UNAVAILABLE);
|
||||
}
|
||||
int status = response.statusCode();
|
||||
if (status == 202) {
|
||||
return ConnectClaimResult.of(ConnectClaimOutcome.PENDING);
|
||||
}
|
||||
if (status >= 500 && status <= 599) {
|
||||
return ConnectClaimResult.of(ConnectClaimOutcome.UNAVAILABLE);
|
||||
}
|
||||
if (status < 200 || status > 299) {
|
||||
return ConnectClaimResult.of(ConnectClaimOutcome.REJECTED);
|
||||
}
|
||||
try {
|
||||
JsonNode body = mapper.readTree(response.body());
|
||||
Long teamId = body.hasNonNull("teamId") ? body.get("teamId").asLong() : null;
|
||||
// A re-authentication says so explicitly and carries no credential, so an absent
|
||||
// credential is only an error when we were expecting one.
|
||||
if ("confirmed".equals(text(body, "status"))) {
|
||||
return new ConnectClaimResult(ConnectClaimOutcome.CONFIRMED, null, null, teamId);
|
||||
}
|
||||
String deviceId = text(body, "deviceId");
|
||||
String deviceSecret = text(body, "deviceSecret");
|
||||
if (deviceId == null || deviceSecret == null) {
|
||||
log.warn("Connect claim succeeded but the reply carried no credential");
|
||||
return ConnectClaimResult.of(ConnectClaimOutcome.REJECTED);
|
||||
}
|
||||
return new ConnectClaimResult(
|
||||
ConnectClaimOutcome.GRANTED, deviceId, deviceSecret, teamId);
|
||||
} catch (RuntimeException e) {
|
||||
log.debug("Connect claim parse failed: {}", e.getMessage());
|
||||
return ConnectClaimResult.of(ConnectClaimOutcome.REJECTED);
|
||||
}
|
||||
Long teamId = root.hasNonNull("teamId") ? root.get("teamId").asLong() : null;
|
||||
return new RegisterResult(deviceId, deviceSecret, teamId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes this instance's own credential on the SaaS side, authenticated by that credential.
|
||||
* Best-effort: returns {@code false} if SaaS is unreachable or rejects, so the caller (local
|
||||
* unlink) can still clear locally and log the orphan for follow-up. Idempotent on SaaS.
|
||||
*/
|
||||
public boolean revokeSelf(String deviceId, String deviceSecret) {
|
||||
try {
|
||||
@@ -174,17 +244,7 @@ public class AccountLinkClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the current entitlement using the stored device credential. Three outcomes:
|
||||
*
|
||||
* <ul>
|
||||
* <li>2xx → the parsed snapshot.
|
||||
* <li>401/403 → {@link RevokedException} (authoritative deny — revoked/invalid credential);
|
||||
* the caller must BLOCK, not fail open.
|
||||
* <li>transport failure, other non-2xx (e.g. 5xx), or a malformed body → {@code null}
|
||||
* ("unknown" — the caller fails open).
|
||||
* </ul>
|
||||
*/
|
||||
/** Fetches the current entitlement using the stored device credential. */
|
||||
public InstanceEntitlement fetchEntitlement(String deviceId, String deviceSecret) {
|
||||
HttpResponse<String> response;
|
||||
try {
|
||||
@@ -224,9 +284,6 @@ public class AccountLinkClient {
|
||||
/**
|
||||
* Reports the period's cumulative per-category units to {@code POST /api/v1/instance/sync} and
|
||||
* returns the fresh entitlement in the same reply — one round-trip both reports and refreshes.
|
||||
* SaaS bills the delta against its last-seen cumulative, so resending the same totals is
|
||||
* idempotent. Same three outcomes as {@link #fetchEntitlement}; on {@code null} the caller must
|
||||
* not advance its last-synced markers so the usage retries next sync.
|
||||
*/
|
||||
public InstanceEntitlement reportUsage(
|
||||
String deviceId,
|
||||
@@ -360,4 +417,19 @@ public class AccountLinkClient {
|
||||
private static String text(JsonNode node, String field) {
|
||||
return node.hasNonNull(field) ? node.get(field).asText() : null;
|
||||
}
|
||||
|
||||
/** Absolute http(s) with a host. */
|
||||
static boolean isAbsoluteHttpUrl(String candidate) {
|
||||
try {
|
||||
URI uri = URI.create(candidate.strip());
|
||||
String scheme = uri.getScheme();
|
||||
return uri.isAbsolute()
|
||||
&& scheme != null
|
||||
&& ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme))
|
||||
&& uri.getHost() != null
|
||||
&& !uri.getHost().isBlank();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+88
-44
@@ -16,21 +16,11 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Same-origin account-link surface on the self-hosted instance (combined-billing "Mode A").
|
||||
*
|
||||
* <p>The portal (served from this same origin, admin authenticated by the existing self-hosted
|
||||
* security chain) calls these. {@code POST /link} relays the admin's Supabase JWT to the SaaS
|
||||
* backend, which mints + returns a device credential we store locally. {@code GET /status} backs
|
||||
* the portal's link card; {@code GET /usage} exposes locally-accrued unsynced usage the portal adds
|
||||
* to SaaS-synced spend; {@code POST /sync-now} forces an immediate usage sync (ops "reconcile now"
|
||||
* / test aid).
|
||||
*
|
||||
* <p>Admin-only, {@code @Profile("!saas")}, gated behind {@code
|
||||
* stirling.billing.account-link.enabled} — off → bean absent → 404.
|
||||
*/
|
||||
/** Same-origin account-link surface on the self-hosted instance (combined billing). */
|
||||
@Slf4j
|
||||
@Hidden
|
||||
@RestController
|
||||
@@ -41,51 +31,110 @@ import lombok.extern.slf4j.Slf4j;
|
||||
public class AccountLinkController {
|
||||
|
||||
private final AccountLinkService service;
|
||||
private final ConnectService connectService;
|
||||
private final LocalUsageService localUsageService;
|
||||
// Present only when metering is on (its own flag); absent → /sync-now reports 409.
|
||||
private final ObjectProvider<UsageSyncService> syncServiceProvider;
|
||||
|
||||
public AccountLinkController(
|
||||
AccountLinkService service,
|
||||
ConnectService connectService,
|
||||
LocalUsageService localUsageService,
|
||||
ObjectProvider<UsageSyncService> syncServiceProvider) {
|
||||
this.service = service;
|
||||
this.connectService = connectService;
|
||||
this.localUsageService = localUsageService;
|
||||
this.syncServiceProvider = syncServiceProvider;
|
||||
}
|
||||
|
||||
/** {@code supabaseJwt} is the admin's short-lived token the portal already holds. */
|
||||
public record LinkRequest(String supabaseJwt, String name) {}
|
||||
/** {@code callbackUrl} is the portal telling us where its own callback route lives. */
|
||||
public record ConnectStartRequest(String name, String callbackUrl) {}
|
||||
|
||||
@PostMapping("/link")
|
||||
public ResponseEntity<?> link(@RequestBody LinkRequest req) {
|
||||
if (req == null || req.supabaseJwt() == null || req.supabaseJwt().isBlank()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(java.util.Map.of("error", "supabaseJwt is required"));
|
||||
}
|
||||
/** {@code nonce} comes from the callback fragment the approval page redirected to. */
|
||||
public record ConnectCompleteRequest(String nonce) {}
|
||||
|
||||
/**
|
||||
* Opens a browser-mediated link handshake and returns the approval URL to send the admin to.
|
||||
*/
|
||||
@PostMapping("/connect/start")
|
||||
public ResponseEntity<?> connectStart(
|
||||
@RequestBody(required = false) ConnectStartRequest req, HttpServletRequest http) {
|
||||
try {
|
||||
return ResponseEntity.ok(service.link(req.supabaseJwt(), req.name()));
|
||||
return ResponseEntity.ok(
|
||||
connectService.start(req != null ? req.name() : null, callbackHint(req, http)));
|
||||
} catch (AccountLinkClient.UpstreamException e) {
|
||||
// Auth failures are the admin's token, not a gateway fault: surface 401/403 as-is so
|
||||
// the portal can prompt a re-sign-in. Anything else upstream → 502. Don't echo the
|
||||
// raw upstream body back to the browser.
|
||||
HttpStatus status =
|
||||
e.status() == HttpStatus.UNAUTHORIZED.value()
|
||||
|| e.status() == HttpStatus.FORBIDDEN.value()
|
||||
? HttpStatus.valueOf(e.status())
|
||||
: HttpStatus.BAD_GATEWAY;
|
||||
log.warn("Account-link register rejected upstream: HTTP {}", e.status());
|
||||
return ResponseEntity.status(status).body(java.util.Map.of("error", "LINK_FAILED"));
|
||||
} catch (IOException e) {
|
||||
// Don't echo e.getMessage() to the browser: a DNS/connection/TLS failure can carry the
|
||||
// configured SaaS host/IP. Log it server-side; return the same opaque body the
|
||||
// UpstreamException branch does.
|
||||
log.warn("Account-link failed (transport): {}", e.getMessage());
|
||||
log.warn("Account-link connect rejected upstream: HTTP {}", e.status());
|
||||
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
|
||||
.body(java.util.Map.of("error", "LINK_FAILED"));
|
||||
.body(java.util.Map.of("error", "CONNECT_FAILED"));
|
||||
} catch (IOException e) {
|
||||
// Same reasoning as /link: a transport message can carry the configured SaaS host.
|
||||
log.warn("Account-link connect failed (transport): {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
|
||||
.body(java.util.Map.of("error", "CONNECT_FAILED"));
|
||||
}
|
||||
}
|
||||
|
||||
/** Re-establishes the admin's SaaS session for a server that is already linked. */
|
||||
@PostMapping("/connect/reauth")
|
||||
public ResponseEntity<?> connectReauth(
|
||||
@RequestBody(required = false) ConnectStartRequest req, HttpServletRequest http) {
|
||||
try {
|
||||
return ResponseEntity.ok(connectService.startReauth(callbackHint(req, http)));
|
||||
} catch (AccountLinkClient.UpstreamException e) {
|
||||
log.warn("Account-link reauth rejected upstream: HTTP {}", e.status());
|
||||
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
|
||||
.body(java.util.Map.of("error", "CONNECT_FAILED"));
|
||||
} catch (IOException e) {
|
||||
log.warn("Account-link reauth failed: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
|
||||
.body(java.util.Map.of("error", "CONNECT_FAILED"));
|
||||
}
|
||||
}
|
||||
|
||||
/** Called by the callback page with the nonce it found in the fragment. */
|
||||
@PostMapping("/connect/complete")
|
||||
public ResponseEntity<ConnectService.ConnectStatus> connectComplete(
|
||||
@RequestBody(required = false) ConnectCompleteRequest req) {
|
||||
return ResponseEntity.ok(connectService.complete(req != null ? req.nonce() : null));
|
||||
}
|
||||
|
||||
/** Everything we know about where the admin's browser is, for the callback. */
|
||||
private static ConnectService.CallbackHint callbackHint(
|
||||
ConnectStartRequest req, HttpServletRequest http) {
|
||||
return new ConnectService.CallbackHint(
|
||||
req != null ? req.callbackUrl() : null, http.getHeader("Origin"), baseUrlOf(http));
|
||||
}
|
||||
|
||||
/**
|
||||
* This instance's base URL as the browser reached it, including any context path so a subpath
|
||||
* deployment builds a callback that actually resolves.
|
||||
*/
|
||||
private static String baseUrlOf(HttpServletRequest request) {
|
||||
String forwardedProto = firstHop(request.getHeader("X-Forwarded-Proto"));
|
||||
String forwardedHost = firstHop(request.getHeader("X-Forwarded-Host"));
|
||||
String scheme = forwardedProto != null ? forwardedProto : request.getScheme();
|
||||
String hostPort;
|
||||
if (forwardedHost != null) {
|
||||
hostPort = forwardedHost;
|
||||
} else {
|
||||
int port = request.getServerPort();
|
||||
boolean defaultPort =
|
||||
("http".equals(scheme) && port == 80)
|
||||
|| ("https".equals(scheme) && port == 443);
|
||||
hostPort = defaultPort ? request.getServerName() : request.getServerName() + ":" + port;
|
||||
}
|
||||
String context = request.getContextPath() == null ? "" : request.getContextPath();
|
||||
return scheme + "://" + hostPort + context;
|
||||
}
|
||||
|
||||
private static String firstHop(String headerValue) {
|
||||
if (headerValue == null || headerValue.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String first = headerValue.split(",")[0].strip();
|
||||
return first.isEmpty() ? null : first;
|
||||
}
|
||||
|
||||
@GetMapping("/status")
|
||||
public ResponseEntity<AccountLinkService.LinkStatus> status() {
|
||||
return ResponseEntity.ok(service.status());
|
||||
@@ -106,12 +155,7 @@ public class AccountLinkController {
|
||||
return ResponseEntity.ok(localUsageService.currentPeriodUnsynced());
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces an immediate usage sync to SaaS — the same work the daily scheduler does. An admin
|
||||
* "reconcile now" action (and a test aid so you don't wait on the scheduler). Idempotent:
|
||||
* re-reports the current cumulative, so a repeat trigger bills nothing. {@code 204} once run;
|
||||
* {@code 409} when metering is off (the sync bean is absent).
|
||||
*/
|
||||
/** Forces an immediate usage sync to SaaS — the same work the daily scheduler does. */
|
||||
@PostMapping("/sync-now")
|
||||
public ResponseEntity<Void> syncNow() {
|
||||
UsageSyncService sync = syncServiceProvider.getIfAvailable();
|
||||
|
||||
+8
-27
@@ -8,29 +8,17 @@ import org.springframework.stereotype.Component;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Self-hosted side of combined-billing "Mode A" (connected self-hosted).
|
||||
*
|
||||
* <p>Binds the {@code stirling.billing.account-link.*} keys. {@link #enabled} mirrors the same flag
|
||||
* the gated beans test with {@code @ConditionalOnProperty}; it is kept here only so non-conditional
|
||||
* code (e.g. the gate's flag-off short-circuit, exposed status) can read it. The whole feature is
|
||||
* <b>off by default</b> and <b>dark</b> — when off nothing gates and the link endpoints 404.
|
||||
*/
|
||||
/** Self-hosted side of combined billing: this instance bills through a linked SaaS team. */
|
||||
@Getter
|
||||
@Setter
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "stirling.billing.account-link")
|
||||
public class AccountLinkProperties {
|
||||
|
||||
/** Master switch. When {@code false} (default) the feature is fully inert. */
|
||||
/** Master switch. */
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
* Base URL of the SaaS backend this instance links to (register + entitlement live there).
|
||||
*
|
||||
* <p>STUB: defaults to the public cloud host; an operator overrides it for staging. There is no
|
||||
* existing SaaS-base-url property in the self-hosted profile, so this is introduced here.
|
||||
*/
|
||||
/** Base URL of the SaaS backend this instance links to (register + entitlement live there). */
|
||||
private String saasBaseUrl = "https://stirling.com/app";
|
||||
|
||||
/** Cached entitlement is reused for this long before a refresh is attempted. */
|
||||
@@ -39,20 +27,18 @@ public class AccountLinkProperties {
|
||||
/** Connect/read timeout for the outbound SaaS calls. */
|
||||
private int requestTimeoutSeconds = 10;
|
||||
|
||||
/** Phase 2 usage metering + daily sync. Keyed under {@code …account-link.metering.*}. */
|
||||
/** Phase 2 usage metering + daily sync. */
|
||||
private final Metering metering = new Metering();
|
||||
|
||||
/**
|
||||
* Dedicated billing switch, <b>separate</b> from {@link #enabled} so the link plumbing can be
|
||||
* enabled (e.g. to test linking) without ever turning on real usage metering, reporting, or cap
|
||||
* enforcement. Both default off; metering requires the master flag too. This is the production
|
||||
* safety key — flipping it on is what actually bills linked instances.
|
||||
* Separate from {@link #enabled} so linking can be exercised without billing anything. Both
|
||||
* default off, and metering needs the master flag as well.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public static class Metering {
|
||||
|
||||
/** Turns on usage metering, the daily sync, and cap enforcement. Default off. */
|
||||
/** Turns on usage metering, the daily sync, and cap enforcement. */
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
@@ -65,12 +51,7 @@ public class AccountLinkProperties {
|
||||
*/
|
||||
private int graceDays = 3;
|
||||
|
||||
/**
|
||||
* Dedup window for identical input sets. A re-run of the same inputs within this window is
|
||||
* treated as workflow chaining and not re-charged; the same inputs run again after it are
|
||||
* billed afresh. Mirrors the cloud's {@code payg.lineage.workflow-window} so the same op
|
||||
* costs the same on the instance and in the cloud.
|
||||
*/
|
||||
/** Dedup window for identical input sets. */
|
||||
private Duration workflowWindow = Duration.ofMinutes(5);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-24
@@ -1,6 +1,5 @@
|
||||
package stirling.software.proprietary.accountlink;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@@ -9,13 +8,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Linking orchestrator (self-hosted side of combined-billing "Mode A").
|
||||
*
|
||||
* <p>{@link #link} is the same-origin action the portal triggers: it relays the admin's Supabase
|
||||
* JWT to the SaaS register endpoint, then persists the returned device credential secure-at-rest.
|
||||
* The credential — not the JWT — authenticates all later unattended entitlement calls.
|
||||
*/
|
||||
/** Linking orchestrator (self-hosted side of combined billing). */
|
||||
@Slf4j
|
||||
@Service
|
||||
@Profile("!saas")
|
||||
@@ -38,24 +31,9 @@ public class AccountLinkService {
|
||||
/** Status of this instance's link, for the portal's "Account link" card. */
|
||||
public record LinkStatus(boolean linked, String deviceId, Long teamId, String linkedAt) {}
|
||||
|
||||
/**
|
||||
* Registers this instance with the SaaS team behind {@code supabaseJwt} and stores the
|
||||
* credential.
|
||||
*
|
||||
* @throws IOException if the SaaS register call fails (surfaced to the admin as a link error).
|
||||
*/
|
||||
public LinkStatus link(String supabaseJwt, String instanceName) throws IOException {
|
||||
AccountLinkClient.RegisterResult result = client.register(supabaseJwt, instanceName);
|
||||
credentialStore.save(result.deviceId(), result.deviceSecret(), result.teamId());
|
||||
entitlementCache.invalidate();
|
||||
log.info("Account-link: instance linked to team {}", result.teamId());
|
||||
return status();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlinks this instance — best-effort tells SaaS to revoke first (so the row gets {@code
|
||||
* revoked_at} set), then clears locally regardless. If SaaS is unreachable the local clear
|
||||
* still proceeds (admin's intent must win); the orphan row can be revoked from the portal.
|
||||
* revoked_at} set), then clears locally regardless.
|
||||
*/
|
||||
public void unlink() {
|
||||
credentialStore
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Singleton row holding this instance's daily-sync bookkeeping (combined-billing "Mode A").
|
||||
* Singleton row holding this instance's daily-sync bookkeeping (combined billing).
|
||||
*
|
||||
* <p>{@link #lastSyncSeq} is reserved (incremented + persisted) <em>before</em> each report so it
|
||||
* is strictly monotonic across restarts and partial failures — SaaS dedups replays by comparing it,
|
||||
|
||||
+1
-1
@@ -2,5 +2,5 @@ package stirling.software.proprietary.accountlink;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
/** Persistence for the singleton {@link AccountLinkSyncState} (combined-billing "Mode A"). */
|
||||
/** Persistence for the singleton {@link AccountLinkSyncState} (combined billing). */
|
||||
public interface AccountLinkSyncStateRepository extends JpaRepository<AccountLinkSyncState, Long> {}
|
||||
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
package stirling.software.proprietary.accountlink;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
/** Browser-mediated account linking, instance side. */
|
||||
@Slf4j
|
||||
@Service
|
||||
@Profile("!saas")
|
||||
@ConditionalOnProperty(name = "stirling.billing.account-link.enabled", havingValue = "true")
|
||||
public class ConnectService {
|
||||
|
||||
/** Frontend route that consumes the callback fragment. */
|
||||
static final String CALLBACK_PATH = "/account-link/callback";
|
||||
|
||||
private static final int SECRET_BYTES = 32;
|
||||
|
||||
private final AccountLinkClient client;
|
||||
private final ConnectStateRepository stateRepo;
|
||||
private final DeviceCredentialStore credentialStore;
|
||||
private final EntitlementCache entitlementCache;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final SecureRandom random = new SecureRandom();
|
||||
|
||||
public ConnectService(
|
||||
AccountLinkClient client,
|
||||
ConnectStateRepository stateRepo,
|
||||
DeviceCredentialStore credentialStore,
|
||||
EntitlementCache entitlementCache,
|
||||
ApplicationProperties applicationProperties) {
|
||||
this.client = client;
|
||||
this.stateRepo = stateRepo;
|
||||
this.credentialStore = credentialStore;
|
||||
this.entitlementCache = entitlementCache;
|
||||
this.applicationProperties = applicationProperties;
|
||||
}
|
||||
|
||||
public enum Phase {
|
||||
/** Nothing in flight and not linked. */
|
||||
NONE,
|
||||
/** A handshake is open, waiting for a leader to approve it on the SaaS site. */
|
||||
PENDING,
|
||||
/** Linked. */
|
||||
LINKED,
|
||||
/** The handshake outlived its window; start a new one. */
|
||||
EXPIRED,
|
||||
/** Declined or already used; start a new one. */
|
||||
REJECTED,
|
||||
/** SaaS could not be reached; the handshake is still valid and can be retried. */
|
||||
UNAVAILABLE
|
||||
}
|
||||
|
||||
/** What the portal renders. */
|
||||
public record ConnectStatus(
|
||||
Phase phase, String authorizeUrl, Long secondsRemaining, Long teamId) {
|
||||
static ConnectStatus of(Phase phase) {
|
||||
return new ConnectStatus(phase, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/** Everything we know about where the admin's browser actually is, in decreasing authority. */
|
||||
public record CallbackHint(
|
||||
String requestedCallbackUrl, String browserOrigin, String derivedBaseUrl) {}
|
||||
|
||||
/** Opens a handshake and returns where to send the admin. */
|
||||
@Transactional
|
||||
public ConnectStatus start(String name, CallbackHint hint) throws IOException {
|
||||
if (credentialStore.isLinked()) {
|
||||
return status();
|
||||
}
|
||||
return open(name, hint, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a handshake that only re-establishes the admin's browser session, for an instance that
|
||||
* is already linked.
|
||||
*/
|
||||
@Transactional
|
||||
public ConnectStatus startReauth(CallbackHint hint) throws IOException {
|
||||
DeviceCredential credential =
|
||||
credentialStore
|
||||
.get()
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new IOException(
|
||||
"This server is not linked, so there is no session"
|
||||
+ " to re-establish"));
|
||||
return open(credential.getDeviceId(), hint, credential);
|
||||
}
|
||||
|
||||
private ConnectStatus open(String name, CallbackHint hint, DeviceCredential credential)
|
||||
throws IOException {
|
||||
String callbackUrl = resolveCallbackUrl(hint);
|
||||
if (callbackUrl == null) {
|
||||
throw new IOException(
|
||||
"Cannot determine where to send the admin back to; set system.frontendUrl");
|
||||
}
|
||||
String nonce = randomSecret();
|
||||
String claimSecret = randomSecret();
|
||||
|
||||
AccountLinkClient.ConnectRequestResult created =
|
||||
client.connectRequest(name, callbackUrl, nonce, claimSecret, credential);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
ConnectState state = new ConnectState();
|
||||
state.setId(ConnectState.SINGLETON_ID);
|
||||
state.setRequestId(created.requestId());
|
||||
state.setNonce(nonce);
|
||||
state.setClaimSecret(claimSecret);
|
||||
state.setCallbackUrl(callbackUrl);
|
||||
state.setAuthorizeUrl(created.authorizeUrl());
|
||||
state.setCreatedAt(now);
|
||||
state.setExpiresAt(
|
||||
now.plusSeconds(created.expiresInSeconds() > 0 ? created.expiresInSeconds() : 900));
|
||||
stateRepo.save(state);
|
||||
|
||||
log.info("Account-link connect: handshake {} opened", created.requestId());
|
||||
return pendingStatus(state, now);
|
||||
}
|
||||
|
||||
/** Finishes a handshake from the callback the approval page redirected to. */
|
||||
@Transactional
|
||||
public ConnectStatus complete(String nonce) {
|
||||
Optional<ConnectState> found = stateRepo.findById(ConnectState.SINGLETON_ID);
|
||||
if (found.isEmpty()) {
|
||||
// Already finished (a double-submitted callback) or never started.
|
||||
return status();
|
||||
}
|
||||
ConnectState state = found.get();
|
||||
if (state.isExpired(LocalDateTime.now())) {
|
||||
stateRepo.delete(state);
|
||||
return ConnectStatus.of(Phase.EXPIRED);
|
||||
}
|
||||
if (nonce == null || !nonceMatches(nonce, state.getNonce())) {
|
||||
log.warn(
|
||||
"Account-link connect: callback for handshake {} had a bad nonce",
|
||||
state.getRequestId());
|
||||
return ConnectStatus.of(Phase.REJECTED);
|
||||
}
|
||||
|
||||
AccountLinkClient.ConnectClaimResult claim =
|
||||
client.connectClaim(state.getRequestId(), state.getClaimSecret());
|
||||
return switch (claim.outcome()) {
|
||||
case GRANTED -> {
|
||||
credentialStore.save(claim.deviceId(), claim.deviceSecret(), claim.teamId());
|
||||
entitlementCache.invalidate();
|
||||
stateRepo.delete(state);
|
||||
log.info("Account-link connect: linked to team {}", claim.teamId());
|
||||
yield new ConnectStatus(Phase.LINKED, null, null, claim.teamId());
|
||||
}
|
||||
case CONFIRMED -> {
|
||||
stateRepo.delete(state);
|
||||
log.info(
|
||||
"Account-link connect: session re-established for team {}", claim.teamId());
|
||||
yield new ConnectStatus(Phase.LINKED, null, null, claim.teamId());
|
||||
}
|
||||
case PENDING ->
|
||||
// The admin reached the callback before the approval committed. The row stays,
|
||||
// so a retry finishes it.
|
||||
ConnectStatus.of(Phase.PENDING);
|
||||
case REJECTED -> {
|
||||
stateRepo.delete(state);
|
||||
yield ConnectStatus.of(Phase.REJECTED);
|
||||
}
|
||||
case UNAVAILABLE -> ConnectStatus.of(Phase.UNAVAILABLE);
|
||||
};
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ConnectStatus status() {
|
||||
Optional<DeviceCredential> credential = credentialStore.get();
|
||||
if (credential.isPresent()) {
|
||||
return new ConnectStatus(Phase.LINKED, null, null, credential.get().getTeamId());
|
||||
}
|
||||
Optional<ConnectState> state = stateRepo.findById(ConnectState.SINGLETON_ID);
|
||||
if (state.isEmpty()) {
|
||||
return ConnectStatus.of(Phase.NONE);
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (state.get().isExpired(now)) {
|
||||
return ConnectStatus.of(Phase.EXPIRED);
|
||||
}
|
||||
return pendingStatus(state.get(), now);
|
||||
}
|
||||
|
||||
private static ConnectStatus pendingStatus(ConnectState state, LocalDateTime now) {
|
||||
long remaining = Duration.between(now, state.getExpiresAt()).toSeconds();
|
||||
return new ConnectStatus(
|
||||
Phase.PENDING, state.getAuthorizeUrl(), Math.max(remaining, 0), null);
|
||||
}
|
||||
|
||||
/** Decides the callback, preferring knowledge over inference. */
|
||||
String resolveCallbackUrl(CallbackHint hint) {
|
||||
String configured = applicationProperties.getSystem().getFrontendUrl();
|
||||
if (configured != null && !configured.isBlank()) {
|
||||
return trimTrailingSlash(configured.strip()) + CALLBACK_PATH;
|
||||
}
|
||||
String browserOrigin = originOf(hint.browserOrigin());
|
||||
if (browserOrigin != null) {
|
||||
String requested = hint.requestedCallbackUrl();
|
||||
if (requested != null && browserOrigin.equals(originOf(requested))) {
|
||||
return requested.strip();
|
||||
}
|
||||
return browserOrigin + CALLBACK_PATH;
|
||||
}
|
||||
return hint.derivedBaseUrl() == null || hint.derivedBaseUrl().isBlank()
|
||||
? null
|
||||
: trimTrailingSlash(hint.derivedBaseUrl().strip()) + CALLBACK_PATH;
|
||||
}
|
||||
|
||||
/** Scheme, host and port of an absolute http(s) URL; null if it is not one. */
|
||||
private static String originOf(String candidate) {
|
||||
if (candidate == null || candidate.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(candidate.strip());
|
||||
} catch (URISyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
if (uri.getScheme() == null || uri.getHost() == null) {
|
||||
return null;
|
||||
}
|
||||
String scheme = uri.getScheme().toLowerCase(Locale.ROOT);
|
||||
if (!"http".equals(scheme) && !"https".equals(scheme)) {
|
||||
return null;
|
||||
}
|
||||
int port = uri.getPort();
|
||||
boolean defaultPort =
|
||||
port == -1
|
||||
|| ("http".equals(scheme) && port == 80)
|
||||
|| ("https".equals(scheme) && port == 443);
|
||||
return defaultPort
|
||||
? scheme + "://" + uri.getHost()
|
||||
: scheme + "://" + uri.getHost() + ":" + port;
|
||||
}
|
||||
|
||||
private static String trimTrailingSlash(String value) {
|
||||
return value.replaceAll("/+$", "");
|
||||
}
|
||||
|
||||
private String randomSecret() {
|
||||
byte[] buf = new byte[SECRET_BYTES];
|
||||
random.nextBytes(buf);
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||
}
|
||||
|
||||
/** Constant-time so a caller cannot probe the nonce a character at a time. */
|
||||
private static boolean nonceMatches(String candidate, String expected) {
|
||||
if (expected == null) {
|
||||
return false;
|
||||
}
|
||||
return MessageDigest.isEqual(
|
||||
candidate.getBytes(StandardCharsets.UTF_8),
|
||||
expected.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package stirling.software.proprietary.accountlink;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/** The one in-flight "connect this server" handshake, instance side. */
|
||||
@Entity
|
||||
@Table(name = "account_link_connect_state")
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class ConnectState implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final Long SINGLETON_ID = 1L;
|
||||
|
||||
@Id
|
||||
@Column(name = "id")
|
||||
private Long id = SINGLETON_ID;
|
||||
|
||||
/** Opaque handle the SaaS side gave us; identifies the handshake on both sides. */
|
||||
@Column(name = "request_id", nullable = false, length = 64)
|
||||
private String requestId;
|
||||
|
||||
/** Correlator we minted. */
|
||||
@Column(name = "nonce", nullable = false, length = 128)
|
||||
private String nonce;
|
||||
|
||||
/** Secret we minted and sent to SaaS server to server. */
|
||||
@Column(name = "claim_secret", nullable = false, length = 128)
|
||||
private String claimSecret;
|
||||
|
||||
/** Where we asked the approval page to send the admin back to. */
|
||||
@Column(name = "callback_url", nullable = false, length = 2048)
|
||||
private String callbackUrl;
|
||||
|
||||
/** The approval URL handed to the browser, so a reload can offer it again. */
|
||||
@Column(name = "authorize_url", nullable = false, length = 2048)
|
||||
private String authorizeUrl;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "expires_at", nullable = false)
|
||||
private LocalDateTime expiresAt;
|
||||
|
||||
public boolean isExpired(LocalDateTime now) {
|
||||
return expiresAt != null && expiresAt.isBefore(now);
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package stirling.software.proprietary.accountlink;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
/** Data access for the singleton {@link ConnectState} row. */
|
||||
public interface ConnectStateRepository extends JpaRepository<ConnectState, Long> {}
|
||||
+2
-2
@@ -13,8 +13,8 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* The device credential this self-hosted instance received when it linked a SaaS account
|
||||
* (combined-billing "Mode A"). Singleton — one instance links to exactly one SaaS team.
|
||||
* The device credential this self-hosted instance received when it linked a SaaS account (combined
|
||||
* billing). Singleton — one instance links to exactly one SaaS team.
|
||||
*
|
||||
* <p>Unlike the SaaS side (which stores only a hash), the instance must keep the plaintext {@code
|
||||
* deviceSecret} so it can present it on every unattended entitlement call. It lives in the local
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Decides whether a request may proceed under combined-billing "Mode A" on a self-hosted instance.
|
||||
* Decides whether a request may proceed under combined billing on a self-hosted instance.
|
||||
*
|
||||
* <p>Rules (in order):
|
||||
*
|
||||
|
||||
+2
-2
@@ -39,8 +39,8 @@ import stirling.software.proprietary.policy.controller.PolicyRunRoutes;
|
||||
import stirling.software.proprietary.security.model.ApiKeyAuthenticationToken;
|
||||
|
||||
/**
|
||||
* Request-time gate + meter for combined-billing "Mode A". {@code preHandle} blocks billable (API /
|
||||
* AI / automation) work when the instance is unlinked or over its limit; manual tools pass through.
|
||||
* Request-time gate + meter for combined billing. {@code preHandle} blocks billable (API / AI /
|
||||
* automation) work when the instance is unlinked or over its limit; manual tools pass through.
|
||||
* {@code afterCompletion} meters a successful billable op into the per-period cumulative counter.
|
||||
*
|
||||
* <p>Blocking responds {@code 402} with a machine-readable body the FE maps to a "link to activate"
|
||||
|
||||
+5
-5
@@ -16,11 +16,11 @@ import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* The last time the instance metered a given input set this period — the local equivalent of the
|
||||
* cloud's lineage join (combined-billing "Mode A"). The meter dedups on a rolling <b>workflow
|
||||
* window</b>: an identical input set re-submitted within the window (see {@link
|
||||
* AccountLinkProperties.Metering}) is treated as workflow chaining and not re-charged, while the
|
||||
* same inputs run again after the window are billed afresh — matching the cloud's 5-minute open-job
|
||||
* window so the same operation costs the same on the instance and in the cloud.
|
||||
* cloud's lineage join (combined billing). The meter dedups on a rolling <b>workflow window</b>: an
|
||||
* identical input set re-submitted within the window (see {@link AccountLinkProperties.Metering})
|
||||
* is treated as workflow chaining and not re-charged, while the same inputs run again after the
|
||||
* window are billed afresh — matching the cloud's 5-minute open-job window so the same operation
|
||||
* costs the same on the instance and in the cloud.
|
||||
*
|
||||
* <p>{@code lastMeteredAt} is refreshed on every sighting (the window slides, as recording a cloud
|
||||
* artifact touches its job). One row per {@code (period, signature)}; the unique constraint also
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
/** Persistence for the per-period metered input-set signatures (combined-billing "Mode A"). */
|
||||
/** Persistence for the per-period metered input-set signatures (combined billing). */
|
||||
public interface MeteredInputSignatureRepository
|
||||
extends JpaRepository<MeteredInputSignature, Long> {
|
||||
|
||||
|
||||
+4
-4
@@ -17,10 +17,10 @@ import lombok.NoArgsConstructor;
|
||||
import stirling.software.proprietary.billing.BillingCategory;
|
||||
|
||||
/**
|
||||
* Durable per-(billing period, category) cumulative usage counter for combined-billing "Mode A".
|
||||
* Each successful billable op increments its row; the daily sync reports the cumulative totals and
|
||||
* SaaS bills the delta since the last sync. The cumulative model is idempotent (a resend bills
|
||||
* nothing) and tamper-evident (a counter that drops is a signal). One row per {@code (period_start,
|
||||
* Durable per-(billing period, category) cumulative usage counter for combined billing. Each
|
||||
* successful billable op increments its row; the daily sync reports the cumulative totals and SaaS
|
||||
* bills the delta since the last sync. The cumulative model is idempotent (a resend bills nothing)
|
||||
* and tamper-evident (a counter that drops is a signal). One row per {@code (period_start,
|
||||
* category)}, auto-created by Hibernate; only the flag-gated {@link UsageMeterService} writes it.
|
||||
*/
|
||||
@Entity
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/** Persistence for the per-period/per-category usage counters (combined-billing "Mode A"). */
|
||||
/** Persistence for the per-period/per-category usage counters (combined billing). */
|
||||
public interface UsageCounterRepository extends JpaRepository<UsageCounter, Long> {
|
||||
|
||||
/**
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.proprietary.billing.BillingCategory;
|
||||
|
||||
/**
|
||||
* Daily usage sender for combined-billing "Mode A". Reports each period's cumulative per-category
|
||||
* usage to SaaS, which bills the delta against its own last-seen totals.
|
||||
* Daily usage sender for combined billing. Reports each period's cumulative per-category usage to
|
||||
* SaaS, which bills the delta against its own last-seen totals.
|
||||
*
|
||||
* <p>Resilience: the sync seq is persisted before the report so it never regresses across
|
||||
* restarts/failures; a transport failure leaves the {@code lastSyncedUnits} markers untouched so
|
||||
|
||||
+3
-3
@@ -11,9 +11,9 @@ import java.util.HexFormat;
|
||||
|
||||
/**
|
||||
* SHA-256 content fingerprint shared by the SaaS charge path and the linked self-hosted instance's
|
||||
* meter (combined-billing "Mode A"), so both derive an <em>identical</em> signature for the same
|
||||
* bytes — the basis for lineage dedup. Pure, no Spring: fixed 64 KiB buffer (allocation independent
|
||||
* of file size), hardware-accelerated by the JVM where available.
|
||||
* meter (combined billing), so both derive an <em>identical</em> signature for the same bytes — the
|
||||
* basis for lineage dedup. Pure, no Spring: fixed 64 KiB buffer (allocation independent of file
|
||||
* size), hardware-accelerated by the JVM where available.
|
||||
*
|
||||
* <p>Lives in {@code :proprietary} (not {@code :common}) so it stays out of the community core
|
||||
* build yet is reachable from {@code :saas} (which depends on {@code :proprietary}).
|
||||
|
||||
+67
-27
@@ -21,9 +21,9 @@ import org.mockito.ArgumentCaptor;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Stubs the {@link HttpClient} so the SaaS endpoint is never actually called. Confirms register
|
||||
* relays the JWT and parses the credential, and that entitlement parsing + the fail-open (null on
|
||||
* unreachable) behaviour hold.
|
||||
* Stubs the {@link HttpClient} so the SaaS endpoint is never actually called. Confirms the connect
|
||||
* handshake refuses an authorize URL it would not navigate to and carries no user token, and that
|
||||
* entitlement parsing + the fail-open (null on unreachable) behaviour hold.
|
||||
*/
|
||||
class AccountLinkClientTest {
|
||||
|
||||
@@ -48,39 +48,79 @@ class AccountLinkClientTest {
|
||||
return resp;
|
||||
}
|
||||
|
||||
// register() is gone with the JWT relay, and with it the two tests that asserted this client
|
||||
// sends an Authorization: Bearer header. Nothing here carries a user token any more.
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void registerRelaysJwtAndParsesCredential() throws Exception {
|
||||
// Build the stub response first: nesting response() inside when() trips Mockito's
|
||||
// unfinished-stubbing check (inner when() runs mid outer when()).
|
||||
void connectRequestRefusesAnAuthorizeUrlItWouldNotNavigateTo() throws Exception {
|
||||
// The reply drives a browser navigation, so a non-absolute or non-http(s) value must fail
|
||||
// loudly here rather than reach the admin.
|
||||
HttpResponse<String> resp =
|
||||
response(201, "{\"deviceId\":\"dev-1\",\"deviceSecret\":\"sec-1\",\"teamId\":42}");
|
||||
ArgumentCaptor<HttpRequest> captor = ArgumentCaptor.forClass(HttpRequest.class);
|
||||
when(httpClient.send(captor.capture(), any(HttpResponse.BodyHandler.class)))
|
||||
.thenReturn(resp);
|
||||
response(201, "{\"requestId\":\"req-1\",\"authorizeUrl\":\"/link?request=req-1\"}");
|
||||
when(httpClient.send(any(), any(HttpResponse.BodyHandler.class))).thenReturn(resp);
|
||||
|
||||
AccountLinkClient.RegisterResult result = client.register("jwt-token", "My Server");
|
||||
|
||||
assertEquals("dev-1", result.deviceId());
|
||||
assertEquals("sec-1", result.deviceSecret());
|
||||
assertEquals(42L, result.teamId());
|
||||
|
||||
HttpRequest sent = captor.getValue();
|
||||
assertEquals("Bearer jwt-token", sent.headers().firstValue("Authorization").orElse(null));
|
||||
assertEquals(
|
||||
"https://saas.example.com/api/v1/account-link/register", sent.uri().toString());
|
||||
assertThrows(
|
||||
java.io.IOException.class,
|
||||
() -> client.connectRequest("n", "https://pdf.example.com/cb", "nonce", "secret"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void registerThrowsUpstreamExceptionWithStatusOnNon2xx() throws Exception {
|
||||
HttpResponse<String> resp = response(401, "{\"error\":\"unauthorized\"}");
|
||||
void connectRequestParsesTheAuthorizeUrlItIsGiven() throws Exception {
|
||||
HttpResponse<String> resp =
|
||||
response(
|
||||
201,
|
||||
"{\"requestId\":\"req-1\",\"expiresIn\":900,"
|
||||
+ "\"authorizeUrl\":\"https://app.example.com/link?request=req-1\"}");
|
||||
ArgumentCaptor<HttpRequest> captor = ArgumentCaptor.forClass(HttpRequest.class);
|
||||
when(httpClient.send(captor.capture(), any(HttpResponse.BodyHandler.class)))
|
||||
.thenReturn(resp);
|
||||
|
||||
AccountLinkClient.ConnectRequestResult result =
|
||||
client.connectRequest("n", "https://pdf.example.com/cb", "nonce", "secret");
|
||||
|
||||
assertEquals("req-1", result.requestId());
|
||||
assertEquals("https://app.example.com/link?request=req-1", result.authorizeUrl());
|
||||
// No user token on this call, by design.
|
||||
assertEquals(null, captor.getValue().headers().firstValue("Authorization").orElse(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void connectClaimGrantsTheCredentialOnSuccess() throws Exception {
|
||||
HttpResponse<String> resp =
|
||||
response(200, "{\"deviceId\":\"dev-1\",\"deviceSecret\":\"sec-1\",\"teamId\":7}");
|
||||
when(httpClient.send(any(), any(HttpResponse.BodyHandler.class))).thenReturn(resp);
|
||||
AccountLinkClient.UpstreamException ex =
|
||||
assertThrows(
|
||||
AccountLinkClient.UpstreamException.class,
|
||||
() -> client.register("jwt", null));
|
||||
assertEquals(401, ex.status());
|
||||
|
||||
AccountLinkClient.ConnectClaimResult result = client.connectClaim("req-1", "secret");
|
||||
|
||||
assertEquals(AccountLinkClient.ConnectClaimOutcome.GRANTED, result.outcome());
|
||||
assertEquals("dev-1", result.deviceId());
|
||||
assertEquals("sec-1", result.deviceSecret());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void connectClaimMapsTheStatusItIsGiven() throws Exception {
|
||||
// The whole point of these four: a claim consumes the request server-side, so
|
||||
// reading 200 as anything but success loses the credential irrecoverably.
|
||||
assertEquals(AccountLinkClient.ConnectClaimOutcome.PENDING, claimOutcome(202, "{}"));
|
||||
assertEquals(AccountLinkClient.ConnectClaimOutcome.UNAVAILABLE, claimOutcome(503, "{}"));
|
||||
assertEquals(AccountLinkClient.ConnectClaimOutcome.REJECTED, claimOutcome(400, "{}"));
|
||||
assertEquals(
|
||||
AccountLinkClient.ConnectClaimOutcome.CONFIRMED,
|
||||
claimOutcome(200, "{\"status\":\"confirmed\",\"teamId\":7}"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private AccountLinkClient.ConnectClaimOutcome claimOutcome(int status, String body)
|
||||
throws Exception {
|
||||
// Built before the when(), not inside it: response() stubs a mock of its own, and
|
||||
// Mockito cannot have that happen mid-stubbing.
|
||||
HttpResponse<String> resp = response(status, body);
|
||||
when(httpClient.send(any(), any(HttpResponse.BodyHandler.class))).thenReturn(resp);
|
||||
return client.connectClaim("req-1", "secret").outcome();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+40
-33
@@ -1,6 +1,7 @@
|
||||
package stirling.software.proprietary.accountlink;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -14,16 +15,15 @@ import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import stirling.software.proprietary.accountlink.AccountLinkController.LinkRequest;
|
||||
|
||||
/**
|
||||
* The local (self-hosted) account-link controller's error mapping: an upstream auth rejection
|
||||
* surfaces as 401/403 (so the portal can prompt a re-sign-in) while other upstream / transport
|
||||
* faults are a 502.
|
||||
* The local (self-hosted) account-link controller's error mapping. Every upstream or transport
|
||||
* failure is a 502, and the response body never echoes the exception, because a DNS or TLS message
|
||||
* can carry the configured SaaS host.
|
||||
*/
|
||||
class AccountLinkControllerTest {
|
||||
|
||||
private AccountLinkService service;
|
||||
private ConnectService connectService;
|
||||
private UsageSyncService syncService;
|
||||
private ObjectProvider<UsageSyncService> syncProvider;
|
||||
private AccountLinkController controller;
|
||||
@@ -32,47 +32,54 @@ class AccountLinkControllerTest {
|
||||
@SuppressWarnings("unchecked")
|
||||
void setUp() {
|
||||
service = mock(AccountLinkService.class);
|
||||
connectService = mock(ConnectService.class);
|
||||
syncService = mock(UsageSyncService.class);
|
||||
syncProvider = mock(ObjectProvider.class);
|
||||
controller =
|
||||
new AccountLinkController(service, mock(LocalUsageService.class), syncProvider);
|
||||
new AccountLinkController(
|
||||
service, connectService, mock(LocalUsageService.class), syncProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
void link_missingJwt_returns400() {
|
||||
ResponseEntity<?> resp = controller.link(new LinkRequest(" ", null));
|
||||
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
// These asserted POST /link's error mapping, which distinguished 401/403 so the portal could
|
||||
// prompt a re-sign-in. That endpoint is gone with the JWT relay, and the distinction went with
|
||||
// it: connect/start carries no user token, so an upstream refusal is never the admin's session
|
||||
// and everything non-transport is a plain gateway failure.
|
||||
|
||||
@Test
|
||||
void link_upstreamUnauthorized_maps401() throws Exception {
|
||||
when(service.link("jwt", null))
|
||||
.thenThrow(new AccountLinkClient.UpstreamException(401, "bad token"));
|
||||
ResponseEntity<?> resp = controller.link(new LinkRequest("jwt", null));
|
||||
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void link_upstreamForbidden_maps403() throws Exception {
|
||||
when(service.link("jwt", null))
|
||||
.thenThrow(new AccountLinkClient.UpstreamException(403, "forbidden"));
|
||||
ResponseEntity<?> resp = controller.link(new LinkRequest("jwt", null));
|
||||
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void link_upstreamServerError_maps502() throws Exception {
|
||||
when(service.link("jwt", null))
|
||||
void connectStart_upstreamFailure_maps502() throws Exception {
|
||||
when(connectService.start(any(), any()))
|
||||
.thenThrow(new AccountLinkClient.UpstreamException(500, "boom"));
|
||||
ResponseEntity<?> resp = controller.link(new LinkRequest("jwt", null));
|
||||
|
||||
ResponseEntity<?> resp = controller.connectStart(null, request());
|
||||
|
||||
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.BAD_GATEWAY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void link_transportFailure_maps502() throws Exception {
|
||||
when(service.link("jwt", null)).thenThrow(new IOException("connection refused"));
|
||||
ResponseEntity<?> resp = controller.link(new LinkRequest("jwt", null));
|
||||
void connectStart_transportFailure_maps502WithoutLeakingTheHost() throws Exception {
|
||||
when(connectService.start(any(), any()))
|
||||
.thenThrow(new IOException("connection refused to saas.internal:8081"));
|
||||
|
||||
ResponseEntity<?> resp = controller.connectStart(null, request());
|
||||
|
||||
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.BAD_GATEWAY);
|
||||
// The body must not echo the exception: a DNS/TLS message can carry the configured SaaS
|
||||
// host.
|
||||
assertThat(String.valueOf(resp.getBody())).doesNotContain("saas.internal");
|
||||
}
|
||||
|
||||
@Test
|
||||
void connectReauth_onAnUnlinkedServer_maps502() throws Exception {
|
||||
when(connectService.startReauth(any())).thenThrow(new IOException("not linked"));
|
||||
|
||||
ResponseEntity<?> resp = controller.connectReauth(null, request());
|
||||
|
||||
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.BAD_GATEWAY);
|
||||
}
|
||||
|
||||
/** Minimal request: the controller only reads Origin and the forwarded/host details from it. */
|
||||
private static jakarta.servlet.http.HttpServletRequest request() {
|
||||
return new org.springframework.mock.web.MockHttpServletRequest();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+6
-16
@@ -3,12 +3,10 @@ package stirling.software.proprietary.accountlink;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -30,33 +28,25 @@ class AccountLinkServiceTest {
|
||||
service = new AccountLinkService(client, store, cache);
|
||||
}
|
||||
|
||||
// The two link() tests here are gone with the JWT relay. Storing a credential and invalidating
|
||||
// the entitlement cache is now ConnectService's job and is covered by ConnectServiceTest; what
|
||||
// remains in this service is status and unlink.
|
||||
|
||||
@Test
|
||||
void link_storesCredentialAndInvalidatesCache() throws IOException {
|
||||
when(client.register("jwt", "name"))
|
||||
.thenReturn(new AccountLinkClient.RegisterResult("dev-1", "sec-1", 7L));
|
||||
void status_linkedFromTheStoredCredential() {
|
||||
DeviceCredential stored = new DeviceCredential();
|
||||
stored.setDeviceId("dev-1");
|
||||
stored.setTeamId(7L);
|
||||
stored.setLinkedAt(LocalDateTime.now());
|
||||
when(store.get()).thenReturn(Optional.of(stored));
|
||||
|
||||
AccountLinkService.LinkStatus status = service.link("jwt", "name");
|
||||
AccountLinkService.LinkStatus status = service.status();
|
||||
|
||||
verify(store).save("dev-1", "sec-1", 7L);
|
||||
verify(cache).invalidate();
|
||||
assertTrue(status.linked());
|
||||
assertEquals("dev-1", status.deviceId());
|
||||
assertEquals(7L, status.teamId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void link_propagatesRegisterFailure() throws IOException {
|
||||
when(client.register(any(), any())).thenThrow(new IOException("boom"));
|
||||
org.junit.jupiter.api.Assertions.assertThrows(
|
||||
IOException.class, () -> service.link("jwt", null));
|
||||
verify(cache, org.mockito.Mockito.never()).invalidate();
|
||||
}
|
||||
|
||||
@Test
|
||||
void status_unlinkedWhenNoCredential() {
|
||||
when(store.get()).thenReturn(Optional.empty());
|
||||
|
||||
+408
@@ -0,0 +1,408 @@
|
||||
package stirling.software.proprietary.accountlink;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.proprietary.accountlink.AccountLinkClient.ConnectClaimOutcome;
|
||||
import stirling.software.proprietary.accountlink.AccountLinkClient.ConnectClaimResult;
|
||||
import stirling.software.proprietary.accountlink.AccountLinkClient.ConnectRequestResult;
|
||||
import stirling.software.proprietary.accountlink.ConnectService.Phase;
|
||||
|
||||
/** Unit tests for the instance half of the connect handshake. */
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class ConnectServiceTest {
|
||||
|
||||
private static final String NONCE = "the-nonce";
|
||||
private static final String CLAIM_SECRET = "the-claim-secret";
|
||||
private static final String AUTHORIZE_URL = "https://app.example.com/link?request=req-1";
|
||||
|
||||
@Mock private AccountLinkClient client;
|
||||
@Mock private ConnectStateRepository stateRepo;
|
||||
@Mock private DeviceCredentialStore credentialStore;
|
||||
@Mock private EntitlementCache entitlementCache;
|
||||
|
||||
private ApplicationProperties applicationProperties;
|
||||
private ConnectService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
applicationProperties = new ApplicationProperties();
|
||||
service =
|
||||
new ConnectService(
|
||||
client,
|
||||
stateRepo,
|
||||
credentialStore,
|
||||
entitlementCache,
|
||||
applicationProperties);
|
||||
}
|
||||
|
||||
private void configureFrontendUrl(String url) {
|
||||
applicationProperties.getSystem().setFrontendUrl(url);
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_advertisesTheConfiguredFrontendUrlInPreferenceToTheRequest() throws Exception {
|
||||
configureFrontendUrl("https://pdf.example.com/");
|
||||
stubCreate();
|
||||
|
||||
service.start("prod-1", fromRequest("http://10.0.0.5:8080"));
|
||||
|
||||
verify(client)
|
||||
.connectRequest(
|
||||
anyString(),
|
||||
// Trailing slash trimmed, and the request's own view ignored.
|
||||
org.mockito.ArgumentMatchers.eq(
|
||||
"https://pdf.example.com" + ConnectService.CALLBACK_PATH),
|
||||
anyString(),
|
||||
anyString(),
|
||||
// A first link carries no credential; that is what makes it a first link.
|
||||
org.mockito.ArgumentMatchers.isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_fallsBackToTheAddressTheRequestArrivedOn() throws Exception {
|
||||
stubCreate();
|
||||
|
||||
service.start(null, fromRequest("https://pdf.internal:8443/stirling"));
|
||||
|
||||
ArgumentCaptor<String> callback = ArgumentCaptor.forClass(String.class);
|
||||
verify(client).connectRequest(any(), callback.capture(), anyString(), anyString(), any());
|
||||
// Context path preserved, so a subpath deployment gets a callback that resolves.
|
||||
assertThat(callback.getValue())
|
||||
.isEqualTo("https://pdf.internal:8443/stirling" + ConnectService.CALLBACK_PATH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_withNoAddressAtAllFailsRatherThanGuessing() {
|
||||
assertThat(catchIo(() -> service.start(null, fromRequest(null))))
|
||||
.hasMessageContaining("system.frontendUrl");
|
||||
verifyNoInteractions(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveCallback_honoursThePortalsOwnCallbackWhenTheBrowserOriginAgrees() {
|
||||
// The frontend is the only party that knows its router's base path.
|
||||
String requested = "http://localhost:5173/app/account-link/callback";
|
||||
|
||||
assertThat(
|
||||
service.resolveCallbackUrl(
|
||||
new ConnectService.CallbackHint(
|
||||
requested,
|
||||
"http://localhost:5173",
|
||||
"http://localhost:8080")))
|
||||
.isEqualTo(requested);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveCallback_ignoresACallbackFromADifferentOrigin() {
|
||||
assertThat(
|
||||
service.resolveCallbackUrl(
|
||||
new ConnectService.CallbackHint(
|
||||
"https://evil.example.com/steal",
|
||||
"http://localhost:5173",
|
||||
"http://localhost:8080")))
|
||||
.isEqualTo("http://localhost:5173" + ConnectService.CALLBACK_PATH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveCallback_prefersTheBrowserOriginOverTheApiRequest() {
|
||||
// The whole point: :5173 is where the admin is, :8080 is where the call landed.
|
||||
assertThat(
|
||||
service.resolveCallbackUrl(
|
||||
new ConnectService.CallbackHint(
|
||||
null, "http://localhost:5173", "http://localhost:8080")))
|
||||
.isEqualTo("http://localhost:5173" + ConnectService.CALLBACK_PATH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveCallback_letsConfigurationBeatEverything() {
|
||||
configureFrontendUrl("https://pdf.example.com/");
|
||||
|
||||
assertThat(
|
||||
service.resolveCallbackUrl(
|
||||
new ConnectService.CallbackHint(
|
||||
"http://localhost:5173/account-link/callback",
|
||||
"http://localhost:5173",
|
||||
"http://localhost:8080")))
|
||||
.isEqualTo("https://pdf.example.com" + ConnectService.CALLBACK_PATH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveCallback_ignoresAnUnusableOriginHeader() {
|
||||
// "null" is what a browser sends for an opaque origin; it must not become a callback.
|
||||
assertThat(
|
||||
service.resolveCallbackUrl(
|
||||
new ConnectService.CallbackHint(
|
||||
null, "null", "http://localhost:8080")))
|
||||
.isEqualTo("http://localhost:8080" + ConnectService.CALLBACK_PATH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_sendsTheAdminWhereverSaaSSaidToSendThem() throws Exception {
|
||||
stubCreate();
|
||||
|
||||
ConnectService.ConnectStatus status =
|
||||
service.start(null, fromRequest("https://pdf.example.com"));
|
||||
|
||||
assertThat(status.phase()).isEqualTo(Phase.PENDING);
|
||||
// Not composed here: only the SaaS side knows where its approval page lives, so an
|
||||
// instance configuring that could only get it wrong.
|
||||
assertThat(status.authorizeUrl()).isEqualTo(AUTHORIZE_URL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_keepsTheNonceAndClaimSecretItSent() throws Exception {
|
||||
stubCreate();
|
||||
|
||||
service.start(null, fromRequest("https://pdf.example.com"));
|
||||
|
||||
ArgumentCaptor<String> nonce = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<String> secret = ArgumentCaptor.forClass(String.class);
|
||||
verify(client).connectRequest(any(), anyString(), nonce.capture(), secret.capture(), any());
|
||||
|
||||
ArgumentCaptor<ConnectState> saved = ArgumentCaptor.forClass(ConnectState.class);
|
||||
verify(stateRepo).save(saved.capture());
|
||||
assertThat(saved.getValue().getNonce()).isEqualTo(nonce.getValue());
|
||||
assertThat(saved.getValue().getClaimSecret()).isEqualTo(secret.getValue());
|
||||
// Two independent secrets, not one value used twice.
|
||||
assertThat(nonce.getValue()).isNotEqualTo(secret.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_whenAlreadyLinkedDoesNothing() throws Exception {
|
||||
when(credentialStore.isLinked()).thenReturn(true);
|
||||
when(credentialStore.get()).thenReturn(Optional.of(credential(7L)));
|
||||
|
||||
ConnectService.ConnectStatus status =
|
||||
service.start(null, fromRequest("https://pdf.example.com"));
|
||||
|
||||
assertThat(status.phase()).isEqualTo(Phase.LINKED);
|
||||
verifyNoInteractions(client);
|
||||
verify(stateRepo, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void complete_withTheRightNonceStoresTheCredentialAndClearsTheHandshake() {
|
||||
ConnectState state = openHandshake();
|
||||
when(stateRepo.findById(ConnectState.SINGLETON_ID)).thenReturn(Optional.of(state));
|
||||
when(client.connectClaim("req-1", CLAIM_SECRET))
|
||||
.thenReturn(new ConnectClaimResult(ConnectClaimOutcome.GRANTED, "dev", "sec", 7L));
|
||||
|
||||
ConnectService.ConnectStatus status = service.complete(NONCE);
|
||||
|
||||
assertThat(status.phase()).isEqualTo(Phase.LINKED);
|
||||
assertThat(status.teamId()).isEqualTo(7L);
|
||||
verify(credentialStore).save("dev", "sec", 7L);
|
||||
verify(entitlementCache).invalidate();
|
||||
verify(stateRepo).delete(state);
|
||||
}
|
||||
|
||||
@Test
|
||||
void complete_withAWrongNonceClaimsNothingAndLeavesTheHandshakeAlone() {
|
||||
ConnectState state = openHandshake();
|
||||
when(stateRepo.findById(ConnectState.SINGLETON_ID)).thenReturn(Optional.of(state));
|
||||
|
||||
ConnectService.ConnectStatus status = service.complete("not-the-nonce");
|
||||
|
||||
assertThat(status.phase()).isEqualTo(Phase.REJECTED);
|
||||
// The important half: an unverified caller cannot cancel a legitimate handshake.
|
||||
verify(stateRepo, never()).delete(any());
|
||||
verifyNoInteractions(credentialStore);
|
||||
verify(client, never()).connectClaim(anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void complete_withNoNonceAtAllIsRejected() {
|
||||
when(stateRepo.findById(ConnectState.SINGLETON_ID))
|
||||
.thenReturn(Optional.of(openHandshake()));
|
||||
|
||||
assertThat(service.complete(null).phase()).isEqualTo(Phase.REJECTED);
|
||||
verify(client, never()).connectClaim(anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void complete_whenSaaSHasNotCommittedTheApprovalKeepsTheHandshake() {
|
||||
when(stateRepo.findById(ConnectState.SINGLETON_ID))
|
||||
.thenReturn(Optional.of(openHandshake()));
|
||||
when(client.connectClaim(anyString(), anyString()))
|
||||
.thenReturn(ConnectClaimResult.of(ConnectClaimOutcome.PENDING));
|
||||
|
||||
assertThat(service.complete(NONCE).phase()).isEqualTo(Phase.PENDING);
|
||||
verify(stateRepo, never()).delete(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void complete_whenSaaSIsUnreachableKeepsTheHandshakeForARetry() {
|
||||
when(stateRepo.findById(ConnectState.SINGLETON_ID))
|
||||
.thenReturn(Optional.of(openHandshake()));
|
||||
when(client.connectClaim(anyString(), anyString()))
|
||||
.thenReturn(ConnectClaimResult.of(ConnectClaimOutcome.UNAVAILABLE));
|
||||
|
||||
assertThat(service.complete(NONCE).phase()).isEqualTo(Phase.UNAVAILABLE);
|
||||
verify(stateRepo, never()).delete(any());
|
||||
verifyNoInteractions(credentialStore);
|
||||
}
|
||||
|
||||
@Test
|
||||
void complete_whenDeclinedClearsTheHandshake() {
|
||||
ConnectState state = openHandshake();
|
||||
when(stateRepo.findById(ConnectState.SINGLETON_ID)).thenReturn(Optional.of(state));
|
||||
when(client.connectClaim(anyString(), anyString()))
|
||||
.thenReturn(ConnectClaimResult.of(ConnectClaimOutcome.REJECTED));
|
||||
|
||||
assertThat(service.complete(NONCE).phase()).isEqualTo(Phase.REJECTED);
|
||||
verify(stateRepo).delete(state);
|
||||
verifyNoInteractions(credentialStore);
|
||||
}
|
||||
|
||||
@Test
|
||||
void complete_onAnExpiredHandshakeClearsItWithoutClaiming() {
|
||||
ConnectState state = openHandshake();
|
||||
state.setExpiresAt(LocalDateTime.now().minusSeconds(1));
|
||||
when(stateRepo.findById(ConnectState.SINGLETON_ID)).thenReturn(Optional.of(state));
|
||||
|
||||
assertThat(service.complete(NONCE).phase()).isEqualTo(Phase.EXPIRED);
|
||||
verify(stateRepo).delete(state);
|
||||
verify(client, never()).connectClaim(anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void startReauth_presentsTheCredentialSoSaaSCanPinTheTeam() throws Exception {
|
||||
when(credentialStore.get()).thenReturn(Optional.of(credential(7L)));
|
||||
when(client.connectRequest(any(), anyString(), anyString(), anyString(), any()))
|
||||
.thenReturn(new ConnectRequestResult("req-1", 900, AUTHORIZE_URL));
|
||||
|
||||
service.startReauth(fromRequest("https://pdf.example.com"));
|
||||
|
||||
// Sending the credential is what makes the pinning trustworthy: the team comes from
|
||||
// something only this instance holds.
|
||||
verify(client)
|
||||
.connectRequest(
|
||||
any(),
|
||||
anyString(),
|
||||
anyString(),
|
||||
anyString(),
|
||||
org.mockito.ArgumentMatchers.argThat(
|
||||
c -> c != null && "dev".equals(c.getDeviceId())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void startReauth_onAnUnlinkedServerFails() {
|
||||
assertThat(catchIo(() -> service.startReauth(fromRequest("https://pdf.example.com"))))
|
||||
.hasMessageContaining("not linked");
|
||||
verifyNoInteractions(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void complete_onAConfirmedReauthKeepsTheExistingCredential() {
|
||||
ConnectState state = openHandshake();
|
||||
when(stateRepo.findById(ConnectState.SINGLETON_ID)).thenReturn(Optional.of(state));
|
||||
when(client.connectClaim(anyString(), anyString()))
|
||||
.thenReturn(new ConnectClaimResult(ConnectClaimOutcome.CONFIRMED, null, null, 7L));
|
||||
|
||||
ConnectService.ConnectStatus status = service.complete(NONCE);
|
||||
|
||||
assertThat(status.phase()).isEqualTo(Phase.LINKED);
|
||||
assertThat(status.teamId()).isEqualTo(7L);
|
||||
// Nothing to store: a second credential would orphan the one we already hold.
|
||||
verify(credentialStore, never()).save(anyString(), anyString(), any());
|
||||
verify(stateRepo).delete(state);
|
||||
}
|
||||
|
||||
@Test
|
||||
void status_reportsNothingInFlightWhenThereIsNoHandshakeOrCredential() {
|
||||
assertThat(service.status().phase()).isEqualTo(Phase.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void status_reportsAnExpiredHandshakeRatherThanOfferingAStaleLink() {
|
||||
ConnectState state = openHandshake();
|
||||
state.setExpiresAt(LocalDateTime.now().minusSeconds(1));
|
||||
when(stateRepo.findById(ConnectState.SINGLETON_ID)).thenReturn(Optional.of(state));
|
||||
|
||||
ConnectService.ConnectStatus status = service.status();
|
||||
|
||||
assertThat(status.phase()).isEqualTo(Phase.EXPIRED);
|
||||
assertThat(status.authorizeUrl()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void status_countsDownWhileAHandshakeIsOpen() {
|
||||
when(stateRepo.findById(ConnectState.SINGLETON_ID))
|
||||
.thenReturn(Optional.of(openHandshake()));
|
||||
|
||||
ConnectService.ConnectStatus status = service.status();
|
||||
|
||||
assertThat(status.phase()).isEqualTo(Phase.PENDING);
|
||||
assertThat(status.secondsRemaining()).isPositive();
|
||||
assertThat(status.authorizeUrl()).isEqualTo("https://app.example.com/link?request=req-1");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
/** A start with nothing but the reconstructed request URL, as a headless caller would send. */
|
||||
private static ConnectService.CallbackHint fromRequest(String derivedBaseUrl) {
|
||||
return new ConnectService.CallbackHint(null, null, derivedBaseUrl);
|
||||
}
|
||||
|
||||
private void stubCreate() throws Exception {
|
||||
// The five-argument overload: a first link passes a null credential rather than none.
|
||||
when(client.connectRequest(any(), anyString(), anyString(), anyString(), any()))
|
||||
.thenReturn(new ConnectRequestResult("req-1", 900, AUTHORIZE_URL));
|
||||
}
|
||||
|
||||
private static ConnectState openHandshake() {
|
||||
ConnectState state = new ConnectState();
|
||||
state.setId(ConnectState.SINGLETON_ID);
|
||||
state.setRequestId("req-1");
|
||||
state.setNonce(NONCE);
|
||||
state.setClaimSecret(CLAIM_SECRET);
|
||||
state.setCallbackUrl("https://pdf.example.com/account-link/callback");
|
||||
state.setAuthorizeUrl("https://app.example.com/link?request=req-1");
|
||||
state.setCreatedAt(LocalDateTime.now());
|
||||
state.setExpiresAt(LocalDateTime.now().plusMinutes(10));
|
||||
return state;
|
||||
}
|
||||
|
||||
private static DeviceCredential credential(Long teamId) {
|
||||
DeviceCredential credential = new DeviceCredential();
|
||||
credential.setDeviceId("dev");
|
||||
credential.setDeviceSecret("sec");
|
||||
credential.setTeamId(teamId);
|
||||
credential.setLinkedAt(LocalDateTime.now());
|
||||
return credential;
|
||||
}
|
||||
|
||||
/** Runs a throwing call and returns the exception, so the assertion reads in one line. */
|
||||
private static Throwable catchIo(ThrowingCall call) {
|
||||
try {
|
||||
call.run();
|
||||
throw new AssertionError("expected the call to fail");
|
||||
} catch (Exception e) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
private interface ThrowingCall {
|
||||
void run() throws Exception;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+7
-85
@@ -4,14 +4,12 @@ import java.util.List;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -19,25 +17,9 @@ import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.enumeration.TeamRole;
|
||||
import stirling.software.proprietary.model.TeamMembership;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.repository.TeamMembershipRepository;
|
||||
import stirling.software.saas.util.AuthenticationUtils;
|
||||
import stirling.software.saas.accountlink.LeaderTeamResolver.LeaderTeam;
|
||||
|
||||
/**
|
||||
* Account-link registration surface (combined-billing "Mode A").
|
||||
*
|
||||
* <p>A self-hosted instance's local backend calls {@code POST /register} with the admin's
|
||||
* short-lived Supabase JWT (validated by the existing {@code SupabaseSecurityConfig} chain — no new
|
||||
* auth here). We resolve the caller's team, mint a device credential bound to it, and return the
|
||||
* secret exactly once. Ongoing entitlement reads authenticate with that device credential, not this
|
||||
* JWT.
|
||||
*
|
||||
* <p>Whole surface gated behind {@code stirling.billing.account-link.enabled}: off → beans absent →
|
||||
* 404. Leader-only, and the team is always derived from the caller (never the request body).
|
||||
*/
|
||||
/** Team-wide management of linked instances (combined billing). */
|
||||
@Slf4j
|
||||
@Hidden
|
||||
@RestController
|
||||
@@ -47,25 +29,13 @@ import stirling.software.saas.util.AuthenticationUtils;
|
||||
public class AccountLinkController {
|
||||
|
||||
private final AccountLinkService service;
|
||||
private final TeamMembershipRepository memberRepo;
|
||||
private final UserRepository userRepository;
|
||||
private final LeaderTeamResolver leaderTeams;
|
||||
|
||||
public AccountLinkController(
|
||||
AccountLinkService service,
|
||||
TeamMembershipRepository memberRepo,
|
||||
UserRepository userRepository) {
|
||||
public AccountLinkController(AccountLinkService service, LeaderTeamResolver leaderTeams) {
|
||||
this.service = service;
|
||||
this.memberRepo = memberRepo;
|
||||
this.userRepository = userRepository;
|
||||
this.leaderTeams = leaderTeams;
|
||||
}
|
||||
|
||||
/** Optional display name for the instance (hostname / label). */
|
||||
public record RegisterRequest(String name) {}
|
||||
|
||||
/** {@code deviceSecret} is plaintext and returned exactly once — the caller must store it. */
|
||||
public record RegisterResponse(
|
||||
Long instanceId, Long teamId, String deviceId, String deviceSecret, String name) {}
|
||||
|
||||
public record InstanceRow(
|
||||
Long instanceId,
|
||||
String deviceId,
|
||||
@@ -74,31 +44,10 @@ public class AccountLinkController {
|
||||
String lastSeenAt,
|
||||
boolean revoked) {}
|
||||
|
||||
@PostMapping("/register")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<RegisterResponse> register(
|
||||
@RequestBody(required = false) RegisterRequest req, Authentication auth) {
|
||||
LeaderTeam lt = resolveLeaderTeam(auth);
|
||||
if (lt.error() != null) {
|
||||
return ResponseEntity.status(lt.error()).build();
|
||||
}
|
||||
String name = req != null ? req.name() : null;
|
||||
AccountLinkService.RegisteredInstance reg =
|
||||
service.register(lt.teamId(), lt.userId(), name);
|
||||
return ResponseEntity.status(HttpStatus.CREATED)
|
||||
.body(
|
||||
new RegisterResponse(
|
||||
reg.instanceId(),
|
||||
lt.teamId(),
|
||||
reg.deviceId(),
|
||||
reg.deviceSecret(),
|
||||
reg.name()));
|
||||
}
|
||||
|
||||
@GetMapping("/instances")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<List<InstanceRow>> list(Authentication auth) {
|
||||
LeaderTeam lt = resolveLeaderTeam(auth);
|
||||
LeaderTeam lt = leaderTeams.resolve(auth);
|
||||
if (lt.error() != null) {
|
||||
return ResponseEntity.status(lt.error()).build();
|
||||
}
|
||||
@@ -124,38 +73,11 @@ public class AccountLinkController {
|
||||
@PostMapping("/instances/{instanceId}/revoke")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<Void> revoke(@PathVariable Long instanceId, Authentication auth) {
|
||||
LeaderTeam lt = resolveLeaderTeam(auth);
|
||||
LeaderTeam lt = leaderTeams.resolve(auth);
|
||||
if (lt.error() != null) {
|
||||
return ResponseEntity.status(lt.error()).build();
|
||||
}
|
||||
boolean ok = service.revoke(lt.teamId(), instanceId);
|
||||
return ok ? ResponseEntity.noContent().build() : ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// Helpers — team always derived from the caller; instance linking is a leader (billing) action.
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolved caller team, or an {@code error} status to return (teamId/userId null when error).
|
||||
*/
|
||||
private record LeaderTeam(Long teamId, Long userId, HttpStatus error) {}
|
||||
|
||||
private LeaderTeam resolveLeaderTeam(Authentication auth) {
|
||||
User user;
|
||||
try {
|
||||
user = AuthenticationUtils.getCurrentUser(auth, userRepository);
|
||||
} catch (SecurityException e) {
|
||||
return new LeaderTeam(null, null, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
List<TeamMembership> rows = memberRepo.findPrimaryMembership(user.getId());
|
||||
if (rows.isEmpty()) {
|
||||
return new LeaderTeam(null, null, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
TeamMembership m = rows.getFirst();
|
||||
if (m.getRole() != TeamRole.LEADER) {
|
||||
return new LeaderTeam(null, null, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
return new LeaderTeam(m.getTeam().getId(), user.getId(), null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,16 +18,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Account-link instance registration + lifecycle (combined-billing "Mode A").
|
||||
*
|
||||
* <p>Mints a {@code device_id} (public) + {@code device_secret} (high-entropy, returned once) bound
|
||||
* to a team, persisting only the SHA-256 hash of the secret. The instance authenticates its
|
||||
* unattended entitlement reads with that credential.
|
||||
*
|
||||
* <p>Gated behind {@code stirling.billing.account-link.enabled}: when off the bean is absent, so
|
||||
* {@link AccountLinkController} (which depends on it) drops out too and its endpoints 404.
|
||||
*/
|
||||
/** Account-link instance registration + lifecycle (combined billing). */
|
||||
@Slf4j
|
||||
@Service
|
||||
@Profile("saas")
|
||||
@@ -80,10 +71,7 @@ public class AccountLinkService {
|
||||
return repo.findByTeamIdOrderByCreatedAtDesc(teamId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes an instance iff it belongs to {@code teamId}. Returns false if not found or owned by
|
||||
* a different team (so a caller can never revoke another team's instance). Idempotent.
|
||||
*/
|
||||
/** Revokes an instance iff it belongs to {@code teamId}. */
|
||||
@Transactional
|
||||
public boolean revoke(Long teamId, Long instanceId) {
|
||||
Optional<LinkedInstance> found = repo.findById(instanceId);
|
||||
@@ -99,13 +87,30 @@ public class AccountLinkService {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an active instance from a device credential, or empty if it does not authenticate.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<LinkedInstance> resolveActiveInstance(String deviceId, String deviceSecret) {
|
||||
if (deviceId == null || deviceSecret == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return repo.findByDeviceIdAndRevokedAtIsNull(deviceId)
|
||||
.filter(
|
||||
instance ->
|
||||
MessageDigest.isEqual(
|
||||
sha256Hex(deviceSecret).getBytes(StandardCharsets.UTF_8),
|
||||
instance.getDeviceSecretHash()
|
||||
.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
private String randomSecret() {
|
||||
byte[] buf = new byte[SECRET_BYTES];
|
||||
random.nextBytes(buf);
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||
}
|
||||
|
||||
/** SHA-256 hex of a value. The device secret is high-entropy, so no salt is required. */
|
||||
/** SHA-256 hex of a value. */
|
||||
static String sha256Hex(String value) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
package stirling.software.saas.accountlink;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.saas.accountlink.LeaderTeamResolver.LeaderTeam;
|
||||
|
||||
/** Browser-mediated "connect this server" handshake. */
|
||||
@Slf4j
|
||||
@Hidden
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/account-link/connect")
|
||||
@Profile("saas")
|
||||
@ConditionalOnProperty(name = "stirling.billing.account-link.enabled", havingValue = "true")
|
||||
public class ConnectController {
|
||||
|
||||
/** Same headers the device-credential filter uses on the {@code /api/v1/instance} paths. */
|
||||
static final String HEADER_DEVICE_ID = "X-Device-Id";
|
||||
|
||||
static final String HEADER_DEVICE_SECRET = "X-Device-Secret";
|
||||
|
||||
/** Frontend route serving the approval page. */
|
||||
static final String LINK_PATH = "/link";
|
||||
|
||||
private final ConnectRequestService service;
|
||||
private final LeaderTeamResolver leaderTeams;
|
||||
private final AccountLinkService accountLinkService;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
public ConnectController(
|
||||
ConnectRequestService service,
|
||||
LeaderTeamResolver leaderTeams,
|
||||
AccountLinkService accountLinkService,
|
||||
ApplicationProperties applicationProperties) {
|
||||
this.service = service;
|
||||
this.leaderTeams = leaderTeams;
|
||||
this.accountLinkService = accountLinkService;
|
||||
this.applicationProperties = applicationProperties;
|
||||
}
|
||||
|
||||
/** Sent by the instance's own backend, before it holds any credential. */
|
||||
public record CreateBody(String name, String callbackUrl, String nonce, String claimSecret) {}
|
||||
|
||||
/** {@code authorizeUrl} is where the instance should send its admin. */
|
||||
public record CreateResponse(String requestId, int expiresIn, String authorizeUrl) {}
|
||||
|
||||
/** What the approval page renders. */
|
||||
public record ViewResponse(
|
||||
String requestId,
|
||||
String name,
|
||||
String callbackOrigin,
|
||||
boolean insecureTransport,
|
||||
String mode,
|
||||
String status) {}
|
||||
|
||||
/** Where the approver's browser goes next, and the correlator the instance is waiting on. */
|
||||
public record ApproveResponse(String callbackUrl, String nonce) {}
|
||||
|
||||
public record ClaimBody(String requestId, String claimSecret) {}
|
||||
|
||||
public record ClaimResponse(String deviceId, String deviceSecret, Long teamId) {}
|
||||
|
||||
/** Opens a handshake. */
|
||||
@PostMapping("/request")
|
||||
public ResponseEntity<?> request(
|
||||
@RequestBody(required = false) CreateBody body, HttpServletRequest http) {
|
||||
if (body == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "BAD_REQUEST"));
|
||||
}
|
||||
String deviceId = http.getHeader(HEADER_DEVICE_ID);
|
||||
String deviceSecret = http.getHeader(HEADER_DEVICE_SECRET);
|
||||
boolean reauthRequested = deviceId != null || deviceSecret != null;
|
||||
|
||||
ConnectRequestService.CreateResult result;
|
||||
if (reauthRequested) {
|
||||
Long pinnedTeamId =
|
||||
accountLinkService
|
||||
.resolveActiveInstance(deviceId, deviceSecret)
|
||||
.map(LinkedInstance::getTeamId)
|
||||
.orElse(null);
|
||||
result =
|
||||
service.createReauth(
|
||||
body.name(),
|
||||
body.callbackUrl(),
|
||||
body.nonce(),
|
||||
body.claimSecret(),
|
||||
clientIp(http),
|
||||
pinnedTeamId);
|
||||
} else {
|
||||
result =
|
||||
service.create(
|
||||
body.name(),
|
||||
body.callbackUrl(),
|
||||
body.nonce(),
|
||||
body.claimSecret(),
|
||||
clientIp(http));
|
||||
}
|
||||
if (result.isRejected()) {
|
||||
return switch (result.rejection()) {
|
||||
case RATE_LIMITED ->
|
||||
ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
|
||||
.body(Map.of("error", "RATE_LIMITED"));
|
||||
case BAD_CALLBACK ->
|
||||
ResponseEntity.badRequest().body(Map.of("error", "BAD_CALLBACK"));
|
||||
case BAD_NONCE -> ResponseEntity.badRequest().body(Map.of("error", "BAD_NONCE"));
|
||||
case BAD_SECRET -> ResponseEntity.badRequest().body(Map.of("error", "BAD_SECRET"));
|
||||
// A credential was offered and did not authenticate. Same answer as any other bad
|
||||
// credential, and deliberately not distinguishable from "revoked".
|
||||
case NOT_LINKED ->
|
||||
ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(Map.of("error", "NOT_LINKED"));
|
||||
};
|
||||
}
|
||||
return ResponseEntity.status(HttpStatus.CREATED)
|
||||
.body(
|
||||
new CreateResponse(
|
||||
result.requestId(),
|
||||
result.expiresInSeconds(),
|
||||
authorizeUrl(result.requestId(), http)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Where to send the admin to approve a handshake. {@code system.frontendUrl} is the web app's
|
||||
* own base URL, including any base path; without it the API's origin has to serve the app too.
|
||||
*/
|
||||
private String authorizeUrl(String requestId, HttpServletRequest http) {
|
||||
String frontendUrl = applicationProperties.getSystem().getFrontendUrl();
|
||||
String base =
|
||||
frontendUrl != null && !frontendUrl.isBlank()
|
||||
? frontendUrl.strip().replaceAll("/+$", "")
|
||||
: requestOrigin(http);
|
||||
return base
|
||||
+ LINK_PATH
|
||||
+ "?request="
|
||||
+ URLEncoder.encode(requestId, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/** Scheme, host and context path as the browser reached us, honouring a reverse proxy. */
|
||||
private static String requestOrigin(HttpServletRequest request) {
|
||||
String proto = firstHop(request.getHeader("X-Forwarded-Proto"));
|
||||
String host = firstHop(request.getHeader("X-Forwarded-Host"));
|
||||
String scheme = proto != null ? proto : request.getScheme();
|
||||
// A forwarded host already carries its own port, if it needs one.
|
||||
String hostPort =
|
||||
host != null
|
||||
? host
|
||||
: Origins.hostPort(
|
||||
scheme, request.getServerName(), request.getServerPort());
|
||||
String context = request.getContextPath() == null ? "" : request.getContextPath();
|
||||
return scheme + "://" + hostPort + context;
|
||||
}
|
||||
|
||||
private static String firstHop(String headerValue) {
|
||||
if (headerValue == null || headerValue.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String first = headerValue.split(",")[0].strip();
|
||||
return first.isEmpty() ? null : first;
|
||||
}
|
||||
|
||||
/** Detail for the approval page. */
|
||||
@GetMapping("/{requestId}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<ViewResponse> view(@PathVariable String requestId) {
|
||||
return service.lookup(requestId)
|
||||
.map(
|
||||
v ->
|
||||
ResponseEntity.ok(
|
||||
new ViewResponse(
|
||||
v.requestId(),
|
||||
v.name(),
|
||||
v.callbackOrigin(),
|
||||
v.insecureTransport(),
|
||||
v.mode().name(),
|
||||
v.status().name())))
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
/** Approves a handshake. */
|
||||
@PostMapping("/{requestId}/approve")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<?> approve(@PathVariable String requestId, Authentication auth) {
|
||||
Optional<ConnectRequestService.ConnectView> view = service.lookup(requestId);
|
||||
if (view.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
boolean reauth = view.get().mode() == ConnectRequest.Mode.REAUTH;
|
||||
LeaderTeam lt = reauth ? leaderTeams.resolveMember(auth) : leaderTeams.resolve(auth);
|
||||
if (lt.isError()) {
|
||||
return ResponseEntity.status(lt.error()).build();
|
||||
}
|
||||
ConnectRequestService.ApproveResult result =
|
||||
service.approve(requestId, lt.teamId(), lt.userId());
|
||||
if (result.isRejected()) {
|
||||
return switch (result.rejection()) {
|
||||
// Named separately so the page can say "you are signed in to a different account"
|
||||
// rather than implying the request itself was bad.
|
||||
case WRONG_TEAM ->
|
||||
ResponseEntity.status(HttpStatus.CONFLICT)
|
||||
.body(Map.of("error", "WRONG_TEAM"));
|
||||
case UNAVAILABLE -> ResponseEntity.notFound().build();
|
||||
};
|
||||
}
|
||||
return ResponseEntity.ok(
|
||||
new ApproveResponse(result.target().callbackUrl(), result.target().nonce()));
|
||||
}
|
||||
|
||||
@PostMapping("/{requestId}/deny")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<Void> deny(@PathVariable String requestId, Authentication auth) {
|
||||
LeaderTeam lt = leaderTeams.resolve(auth);
|
||||
if (lt.isError()) {
|
||||
return ResponseEntity.status(lt.error()).build();
|
||||
}
|
||||
return service.deny(requestId)
|
||||
? ResponseEntity.noContent().build()
|
||||
: ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
/** Collects the device credential. */
|
||||
@PostMapping("/claim")
|
||||
public ResponseEntity<?> claim(@RequestBody(required = false) ClaimBody body) {
|
||||
if (body == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "BAD_REQUEST"));
|
||||
}
|
||||
ConnectRequestService.ClaimResult result =
|
||||
service.claim(body.requestId(), body.claimSecret());
|
||||
return switch (result.outcome()) {
|
||||
case GRANTED ->
|
||||
ResponseEntity.ok(
|
||||
new ClaimResponse(
|
||||
result.deviceId(), result.deviceSecret(), result.teamId()));
|
||||
// A re-authentication carries no credential: the instance already has one. It only
|
||||
// needs to know the browser leg succeeded, and which team it was confirmed against.
|
||||
case CONFIRMED ->
|
||||
ResponseEntity.ok(Map.of("status", "confirmed", "teamId", result.teamId()));
|
||||
case PENDING ->
|
||||
ResponseEntity.status(HttpStatus.ACCEPTED).body(Map.of("status", "pending"));
|
||||
case REJECTED -> ResponseEntity.badRequest().body(Map.of("error", "CONNECT_REJECTED"));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Source address for the creation cap.
|
||||
*
|
||||
* <p>Deliberately not reading {@code X-Forwarded-For}: the caller sets it, so keying a cap on
|
||||
* it lets one rotate fake addresses and have no cap at all. {@code
|
||||
* server.forward-headers-strategy} is NATIVE, so the container has already resolved the real
|
||||
* client from trusted proxies.
|
||||
*/
|
||||
private static String clientIp(HttpServletRequest request) {
|
||||
String remote = request.getRemoteAddr();
|
||||
return remote == null || remote.length() <= 45 ? remote : remote.substring(0, 45);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package stirling.software.saas.accountlink;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/** One in-flight "connect this server" handshake. Short lived and single use. */
|
||||
@Entity
|
||||
@Table(
|
||||
name = "account_link_connect_request",
|
||||
indexes = @Index(name = "idx_alcr_ip_created", columnList = "requester_ip,created_at"))
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class ConnectRequest {
|
||||
|
||||
public enum Mode {
|
||||
LINK,
|
||||
REAUTH
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
PENDING,
|
||||
APPROVED,
|
||||
DENIED,
|
||||
CONSUMED
|
||||
}
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "request_id", nullable = false, unique = true, length = 64)
|
||||
private String requestId;
|
||||
|
||||
@Column(name = "name", length = 255)
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Read back from here on approval, never from the request: that is what stops an open redirect.
|
||||
*/
|
||||
@Column(name = "callback_url", nullable = false, length = 2048)
|
||||
private String callbackUrl;
|
||||
|
||||
@Column(name = "callback_origin", nullable = false, length = 255)
|
||||
private String callbackOrigin;
|
||||
|
||||
@Column(name = "nonce", nullable = false, length = 128)
|
||||
private String nonce;
|
||||
|
||||
/** SHA-256; the secret itself is never stored. */
|
||||
@Column(name = "claim_secret_hash", nullable = false, length = 64)
|
||||
private String claimSecretHash;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "mode", nullable = false, length = 16)
|
||||
private Mode mode = Mode.LINK;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 16)
|
||||
private Status status = Status.PENDING;
|
||||
|
||||
/** LINK: set on approval. REAUTH: pinned at creation, so approval can only confirm it. */
|
||||
@Column(name = "team_id")
|
||||
private Long teamId;
|
||||
|
||||
@Column(name = "approved_by_user_id")
|
||||
private Long approvedByUserId;
|
||||
|
||||
@Column(name = "requester_ip", length = 45)
|
||||
private String requesterIp;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "expires_at", nullable = false)
|
||||
private LocalDateTime expiresAt;
|
||||
|
||||
@Column(name = "approved_at")
|
||||
private LocalDateTime approvedAt;
|
||||
|
||||
@Column(name = "consumed_at")
|
||||
private LocalDateTime consumedAt;
|
||||
|
||||
public boolean isExpired(LocalDateTime now) {
|
||||
return expiresAt != null && expiresAt.isBefore(now);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package stirling.software.saas.accountlink;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Removes connect requests that are past use.
|
||||
*
|
||||
* <p>Needed rather than merely tidy: anyone can create a row on {@code POST /connect/request}, and
|
||||
* nothing else deletes one. Requests hold a callback URL and the requester's address, so they are
|
||||
* swept soon after expiry rather than kept.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Profile("saas")
|
||||
@ConditionalOnProperty(name = "stirling.billing.account-link.enabled", havingValue = "true")
|
||||
@RequiredArgsConstructor
|
||||
public class ConnectRequestCleanupService {
|
||||
|
||||
/** Long enough to answer "what happened to my link?" the next morning, and no longer. */
|
||||
private static final int RETAIN_HOURS = 24;
|
||||
|
||||
private final ConnectRequestRepository repo;
|
||||
|
||||
@Scheduled(cron = "0 30 3 * * *")
|
||||
@Transactional
|
||||
public void purgeExpired() {
|
||||
try {
|
||||
LocalDateTime cutoff = LocalDateTime.now().minusHours(RETAIN_HOURS);
|
||||
int deleted = repo.deleteByExpiresAtBefore(cutoff);
|
||||
if (deleted > 0) {
|
||||
log.info("Account-link connect: purged {} expired requests", deleted);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// A failed sweep must not take the scheduler down; the next run retries.
|
||||
log.error("Account-link connect: purge failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package stirling.software.saas.accountlink;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Lock;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import jakarta.persistence.LockModeType;
|
||||
|
||||
/** Data access for {@link ConnectRequest}. */
|
||||
public interface ConnectRequestRepository extends JpaRepository<ConnectRequest, Long> {
|
||||
|
||||
Optional<ConnectRequest> findByRequestId(String requestId);
|
||||
|
||||
/** Row-locking read used by approve, deny and claim. */
|
||||
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
||||
@Query("SELECT r FROM ConnectRequest r WHERE r.requestId = :requestId")
|
||||
Optional<ConnectRequest> findByRequestIdForUpdate(@Param("requestId") String requestId);
|
||||
|
||||
/** Backs the per-IP creation cap, since creating a request needs no authentication. */
|
||||
long countByRequesterIpAndCreatedAtAfter(String requesterIp, LocalDateTime after);
|
||||
|
||||
/** Sweeps rows past use, whatever they settled as. Anyone can create these. */
|
||||
int deleteByExpiresAtBefore(LocalDateTime cutoff);
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
package stirling.software.saas.accountlink;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/** The "connect this server" handshake, SaaS side. */
|
||||
@Slf4j
|
||||
@Service
|
||||
@Profile("saas")
|
||||
@ConditionalOnProperty(name = "stirling.billing.account-link.enabled", havingValue = "true")
|
||||
public class ConnectRequestService {
|
||||
|
||||
/**
|
||||
* Long enough for the approver to sign in, pick the right account and read the origin. Sized
|
||||
* for the slowest real route: signing up, waiting for a confirmation email, and coming back.
|
||||
*/
|
||||
static final int LIFETIME_MINUTES = 30;
|
||||
|
||||
/** Creating a request needs no authentication, so the only brake is per-source volume. */
|
||||
static final int MAX_REQUESTS_PER_IP = 10;
|
||||
|
||||
private static final int REQUEST_ID_BYTES = 32;
|
||||
private static final int MAX_NONCE_LENGTH = 128;
|
||||
private static final int MAX_CALLBACK_LENGTH = 2048;
|
||||
private static final int MAX_NAME_LENGTH = 255;
|
||||
|
||||
private final ConnectRequestRepository repo;
|
||||
private final AccountLinkService accountLinkService;
|
||||
private final SecureRandom random = new SecureRandom();
|
||||
|
||||
public ConnectRequestService(
|
||||
ConnectRequestRepository repo, AccountLinkService accountLinkService) {
|
||||
this.repo = repo;
|
||||
this.accountLinkService = accountLinkService;
|
||||
}
|
||||
|
||||
/** Rejected creation attempts, so the controller can pick a status without parsing messages. */
|
||||
public enum CreateRejection {
|
||||
BAD_CALLBACK,
|
||||
BAD_NONCE,
|
||||
BAD_SECRET,
|
||||
RATE_LIMITED,
|
||||
/**
|
||||
* A re-authentication was asked for by something that could not prove it is a linked
|
||||
* instance.
|
||||
*/
|
||||
NOT_LINKED
|
||||
}
|
||||
|
||||
/** Either a created request id, or the reason we would not create one. */
|
||||
public record CreateResult(String requestId, int expiresInSeconds, CreateRejection rejection) {
|
||||
static CreateResult ok(String requestId, int expiresInSeconds) {
|
||||
return new CreateResult(requestId, expiresInSeconds, null);
|
||||
}
|
||||
|
||||
static CreateResult rejected(CreateRejection rejection) {
|
||||
return new CreateResult(null, 0, rejection);
|
||||
}
|
||||
|
||||
public boolean isRejected() {
|
||||
return rejection != null;
|
||||
}
|
||||
}
|
||||
|
||||
/** What the approval page shows. */
|
||||
public record ConnectView(
|
||||
String requestId,
|
||||
String name,
|
||||
String callbackOrigin,
|
||||
boolean insecureTransport,
|
||||
ConnectRequest.Mode mode,
|
||||
ConnectRequest.Status status) {}
|
||||
|
||||
/** Where to send the browser once approved, plus the correlator the instance is expecting. */
|
||||
public record ApprovalTarget(String callbackUrl, String nonce) {}
|
||||
|
||||
public enum ClaimOutcome {
|
||||
/** Approved and collected; {@code credential} is populated. */
|
||||
GRANTED,
|
||||
/** A re-authentication was approved. */
|
||||
CONFIRMED,
|
||||
/** Still waiting on a human. */
|
||||
PENDING,
|
||||
/** Declined, expired, unknown, already collected, or a bad claim secret. */
|
||||
REJECTED
|
||||
}
|
||||
|
||||
public record ClaimResult(
|
||||
ClaimOutcome outcome, String deviceId, String deviceSecret, Long teamId) {
|
||||
static ClaimResult of(ClaimOutcome outcome) {
|
||||
return new ClaimResult(outcome, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
/** Records a handshake on behalf of an instance that has no credential yet. */
|
||||
@Transactional
|
||||
public CreateResult create(
|
||||
String name, String callbackUrl, String nonce, String claimSecret, String requesterIp) {
|
||||
return create(name, callbackUrl, nonce, claimSecret, requesterIp, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* As {@link #create}, but for an instance that is already linked and only needs its admin's
|
||||
* browser signed in again.
|
||||
*/
|
||||
@Transactional
|
||||
public CreateResult createReauth(
|
||||
String name,
|
||||
String callbackUrl,
|
||||
String nonce,
|
||||
String claimSecret,
|
||||
String requesterIp,
|
||||
Long pinnedTeamId) {
|
||||
if (pinnedTeamId == null) {
|
||||
return CreateResult.rejected(CreateRejection.NOT_LINKED);
|
||||
}
|
||||
return create(name, callbackUrl, nonce, claimSecret, requesterIp, pinnedTeamId);
|
||||
}
|
||||
|
||||
private CreateResult create(
|
||||
String name,
|
||||
String callbackUrl,
|
||||
String nonce,
|
||||
String claimSecret,
|
||||
String requesterIp,
|
||||
Long pinnedTeamId) {
|
||||
if (nonce == null || nonce.isBlank() || nonce.length() > MAX_NONCE_LENGTH) {
|
||||
return CreateResult.rejected(CreateRejection.BAD_NONCE);
|
||||
}
|
||||
if (claimSecret == null || claimSecret.isBlank()) {
|
||||
return CreateResult.rejected(CreateRejection.BAD_SECRET);
|
||||
}
|
||||
Optional<URI> parsed = validateCallback(callbackUrl);
|
||||
if (parsed.isEmpty()) {
|
||||
return CreateResult.rejected(CreateRejection.BAD_CALLBACK);
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (requesterIp != null
|
||||
&& repo.countByRequesterIpAndCreatedAtAfter(requesterIp, now.minusHours(1))
|
||||
>= MAX_REQUESTS_PER_IP) {
|
||||
return CreateResult.rejected(CreateRejection.RATE_LIMITED);
|
||||
}
|
||||
|
||||
URI uri = parsed.get();
|
||||
ConnectRequest request = new ConnectRequest();
|
||||
request.setRequestId(randomToken());
|
||||
request.setName(trim(name, MAX_NAME_LENGTH));
|
||||
request.setCallbackUrl(uri.toString());
|
||||
request.setCallbackOrigin(originOf(uri));
|
||||
request.setNonce(nonce);
|
||||
request.setClaimSecretHash(sha256Hex(claimSecret));
|
||||
request.setStatus(ConnectRequest.Status.PENDING);
|
||||
request.setMode(
|
||||
pinnedTeamId == null ? ConnectRequest.Mode.LINK : ConnectRequest.Mode.REAUTH);
|
||||
request.setTeamId(pinnedTeamId);
|
||||
request.setRequesterIp(requesterIp);
|
||||
request.setExpiresAt(now.plusMinutes(LIFETIME_MINUTES));
|
||||
repo.save(request);
|
||||
|
||||
// Never log the nonce or the claim secret; both are live. The request id is the safe
|
||||
// handle for correlating a support request against this row.
|
||||
log.info(
|
||||
"Account-link connect: request {} created for origin {}",
|
||||
request.getRequestId(),
|
||||
request.getCallbackOrigin());
|
||||
return CreateResult.ok(request.getRequestId(), LIFETIME_MINUTES * 60);
|
||||
}
|
||||
|
||||
/** The approver's view of a handshake. */
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<ConnectView> lookup(String requestId) {
|
||||
return repo.findByRequestId(requestId)
|
||||
.filter(r -> !r.isExpired(LocalDateTime.now()))
|
||||
.map(
|
||||
r ->
|
||||
new ConnectView(
|
||||
r.getRequestId(),
|
||||
r.getName(),
|
||||
r.getCallbackOrigin(),
|
||||
!"https".equals(schemeOf(r.getCallbackOrigin())),
|
||||
r.getMode(),
|
||||
r.getStatus()));
|
||||
}
|
||||
|
||||
/** Why an approval was refused, so the page can say something useful. */
|
||||
public enum ApproveRejection {
|
||||
/** Unknown, expired, or already settled. */
|
||||
UNAVAILABLE,
|
||||
/** The approver's team is not the team this server already belongs to. */
|
||||
WRONG_TEAM
|
||||
}
|
||||
|
||||
public record ApproveResult(ApprovalTarget target, ApproveRejection rejection) {
|
||||
public boolean isRejected() {
|
||||
return target == null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Binds a pending handshake to the approver's team and returns where to send them next. */
|
||||
@Transactional
|
||||
public ApproveResult approve(String requestId, Long teamId, Long userId) {
|
||||
Optional<ConnectRequest> found = repo.findByRequestIdForUpdate(requestId);
|
||||
if (found.isEmpty()) {
|
||||
return new ApproveResult(null, ApproveRejection.UNAVAILABLE);
|
||||
}
|
||||
ConnectRequest request = found.get();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (request.isExpired(now) || request.getStatus() != ConnectRequest.Status.PENDING) {
|
||||
return new ApproveResult(null, ApproveRejection.UNAVAILABLE);
|
||||
}
|
||||
Long pinned = request.getTeamId();
|
||||
if (pinned != null && !pinned.equals(teamId)) {
|
||||
log.warn(
|
||||
"Account-link connect: request {} approved by team {} but is pinned to team {};"
|
||||
+ " refusing",
|
||||
requestId,
|
||||
teamId,
|
||||
pinned);
|
||||
return new ApproveResult(null, ApproveRejection.WRONG_TEAM);
|
||||
}
|
||||
request.setStatus(ConnectRequest.Status.APPROVED);
|
||||
request.setTeamId(teamId);
|
||||
request.setApprovedByUserId(userId);
|
||||
request.setApprovedAt(now);
|
||||
repo.save(request);
|
||||
log.info(
|
||||
"Account-link connect: request {} approved for team {} ({})",
|
||||
requestId,
|
||||
teamId,
|
||||
request.getMode());
|
||||
return new ApproveResult(
|
||||
new ApprovalTarget(request.getCallbackUrl(), request.getNonce()), null);
|
||||
}
|
||||
|
||||
/** Declines a pending handshake. */
|
||||
@Transactional
|
||||
public boolean deny(String requestId) {
|
||||
Optional<ConnectRequest> found = repo.findByRequestIdForUpdate(requestId);
|
||||
if (found.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
ConnectRequest request = found.get();
|
||||
if (request.getStatus() != ConnectRequest.Status.PENDING) {
|
||||
return false;
|
||||
}
|
||||
request.setStatus(ConnectRequest.Status.DENIED);
|
||||
repo.save(request);
|
||||
log.info("Account-link connect: request {} denied", requestId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Collects the device credential for an approved handshake. */
|
||||
@Transactional
|
||||
public ClaimResult claim(String requestId, String claimSecret) {
|
||||
if (requestId == null || claimSecret == null) {
|
||||
return ClaimResult.of(ClaimOutcome.REJECTED);
|
||||
}
|
||||
Optional<ConnectRequest> found = repo.findByRequestIdForUpdate(requestId);
|
||||
if (found.isEmpty()) {
|
||||
return ClaimResult.of(ClaimOutcome.REJECTED);
|
||||
}
|
||||
ConnectRequest request = found.get();
|
||||
if (!secretMatches(claimSecret, request.getClaimSecretHash())) {
|
||||
// Same answer as an unknown id: a caller probing ids learns nothing from the
|
||||
// difference.
|
||||
log.warn("Account-link connect: claim for request {} had a bad secret", requestId);
|
||||
return ClaimResult.of(ClaimOutcome.REJECTED);
|
||||
}
|
||||
if (request.isExpired(LocalDateTime.now())) {
|
||||
return ClaimResult.of(ClaimOutcome.REJECTED);
|
||||
}
|
||||
return switch (request.getStatus()) {
|
||||
case PENDING -> ClaimResult.of(ClaimOutcome.PENDING);
|
||||
case APPROVED -> mint(request);
|
||||
case DENIED, CONSUMED -> ClaimResult.of(ClaimOutcome.REJECTED);
|
||||
};
|
||||
}
|
||||
|
||||
/** Settles an approved handshake. */
|
||||
private ClaimResult mint(ConnectRequest request) {
|
||||
if (request.getMode() == ConnectRequest.Mode.REAUTH) {
|
||||
request.setStatus(ConnectRequest.Status.CONSUMED);
|
||||
request.setConsumedAt(LocalDateTime.now());
|
||||
repo.save(request);
|
||||
log.info(
|
||||
"Account-link connect: request {} re-authenticated for team {}",
|
||||
request.getRequestId(),
|
||||
request.getTeamId());
|
||||
return new ClaimResult(ClaimOutcome.CONFIRMED, null, null, request.getTeamId());
|
||||
}
|
||||
AccountLinkService.RegisteredInstance registered =
|
||||
accountLinkService.register(
|
||||
request.getTeamId(), request.getApprovedByUserId(), request.getName());
|
||||
request.setStatus(ConnectRequest.Status.CONSUMED);
|
||||
request.setConsumedAt(LocalDateTime.now());
|
||||
repo.save(request);
|
||||
log.info(
|
||||
"Account-link connect: request {} claimed, instance {} bound to team {}",
|
||||
request.getRequestId(),
|
||||
registered.instanceId(),
|
||||
request.getTeamId());
|
||||
return new ClaimResult(
|
||||
ClaimOutcome.GRANTED,
|
||||
registered.deviceId(),
|
||||
registered.deviceSecret(),
|
||||
request.getTeamId());
|
||||
}
|
||||
|
||||
/** Absolute http(s) URL, with a host, no credentials and no fragment of its own. */
|
||||
static Optional<URI> validateCallback(String candidate) {
|
||||
if (candidate == null || candidate.isBlank() || candidate.length() > MAX_CALLBACK_LENGTH) {
|
||||
return Optional.empty();
|
||||
}
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(candidate.strip());
|
||||
} catch (URISyntaxException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (!uri.isAbsolute() || uri.getScheme() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String scheme = uri.getScheme().toLowerCase(Locale.ROOT);
|
||||
if (!"http".equals(scheme) && !"https".equals(scheme)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (uri.getHost() == null || uri.getHost().isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (uri.getUserInfo() != null || uri.getFragment() != null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(uri);
|
||||
}
|
||||
|
||||
/** Scheme, host and port, with the default port omitted so origins compare cleanly. */
|
||||
static String originOf(URI uri) {
|
||||
String scheme = uri.getScheme().toLowerCase(Locale.ROOT);
|
||||
return scheme + "://" + Origins.hostPort(scheme, uri.getHost(), uri.getPort());
|
||||
}
|
||||
|
||||
private static String schemeOf(String origin) {
|
||||
int sep = origin.indexOf("://");
|
||||
return sep < 0 ? "" : origin.substring(0, sep);
|
||||
}
|
||||
|
||||
private static String trim(String value, int max) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String stripped = value.strip();
|
||||
if (stripped.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return stripped.length() <= max ? stripped : stripped.substring(0, max);
|
||||
}
|
||||
|
||||
private String randomToken() {
|
||||
byte[] buf = new byte[REQUEST_ID_BYTES];
|
||||
random.nextBytes(buf);
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||
}
|
||||
|
||||
/** Constant-time comparison so a claim cannot be brute-forced a byte at a time. */
|
||||
private static boolean secretMatches(String candidate, String expectedHash) {
|
||||
if (expectedHash == null) {
|
||||
return false;
|
||||
}
|
||||
return MessageDigest.isEqual(
|
||||
sha256Hex(candidate).getBytes(StandardCharsets.UTF_8),
|
||||
expectedHash.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static String sha256Hex(String value) {
|
||||
return AccountLinkService.sha256Hex(value);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -19,7 +19,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Authenticates a linked self-hosted instance by its device credential (combined-billing "Mode A").
|
||||
* Authenticates a linked self-hosted instance by its device credential (combined billing).
|
||||
*
|
||||
* <p>Reads {@code X-Device-Id} + {@code X-Device-Secret}, looks up the active {@link
|
||||
* LinkedInstance}, and constant-time compares the SHA-256 of the presented secret against the
|
||||
|
||||
@@ -32,9 +32,9 @@ import stirling.software.saas.payg.policy.PricingPolicy;
|
||||
import stirling.software.saas.payg.policy.PricingPolicyService;
|
||||
|
||||
/**
|
||||
* Instance-facing surface (combined-billing "Mode A"), authenticated by the <b>device
|
||||
* credential</b> — not a user JWT. Separate path prefix ({@code /api/v1/instance/**}) so the device
|
||||
* credential is scoped here and nowhere else.
|
||||
* Instance-facing surface (combined billing), authenticated by the <b>device credential</b> — not a
|
||||
* user JWT. Separate path prefix ({@code /api/v1/instance/**}) so the device credential is scoped
|
||||
* here and nowhere else.
|
||||
*
|
||||
* <p>{@code GET /whoami} is the MVP round-trip proof: a registered instance presenting a valid
|
||||
* device credential gets back its resolved {@code instanceId} + {@code teamId}. {@code GET
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package stirling.software.saas.accountlink;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import stirling.software.common.model.enumeration.TeamRole;
|
||||
import stirling.software.proprietary.model.TeamMembership;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.repository.TeamMembershipRepository;
|
||||
import stirling.software.saas.util.AuthenticationUtils;
|
||||
|
||||
/** Who is allowed to bind a self-hosted instance to a team. */
|
||||
@Component
|
||||
@Profile("saas")
|
||||
@ConditionalOnProperty(name = "stirling.billing.account-link.enabled", havingValue = "true")
|
||||
public class LeaderTeamResolver {
|
||||
|
||||
private final TeamMembershipRepository memberRepo;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public LeaderTeamResolver(TeamMembershipRepository memberRepo, UserRepository userRepository) {
|
||||
this.memberRepo = memberRepo;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved caller, or an {@code error} status to return ({@code teamId}/{@code userId} null).
|
||||
*/
|
||||
public record LeaderTeam(Long teamId, Long userId, HttpStatus error) {
|
||||
public boolean isError() {
|
||||
return error != null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Caller must lead their team. */
|
||||
public LeaderTeam resolve(Authentication auth) {
|
||||
return resolve(auth, true);
|
||||
}
|
||||
|
||||
/** Caller need only belong to a team. */
|
||||
public LeaderTeam resolveMember(Authentication auth) {
|
||||
return resolve(auth, false);
|
||||
}
|
||||
|
||||
private LeaderTeam resolve(Authentication auth, boolean requireLeader) {
|
||||
User user;
|
||||
try {
|
||||
user = AuthenticationUtils.getCurrentUser(auth, userRepository);
|
||||
} catch (SecurityException e) {
|
||||
return new LeaderTeam(null, null, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
List<TeamMembership> rows = memberRepo.findPrimaryMembership(user.getId());
|
||||
if (rows.isEmpty()) {
|
||||
return new LeaderTeam(null, null, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
TeamMembership membership = rows.getFirst();
|
||||
if (requireLeader && membership.getRole() != TeamRole.LEADER) {
|
||||
return new LeaderTeam(null, null, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
return new LeaderTeam(membership.getTeam().getId(), user.getId(), null);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* One self-hosted instance that has linked a SaaS account (combined-billing "Mode A", {@code
|
||||
* One self-hosted instance that has linked a SaaS account (combined billing, {@code
|
||||
* linked_instance}, V22).
|
||||
*
|
||||
* <p>Created by {@code POST /api/v1/account-link/register}, authenticated with the admin's
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
|
||||
/**
|
||||
* Authentication for a linked self-hosted instance (combined-billing "Mode A").
|
||||
* Authentication for a linked self-hosted instance (combined billing).
|
||||
*
|
||||
* <p>Deliberately <em>not</em> a user: the principal is the instance ({@code instanceId}) bound to
|
||||
* a {@code teamId}, with the single authority {@code ROLE_LINKED_INSTANCE}. It carries no {@code
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package stirling.software.saas.accountlink;
|
||||
|
||||
/**
|
||||
* Origin formatting shared by the connect handshake.
|
||||
*
|
||||
* <p>One place on purpose: the origin a request arrives on and the origin parsed out of a callback
|
||||
* URL are compared with each other, so if either side stopped omitting the default port the
|
||||
* comparison would start failing quietly.
|
||||
*/
|
||||
final class Origins {
|
||||
|
||||
private Origins() {}
|
||||
|
||||
/** {@code host} or {@code host:port}, dropping a port that is the scheme's default. */
|
||||
static String hostPort(String scheme, String host, int port) {
|
||||
boolean isDefault =
|
||||
port <= 0
|
||||
|| ("http".equals(scheme) && port == 80)
|
||||
|| ("https".equals(scheme) && port == 443);
|
||||
return isDefault ? host : host + ":" + port;
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ public final class SaasSchemaOwnership {
|
||||
*/
|
||||
public static final Set<String> MIGRATION_OWNED =
|
||||
Set.of(
|
||||
"account_link_connect_request",
|
||||
"ai_create_sessions",
|
||||
"audit_events",
|
||||
"authorities",
|
||||
@@ -78,6 +79,7 @@ public final class SaasSchemaOwnership {
|
||||
*/
|
||||
public static final Set<String> HIBERNATE_MANAGED =
|
||||
Set.of(
|
||||
"account_link_connect_state",
|
||||
"account_link_device_credential",
|
||||
"account_link_metered_signature",
|
||||
"account_link_sync_state",
|
||||
|
||||
+6
-6
@@ -18,12 +18,12 @@ import stirling.software.saas.payg.model.ProcessType;
|
||||
import stirling.software.saas.payg.repository.PaygInstanceUsageRepository;
|
||||
|
||||
/**
|
||||
* Ingests a linked instance's daily usage sync (combined-billing "Mode A"). The instance reports a
|
||||
* monotonic cumulative unit total per {@link BillingCategory}; we bill only the delta since the
|
||||
* last sync via {@link JobChargeService#chargeStandalone} (reusing the in-cloud free-grant split,
|
||||
* ledger DEBIT, Stripe meter and idempotency). Idempotent (a resend → delta 0 → no charge) and
|
||||
* tamper-evident (a backwards total is refused; a monotonic {@code syncSeq} dedups replays). The
|
||||
* cap is enforced at the instance gate, not here. Gated behind {@code account-link.enabled}.
|
||||
* Ingests a linked instance's daily usage sync (combined billing). The instance reports a monotonic
|
||||
* cumulative unit total per {@link BillingCategory}; we bill only the delta since the last sync via
|
||||
* {@link JobChargeService#chargeStandalone} (reusing the in-cloud free-grant split, ledger DEBIT,
|
||||
* Stripe meter and idempotency). Idempotent (a resend → delta 0 → no charge) and tamper-evident (a
|
||||
* backwards total is refused; a monotonic {@code syncSeq} dedups replays). The cap is enforced at
|
||||
* the instance gate, not here. Gated behind {@code account-link.enabled}.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
|
||||
@@ -19,9 +19,9 @@ import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Last-seen cumulative usage a linked self-hosted instance has reported for one {@code (team,
|
||||
* billing period, category)} (combined-billing "Mode A"). The instance reports monotonic cumulative
|
||||
* unit totals on its daily sync; SaaS bills {@code reportedCumulative - lastCumulativeUnits} via
|
||||
* the standard charge path and advances this row. {@code lastSyncSeq} dedups replays.
|
||||
* billing period, category)} (combined billing). The instance reports monotonic cumulative unit
|
||||
* totals on its daily sync; SaaS bills {@code reportedCumulative - lastCumulativeUnits} via the
|
||||
* standard charge path and advances this row. {@code lastSyncSeq} dedups replays.
|
||||
*/
|
||||
@Entity
|
||||
@Table(
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.config.Customizer;
|
||||
@@ -71,6 +72,7 @@ public class SupabaseSecurityConfig {
|
||||
private final SaasTeamService saasTeamService;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final ApiKeyAuthenticationService apiKeyAuthenticationService;
|
||||
private final Environment environment;
|
||||
|
||||
@Value("${app.supabase.issuer:}")
|
||||
private String issuer;
|
||||
@@ -105,6 +107,17 @@ public class SupabaseSecurityConfig {
|
||||
.permitAll()
|
||||
.requestMatchers("/actuator/health", "/api/v1/config/**")
|
||||
.permitAll()
|
||||
// Account-link connect handshake: an instance calls these
|
||||
// before it holds any credential, so there is nothing to
|
||||
// authenticate with yet. Neither grants anything on its
|
||||
// own — /request records an intent a human must approve,
|
||||
// and /claim requires a secret only the instance that
|
||||
// created the request has ever held.
|
||||
.requestMatchers(
|
||||
HttpMethod.POST,
|
||||
"/api/v1/account-link/connect/request",
|
||||
"/api/v1/account-link/connect/claim")
|
||||
.permitAll()
|
||||
.requestMatchers(
|
||||
req ->
|
||||
RequestUriUtils.isStaticResource(
|
||||
@@ -144,7 +157,7 @@ public class SupabaseSecurityConfig {
|
||||
SupabaseSecurityConfig
|
||||
::toAuthentication)));
|
||||
|
||||
// Device-credential auth for linked self-hosted instances (combined-billing Mode A).
|
||||
// Device-credential auth for linked self-hosted instances (combined billing).
|
||||
// The filter bean exists only when stirling.billing.account-link.enabled=true; when off it
|
||||
// is absent here, so the instance surface cannot authenticate at all until release.
|
||||
DeviceCredentialAuthenticationFilter deviceFilter =
|
||||
@@ -268,6 +281,28 @@ public class SupabaseSecurityConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loopback on any port, as Spring origin patterns. Only added outside production; see {@link
|
||||
* #corsConfigurationSource()}.
|
||||
*/
|
||||
private static final List<String> LOOPBACK_ANY_PORT =
|
||||
List.of("http://localhost:[*]", "http://127.0.0.1:[*]");
|
||||
|
||||
/**
|
||||
* Profiles that mean "a developer's machine or a preview environment", never the production
|
||||
* deployment. Production runs the bare {@code saas} profile.
|
||||
*/
|
||||
private static final List<String> NON_PRODUCTION_PROFILES = List.of("dev", "staging", "local");
|
||||
|
||||
private boolean isNonProduction() {
|
||||
for (String profile : environment.getActiveProfiles()) {
|
||||
if (NON_PRODUCTION_PROFILES.contains(profile)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Bean
|
||||
CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration cfg = new CorsConfiguration();
|
||||
@@ -297,7 +332,23 @@ public class SupabaseSecurityConfig {
|
||||
origins.add(desktopOrigin);
|
||||
}
|
||||
}
|
||||
if (origins.stream().anyMatch(o -> o.contains("*"))) {
|
||||
// Outside production, allow loopback on ANY port. Several dev servers run side by side
|
||||
// (editor, saas web app, one per flavour under test) and their ports move, so pinning a
|
||||
// list means every new local environment shows up as an opaque CORS failure. Unlike a
|
||||
// wildcard subdomain, a wildcard port on loopback cannot be taken over: nothing but this
|
||||
// machine can answer on it, so there is no lapsed-DNS or abandoned-vhost risk. Absent in
|
||||
// production, where the profile check below is false.
|
||||
if (!operatorOverride && isNonProduction()) {
|
||||
origins.addAll(LOOPBACK_ANY_PORT);
|
||||
log.info(
|
||||
"Non-production profile active: allowing loopback CORS origins on any port {}",
|
||||
LOOPBACK_ANY_PORT);
|
||||
}
|
||||
// Loopback port wildcards are exempt: the warning below is about hostname takeover, which
|
||||
// does not apply to an origin only this machine can serve.
|
||||
if (origins.stream()
|
||||
.filter(o -> !LOOPBACK_ANY_PORT.contains(o))
|
||||
.anyMatch(o -> o.contains("*"))) {
|
||||
log.warn(
|
||||
"CORS origins contain a wildcard paired with allowCredentials=true: {}."
|
||||
+ " Wildcard subdomains can be taken over by an attacker (lapsed DNS,"
|
||||
|
||||
@@ -519,8 +519,8 @@ public class SaasTeamService {
|
||||
* membership and its wallet) rather than deleting it, so a plain team is never orphaned. The
|
||||
* only real hazard is a team the user is the <em>last</em> leader of that still carries live
|
||||
* billing: an active paid/PAYG subscription, or a non-revoked linked self-hosted instance
|
||||
* ("Mode A"). Those block the join until the plan is cancelled / leadership transferred /
|
||||
* instances revoked. An unpaid, unlinked team (personal or shared) no longer blocks.
|
||||
* (combined billing). Those block the join until the plan is cancelled / leadership transferred
|
||||
* / instances revoked. An unpaid, unlinked team (personal or shared) no longer blocks.
|
||||
*
|
||||
* <p>The home team and the team being joined are excluded: neither is left by the join (home is
|
||||
* parked, the joined team is kept), so their live billing cannot be stranded.
|
||||
|
||||
+23
-23
@@ -1,6 +1,7 @@
|
||||
package stirling.software.saas.accountlink;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -23,8 +24,7 @@ import stirling.software.proprietary.model.TeamMembership;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.repository.TeamMembershipRepository;
|
||||
import stirling.software.saas.accountlink.AccountLinkController.RegisterRequest;
|
||||
import stirling.software.saas.accountlink.AccountLinkController.RegisterResponse;
|
||||
import stirling.software.saas.accountlink.AccountLinkController.InstanceRow;
|
||||
import stirling.software.saas.util.AuthenticationUtils;
|
||||
|
||||
/**
|
||||
@@ -44,20 +44,27 @@ class AccountLinkControllerTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
controller = new AccountLinkController(service, memberRepo, userRepository);
|
||||
// Real resolver over the mocked repositories: the leader ladder moved into
|
||||
// LeaderTeamResolver, and these tests are still asserting that ladder's behaviour
|
||||
// through the controller.
|
||||
controller =
|
||||
new AccountLinkController(
|
||||
service, new LeaderTeamResolver(memberRepo, userRepository));
|
||||
auth =
|
||||
new AnonymousAuthenticationToken(
|
||||
"k", "anonymousUser", List.of(new SimpleGrantedAuthority("ROLE_USER")));
|
||||
}
|
||||
|
||||
// The leader ladder used to be asserted through POST /register, which has been removed along
|
||||
// with the JWT relay. It is exercised through /instances instead: same resolver, same rungs.
|
||||
|
||||
@Test
|
||||
void register_unauthenticated_returns401() {
|
||||
void list_unauthenticated_returns401() {
|
||||
try (var mocked = org.mockito.Mockito.mockStatic(AuthenticationUtils.class)) {
|
||||
mocked.when(() -> AuthenticationUtils.getCurrentUser(auth, userRepository))
|
||||
.thenThrow(new SecurityException("not authenticated"));
|
||||
|
||||
ResponseEntity<RegisterResponse> resp =
|
||||
controller.register(new RegisterRequest("host"), auth);
|
||||
ResponseEntity<List<InstanceRow>> resp = controller.list(auth);
|
||||
|
||||
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
|
||||
verifyNoInteractions(service);
|
||||
@@ -65,14 +72,14 @@ class AccountLinkControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void register_noMembership_returns403() {
|
||||
void list_noMembership_returns403() {
|
||||
User user = mockUser(42L);
|
||||
try (var mocked = org.mockito.Mockito.mockStatic(AuthenticationUtils.class)) {
|
||||
mocked.when(() -> AuthenticationUtils.getCurrentUser(auth, userRepository))
|
||||
.thenReturn(user);
|
||||
when(memberRepo.findPrimaryMembership(42L)).thenReturn(List.of());
|
||||
|
||||
ResponseEntity<RegisterResponse> resp = controller.register(null, auth);
|
||||
ResponseEntity<List<InstanceRow>> resp = controller.list(auth);
|
||||
|
||||
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
|
||||
verifyNoInteractions(service);
|
||||
@@ -80,7 +87,7 @@ class AccountLinkControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void register_nonLeader_returns403() {
|
||||
void list_nonLeader_returns403() {
|
||||
User user = mockUser(42L);
|
||||
TeamMembership member = membership(7L, TeamRole.MEMBER);
|
||||
try (var mocked = org.mockito.Mockito.mockStatic(AuthenticationUtils.class)) {
|
||||
@@ -88,7 +95,7 @@ class AccountLinkControllerTest {
|
||||
.thenReturn(user);
|
||||
when(memberRepo.findPrimaryMembership(42L)).thenReturn(List.of(member));
|
||||
|
||||
ResponseEntity<RegisterResponse> resp = controller.register(null, auth);
|
||||
ResponseEntity<List<InstanceRow>> resp = controller.list(auth);
|
||||
|
||||
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
|
||||
verifyNoInteractions(service);
|
||||
@@ -96,27 +103,20 @@ class AccountLinkControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void register_leader_mintsCredentialForCallerTeam() {
|
||||
void list_leader_readsOnlyTheCallersTeam() {
|
||||
User user = mockUser(42L);
|
||||
TeamMembership leader = membership(7L, TeamRole.LEADER);
|
||||
when(service.register(7L, 42L, "host"))
|
||||
.thenReturn(
|
||||
new AccountLinkService.RegisteredInstance(99L, "dev-x", "sec-x", "host"));
|
||||
when(service.list(7L)).thenReturn(List.of());
|
||||
try (var mocked = org.mockito.Mockito.mockStatic(AuthenticationUtils.class)) {
|
||||
mocked.when(() -> AuthenticationUtils.getCurrentUser(auth, userRepository))
|
||||
.thenReturn(user);
|
||||
when(memberRepo.findPrimaryMembership(42L)).thenReturn(List.of(leader));
|
||||
|
||||
ResponseEntity<RegisterResponse> resp =
|
||||
controller.register(new RegisterRequest("host"), auth);
|
||||
ResponseEntity<List<InstanceRow>> resp = controller.list(auth);
|
||||
|
||||
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
||||
RegisterResponse body = resp.getBody();
|
||||
assertThat(body).isNotNull();
|
||||
// Team comes from the caller's membership and is surfaced in the response.
|
||||
assertThat(body.teamId()).isEqualTo(7L);
|
||||
assertThat(body.instanceId()).isEqualTo(99L);
|
||||
assertThat(body.deviceSecret()).isEqualTo("sec-x");
|
||||
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
// The team comes from the caller's membership, never from the request.
|
||||
verify(service).list(7L);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package stirling.software.saas.accountlink;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.saas.accountlink.ConnectController.CreateBody;
|
||||
import stirling.software.saas.accountlink.ConnectController.CreateResponse;
|
||||
|
||||
/**
|
||||
* The authorize URL the instance is told to send its admin to. Everything else on this controller
|
||||
* delegates; this is the only decision it makes on its own.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class ConnectControllerTest {
|
||||
|
||||
private static final CreateBody BODY =
|
||||
new CreateBody("prod-1", "https://pdf.example.com/account-link/callback", "n", "s");
|
||||
|
||||
@Mock private ConnectRequestService service;
|
||||
@Mock private LeaderTeamResolver leaderTeams;
|
||||
@Mock private AccountLinkService accountLinkService;
|
||||
|
||||
private ApplicationProperties applicationProperties;
|
||||
private ConnectController controller;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
applicationProperties = new ApplicationProperties();
|
||||
controller =
|
||||
new ConnectController(
|
||||
service, leaderTeams, accountLinkService, applicationProperties);
|
||||
when(service.create(anyString(), anyString(), anyString(), anyString(), any()))
|
||||
.thenReturn(ConnectRequestService.CreateResult.ok("req-1", 1800));
|
||||
}
|
||||
|
||||
private String authorizeUrl(MockHttpServletRequest request) {
|
||||
Object body = controller.request(BODY, request).getBody();
|
||||
assertThat(body).isInstanceOf(CreateResponse.class);
|
||||
return ((CreateResponse) body).authorizeUrl();
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest request(String scheme, String host, int port) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setScheme(scheme);
|
||||
request.setServerName(host);
|
||||
request.setServerPort(port);
|
||||
return request;
|
||||
}
|
||||
|
||||
@Test
|
||||
void prefersTheConfiguredFrontendUrl() {
|
||||
applicationProperties.getSystem().setFrontendUrl("https://app.example.com/app/");
|
||||
|
||||
// Trailing slash trimmed, base path kept, and the API's own origin ignored.
|
||||
assertThat(authorizeUrl(request("https", "api.example.com", 443)))
|
||||
.isEqualTo("https://app.example.com/app/link?request=req-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToTheOriginTheApiWasReachedOn() {
|
||||
assertThat(authorizeUrl(request("https", "api.example.com", 443)))
|
||||
.isEqualTo("https://api.example.com/link?request=req-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsANonDefaultPortAndTheContextPath() {
|
||||
MockHttpServletRequest request = request("http", "localhost", 8081);
|
||||
request.setContextPath("/stirling");
|
||||
|
||||
assertThat(authorizeUrl(request))
|
||||
.isEqualTo("http://localhost:8081/stirling/link?request=req-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void honoursTheForwardedSchemeAndHost() {
|
||||
MockHttpServletRequest request = request("http", "10.0.0.5", 8080);
|
||||
request.addHeader("X-Forwarded-Proto", "https");
|
||||
request.addHeader("X-Forwarded-Host", "api.example.com");
|
||||
|
||||
assertThat(authorizeUrl(request)).isEqualTo("https://api.example.com/link?request=req-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void takesOnlyTheFirstForwardedHop() {
|
||||
MockHttpServletRequest request = request("http", "10.0.0.5", 8080);
|
||||
request.addHeader("X-Forwarded-Proto", "https, http");
|
||||
request.addHeader("X-Forwarded-Host", "api.example.com, evil.example.com");
|
||||
|
||||
assertThat(authorizeUrl(request)).isEqualTo("https://api.example.com/link?request=req-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void percentEncodesTheRequestId() {
|
||||
when(service.create(anyString(), anyString(), anyString(), anyString(), any()))
|
||||
.thenReturn(ConnectRequestService.CreateResult.ok("a b&c", 1800));
|
||||
|
||||
assertThat(authorizeUrl(request("https", "api.example.com", 443)))
|
||||
.isEqualTo("https://api.example.com/link?request=a+b%26c");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aBodylessRequestIsRejectedBeforeAnythingIsRecorded() {
|
||||
assertThat(controller.request(null, request("https", "api.example.com", 443)).getBody())
|
||||
.isEqualTo(java.util.Map.of("error", "BAD_REQUEST"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void offeringNoCredentialTakesTheFirstLinkPath() {
|
||||
authorizeUrl(request("https", "api.example.com", 443));
|
||||
|
||||
// createReauth is the credentialled path; a first link must not reach it.
|
||||
org.mockito.Mockito.verify(service, org.mockito.Mockito.never())
|
||||
.createReauth(anyString(), anyString(), anyString(), anyString(), any(), isNull());
|
||||
}
|
||||
}
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
package stirling.software.saas.accountlink;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import stirling.software.saas.accountlink.ConnectRequestService.ClaimOutcome;
|
||||
import stirling.software.saas.accountlink.ConnectRequestService.CreateRejection;
|
||||
|
||||
/**
|
||||
* Unit tests for the connect handshake's security properties, which are the reason this flow is
|
||||
* safe rather than an open redirect: the callback is validated once and then read back from
|
||||
* storage, the claim secret authenticates the collection, and one approval mints exactly one
|
||||
* credential.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class ConnectRequestServiceTest {
|
||||
|
||||
private static final String CALLBACK = "https://pdf.example.com/account-link/callback";
|
||||
private static final String NONCE = "nonce-value";
|
||||
private static final String CLAIM_SECRET = "claim-secret-value";
|
||||
|
||||
@Mock private ConnectRequestRepository repo;
|
||||
@Mock private AccountLinkService accountLinkService;
|
||||
|
||||
private ConnectRequestService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new ConnectRequestService(repo, accountLinkService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void create_storesTheValidatedCallbackAndItsOrigin() {
|
||||
ConnectRequestService.CreateResult result =
|
||||
service.create("prod-1", CALLBACK, NONCE, CLAIM_SECRET, "10.0.0.1");
|
||||
|
||||
assertThat(result.isRejected()).isFalse();
|
||||
assertThat(result.requestId()).isNotBlank();
|
||||
|
||||
ArgumentCaptor<ConnectRequest> saved = ArgumentCaptor.forClass(ConnectRequest.class);
|
||||
verify(repo).save(saved.capture());
|
||||
ConnectRequest row = saved.getValue();
|
||||
assertThat(row.getCallbackUrl()).isEqualTo(CALLBACK);
|
||||
assertThat(row.getCallbackOrigin()).isEqualTo("https://pdf.example.com");
|
||||
assertThat(row.getStatus()).isEqualTo(ConnectRequest.Status.PENDING);
|
||||
assertThat(row.getName()).isEqualTo("prod-1");
|
||||
// The claim secret is only ever stored as a hash.
|
||||
assertThat(row.getClaimSecretHash()).isNotEqualTo(CLAIM_SECRET).hasSize(64);
|
||||
}
|
||||
|
||||
@Test
|
||||
void create_keepsANonDefaultPortInTheOrigin() {
|
||||
service.create(
|
||||
null, "http://pdf.internal:8080/account-link/callback", NONCE, CLAIM_SECRET, null);
|
||||
|
||||
ArgumentCaptor<ConnectRequest> saved = ArgumentCaptor.forClass(ConnectRequest.class);
|
||||
verify(repo).save(saved.capture());
|
||||
assertThat(saved.getValue().getCallbackOrigin()).isEqualTo("http://pdf.internal:8080");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(
|
||||
strings = {
|
||||
"/account-link/callback", // not absolute
|
||||
"ftp://pdf.example.com/cb", // wrong scheme
|
||||
"javascript:alert(1)", // not a hierarchical http(s) URL
|
||||
"https://user:pw@pdf.example.com/cb", // credentials in the URL
|
||||
"https://pdf.example.com/cb#already", // would collide with our fragment
|
||||
"https:///cb" // no host
|
||||
})
|
||||
void create_refusesCallbacksWeWouldNotWantToRedirectTo(String callback) {
|
||||
ConnectRequestService.CreateResult result =
|
||||
service.create(null, callback, NONCE, CLAIM_SECRET, null);
|
||||
|
||||
assertThat(result.rejection()).isEqualTo(CreateRejection.BAD_CALLBACK);
|
||||
verify(repo, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void create_refusesAMissingNonce() {
|
||||
assertThat(service.create(null, CALLBACK, " ", CLAIM_SECRET, null).rejection())
|
||||
.isEqualTo(CreateRejection.BAD_NONCE);
|
||||
verify(repo, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void create_namesTheSecretWhenTheSecretIsWhatIsMissing() {
|
||||
assertThat(service.create(null, CALLBACK, NONCE, " ", null).rejection())
|
||||
.isEqualTo(CreateRejection.BAD_SECRET);
|
||||
verify(repo, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void create_isCappedPerSourceAddress() {
|
||||
when(repo.countByRequesterIpAndCreatedAtAfter(anyString(), any()))
|
||||
.thenReturn((long) ConnectRequestService.MAX_REQUESTS_PER_IP);
|
||||
|
||||
ConnectRequestService.CreateResult result =
|
||||
service.create(null, CALLBACK, NONCE, CLAIM_SECRET, "10.0.0.1");
|
||||
|
||||
assertThat(result.rejection()).isEqualTo(CreateRejection.RATE_LIMITED);
|
||||
verify(repo, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void lookup_flagsPlaintextTransportSoTheApproverCanSeeIt() {
|
||||
ConnectRequest row = pending();
|
||||
row.setCallbackOrigin("http://pdf.internal:8080");
|
||||
when(repo.findByRequestId("req")).thenReturn(Optional.of(row));
|
||||
|
||||
assertThat(service.lookup("req")).get().extracting("insecureTransport").isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lookup_hidesAnExpiredHandshake() {
|
||||
ConnectRequest row = pending();
|
||||
row.setExpiresAt(LocalDateTime.now().minusMinutes(1));
|
||||
when(repo.findByRequestId("req")).thenReturn(Optional.of(row));
|
||||
|
||||
assertThat(service.lookup("req")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void approve_bindsTheTeamAndReturnsTheStoredCallback() {
|
||||
ConnectRequest row = pending();
|
||||
when(repo.findByRequestIdForUpdate("req")).thenReturn(Optional.of(row));
|
||||
|
||||
ConnectRequestService.ApproveResult result = service.approve("req", 7L, 42L);
|
||||
|
||||
assertThat(result.isRejected()).isFalse();
|
||||
// The destination comes from the row, never from the caller.
|
||||
assertThat(result.target().callbackUrl()).isEqualTo(CALLBACK);
|
||||
assertThat(result.target().nonce()).isEqualTo(NONCE);
|
||||
assertThat(row.getStatus()).isEqualTo(ConnectRequest.Status.APPROVED);
|
||||
assertThat(row.getTeamId()).isEqualTo(7L);
|
||||
assertThat(row.getApprovedByUserId()).isEqualTo(42L);
|
||||
// Approval on its own must not mint anything.
|
||||
verifyNoInteractions(accountLinkService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void approve_isSingleUse() {
|
||||
ConnectRequest row = pending();
|
||||
row.setStatus(ConnectRequest.Status.APPROVED);
|
||||
when(repo.findByRequestIdForUpdate("req")).thenReturn(Optional.of(row));
|
||||
|
||||
assertThat(service.approve("req", 7L, 42L).isRejected()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void approve_refusesAnExpiredHandshake() {
|
||||
ConnectRequest row = pending();
|
||||
row.setExpiresAt(LocalDateTime.now().minusSeconds(1));
|
||||
when(repo.findByRequestIdForUpdate("req")).thenReturn(Optional.of(row));
|
||||
|
||||
assertThat(service.approve("req", 7L, 42L).isRejected()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createReauth_pinsTheTeamItWasToldByTheCredential() {
|
||||
ConnectRequestService.CreateResult result =
|
||||
service.createReauth(null, CALLBACK, NONCE, CLAIM_SECRET, null, 7L);
|
||||
|
||||
assertThat(result.isRejected()).isFalse();
|
||||
ArgumentCaptor<ConnectRequest> saved = ArgumentCaptor.forClass(ConnectRequest.class);
|
||||
verify(repo).save(saved.capture());
|
||||
assertThat(saved.getValue().getMode()).isEqualTo(ConnectRequest.Mode.REAUTH);
|
||||
assertThat(saved.getValue().getTeamId()).isEqualTo(7L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createReauth_withoutAnAuthenticatedInstanceIsRefused() {
|
||||
// The controller passes null when the offered device credential did not authenticate.
|
||||
assertThat(
|
||||
service.createReauth(null, CALLBACK, NONCE, CLAIM_SECRET, null, null)
|
||||
.rejection())
|
||||
.isEqualTo(CreateRejection.NOT_LINKED);
|
||||
verify(repo, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void create_leavesTheTeamOpenForAFirstLink() {
|
||||
service.create("n", CALLBACK, NONCE, CLAIM_SECRET, null);
|
||||
|
||||
ArgumentCaptor<ConnectRequest> saved = ArgumentCaptor.forClass(ConnectRequest.class);
|
||||
verify(repo).save(saved.capture());
|
||||
assertThat(saved.getValue().getMode()).isEqualTo(ConnectRequest.Mode.LINK);
|
||||
// Approval is what decides the team on a first link.
|
||||
assertThat(saved.getValue().getTeamId()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void approve_refusesAnApproverFromADifferentTeam() {
|
||||
ConnectRequest row = reauthPinnedTo(7L);
|
||||
when(repo.findByRequestIdForUpdate("req")).thenReturn(Optional.of(row));
|
||||
|
||||
ConnectRequestService.ApproveResult result = service.approve("req", 99L, 42L);
|
||||
|
||||
// This is the "signed in to the wrong account" case, and it must not silently rebind.
|
||||
assertThat(result.rejection()).isEqualTo(ConnectRequestService.ApproveRejection.WRONG_TEAM);
|
||||
assertThat(row.getStatus()).isEqualTo(ConnectRequest.Status.PENDING);
|
||||
assertThat(row.getTeamId()).isEqualTo(7L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void approve_acceptsTheTeamTheServerAlreadyBelongsTo() {
|
||||
ConnectRequest row = reauthPinnedTo(7L);
|
||||
when(repo.findByRequestIdForUpdate("req")).thenReturn(Optional.of(row));
|
||||
|
||||
assertThat(service.approve("req", 7L, 42L).isRejected()).isFalse();
|
||||
assertThat(row.getStatus()).isEqualTo(ConnectRequest.Status.APPROVED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void claim_onAReauthConfirmsWithoutMintingASecondCredential() {
|
||||
ConnectRequest row = reauthPinnedTo(7L);
|
||||
row.setStatus(ConnectRequest.Status.APPROVED);
|
||||
row.setApprovedByUserId(42L);
|
||||
when(repo.findByRequestIdForUpdate("req")).thenReturn(Optional.of(row));
|
||||
|
||||
ConnectRequestService.ClaimResult result = service.claim("req", CLAIM_SECRET);
|
||||
|
||||
assertThat(result.outcome()).isEqualTo(ClaimOutcome.CONFIRMED);
|
||||
assertThat(result.deviceId()).isNull();
|
||||
assertThat(result.deviceSecret()).isNull();
|
||||
assertThat(result.teamId()).isEqualTo(7L);
|
||||
assertThat(row.getStatus()).isEqualTo(ConnectRequest.Status.CONSUMED);
|
||||
// A second credential would orphan the one the instance already holds.
|
||||
verifyNoInteractions(accountLinkService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void claim_mintsOnceForAnApprovedHandshake() {
|
||||
ConnectRequest row = approved();
|
||||
when(repo.findByRequestIdForUpdate("req")).thenReturn(Optional.of(row));
|
||||
when(accountLinkService.register(anyLong(), anyLong(), any()))
|
||||
.thenReturn(
|
||||
new AccountLinkService.RegisteredInstance(9L, "dev-id", "dev-secret", "n"));
|
||||
|
||||
ConnectRequestService.ClaimResult result = service.claim("req", CLAIM_SECRET);
|
||||
|
||||
assertThat(result.outcome()).isEqualTo(ClaimOutcome.GRANTED);
|
||||
assertThat(result.deviceId()).isEqualTo("dev-id");
|
||||
assertThat(result.deviceSecret()).isEqualTo("dev-secret");
|
||||
assertThat(result.teamId()).isEqualTo(7L);
|
||||
assertThat(row.getStatus()).isEqualTo(ConnectRequest.Status.CONSUMED);
|
||||
verify(accountLinkService).register(7L, 42L, "n");
|
||||
}
|
||||
|
||||
@Test
|
||||
void claim_refusesASecondCollection() {
|
||||
ConnectRequest row = approved();
|
||||
row.setStatus(ConnectRequest.Status.CONSUMED);
|
||||
when(repo.findByRequestIdForUpdate("req")).thenReturn(Optional.of(row));
|
||||
|
||||
assertThat(service.claim("req", CLAIM_SECRET).outcome()).isEqualTo(ClaimOutcome.REJECTED);
|
||||
verifyNoInteractions(accountLinkService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void claim_withTheWrongSecretMintsNothing() {
|
||||
ConnectRequest row = approved();
|
||||
when(repo.findByRequestIdForUpdate("req")).thenReturn(Optional.of(row));
|
||||
|
||||
assertThat(service.claim("req", "not-the-secret").outcome())
|
||||
.isEqualTo(ClaimOutcome.REJECTED);
|
||||
assertThat(row.getStatus()).isEqualTo(ConnectRequest.Status.APPROVED);
|
||||
verifyNoInteractions(accountLinkService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void claim_beforeApprovalTellsTheInstanceToWait() {
|
||||
when(repo.findByRequestIdForUpdate("req")).thenReturn(Optional.of(pending()));
|
||||
|
||||
assertThat(service.claim("req", CLAIM_SECRET).outcome()).isEqualTo(ClaimOutcome.PENDING);
|
||||
verifyNoInteractions(accountLinkService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void claim_afterDenialIsTerminal() {
|
||||
ConnectRequest row = pending();
|
||||
row.setStatus(ConnectRequest.Status.DENIED);
|
||||
when(repo.findByRequestIdForUpdate("req")).thenReturn(Optional.of(row));
|
||||
|
||||
assertThat(service.claim("req", CLAIM_SECRET).outcome()).isEqualTo(ClaimOutcome.REJECTED);
|
||||
verifyNoInteractions(accountLinkService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void claim_onAnExpiredHandshakeMintsNothing() {
|
||||
ConnectRequest row = approved();
|
||||
row.setExpiresAt(LocalDateTime.now().minusSeconds(1));
|
||||
when(repo.findByRequestIdForUpdate("req")).thenReturn(Optional.of(row));
|
||||
|
||||
assertThat(service.claim("req", CLAIM_SECRET).outcome()).isEqualTo(ClaimOutcome.REJECTED);
|
||||
verifyNoInteractions(accountLinkService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void claim_forAnUnknownIdLooksTheSameAsABadSecret() {
|
||||
when(repo.findByRequestIdForUpdate("nope")).thenReturn(Optional.empty());
|
||||
|
||||
assertThat(service.claim("nope", CLAIM_SECRET).outcome()).isEqualTo(ClaimOutcome.REJECTED);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
private static ConnectRequest pending() {
|
||||
ConnectRequest row = new ConnectRequest();
|
||||
row.setRequestId("req");
|
||||
row.setName("n");
|
||||
row.setCallbackUrl(CALLBACK);
|
||||
row.setCallbackOrigin("https://pdf.example.com");
|
||||
row.setNonce(NONCE);
|
||||
row.setClaimSecretHash(AccountLinkService.sha256Hex(CLAIM_SECRET));
|
||||
row.setStatus(ConnectRequest.Status.PENDING);
|
||||
row.setExpiresAt(LocalDateTime.now().plusMinutes(10));
|
||||
return row;
|
||||
}
|
||||
|
||||
/** A re-authentication whose team came from the instance's credential, not from a browser. */
|
||||
private static ConnectRequest reauthPinnedTo(Long teamId) {
|
||||
ConnectRequest row = pending();
|
||||
row.setMode(ConnectRequest.Mode.REAUTH);
|
||||
row.setTeamId(teamId);
|
||||
return row;
|
||||
}
|
||||
|
||||
private static ConnectRequest approved() {
|
||||
ConnectRequest row = pending();
|
||||
row.setStatus(ConnectRequest.Status.APPROVED);
|
||||
row.setTeamId(7L);
|
||||
row.setApprovedByUserId(42L);
|
||||
row.setApprovedAt(LocalDateTime.now());
|
||||
return row;
|
||||
}
|
||||
}
|
||||
+53
-1
@@ -14,6 +14,8 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
@@ -48,13 +50,19 @@ class SupabaseSecurityConfigMoreTest {
|
||||
apiKeyAuthenticationService;
|
||||
|
||||
private SupabaseSecurityConfig config(ApplicationProperties props) {
|
||||
return config(props, new MockEnvironment());
|
||||
}
|
||||
|
||||
/** Loopback CORS origins are only added outside production, so the environment decides. */
|
||||
private SupabaseSecurityConfig config(ApplicationProperties props, Environment environment) {
|
||||
return new SupabaseSecurityConfig(
|
||||
userService,
|
||||
teamService,
|
||||
supabaseUserService,
|
||||
saasTeamService,
|
||||
props,
|
||||
apiKeyAuthenticationService);
|
||||
apiKeyAuthenticationService,
|
||||
environment);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@@ -204,6 +212,50 @@ class SupabaseSecurityConfigMoreTest {
|
||||
.hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("production does not allow loopback on arbitrary ports")
|
||||
void productionHasNoLoopbackWildcard() {
|
||||
CorsConfiguration cfg =
|
||||
cors(config(new ApplicationProperties()).corsConfigurationSource());
|
||||
|
||||
assertThat(cfg.getAllowedOriginPatterns())
|
||||
.doesNotContain("http://localhost:[*]", "http://127.0.0.1:[*]");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("non-production allows loopback on any port so dev servers can move")
|
||||
void devAllowsAnyLoopbackPort() {
|
||||
// Several dev servers run side by side and their ports change; pinning a list turns
|
||||
// every new local environment into an opaque CORS failure.
|
||||
MockEnvironment dev = new MockEnvironment();
|
||||
dev.setActiveProfiles("saas", "dev");
|
||||
|
||||
CorsConfiguration cfg =
|
||||
cors(config(new ApplicationProperties(), dev).corsConfigurationSource());
|
||||
|
||||
assertThat(cfg.getAllowedOriginPatterns())
|
||||
.contains("http://localhost:[*]", "http://127.0.0.1:[*]")
|
||||
// Still credentialed, which is the reason the pattern form matters.
|
||||
.contains("https://stirling.com");
|
||||
assertThat(cfg.getAllowCredentials()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("an operator origin list is respected verbatim even in dev")
|
||||
void operatorOverrideSuppressesLoopbackWildcard() {
|
||||
ApplicationProperties props = new ApplicationProperties();
|
||||
props.getSystem().setCorsAllowedOrigins(List.of("https://custom.example.com"));
|
||||
MockEnvironment dev = new MockEnvironment();
|
||||
dev.setActiveProfiles("saas", "dev");
|
||||
|
||||
CorsConfiguration cfg = cors(config(props, dev).corsConfigurationSource());
|
||||
|
||||
// An operator who set the list meant it; we do not widen it behind their back.
|
||||
assertThat(cfg.getAllowedOriginPatterns())
|
||||
.contains("https://custom.example.com")
|
||||
.doesNotContain("http://localhost:[*]");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("operator override replaces the default origin list")
|
||||
void operatorOverrideUsed() {
|
||||
|
||||
+10
-4
@@ -34,7 +34,7 @@ ext {
|
||||
commonsIoVersion = "2.22.0"
|
||||
commonsLang3 = "3.20.0"
|
||||
rhinoVersion = "1.9.1"
|
||||
okhttpBomVersion = "5.3.2"
|
||||
okhttpBomVersion = "5.4.0"
|
||||
gsonVersion = "2.14.0"
|
||||
guavaVersion = "33.6.0-jre"
|
||||
jinjavaVersion = "2.8.4"
|
||||
@@ -42,7 +42,7 @@ ext {
|
||||
bucket4jVersion = "8.19.0"
|
||||
archunitVersion = "1.4.2"
|
||||
batikVersion = "1.19"
|
||||
jpdfiumVersion = "1.0.4"
|
||||
jpdfiumVersion = "1.1.3"
|
||||
jwtVersion = "0.13.0"
|
||||
awsSdkVersion = "2.51.3"
|
||||
jschVersion = "2.28.6"
|
||||
@@ -265,7 +265,6 @@ subprojects {
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
implementation 'io.github.pixee:java-security-toolkit:1.2.3'
|
||||
|
||||
//tmp for security bumps
|
||||
implementation "ch.qos.logback:logback-core:$logback"
|
||||
@@ -307,7 +306,7 @@ subprojects {
|
||||
systemProperty 'apple.awt.UIElement', 'true'
|
||||
|
||||
testLogging {
|
||||
events "started", "failed"
|
||||
events "skipped", "failed"
|
||||
showExceptions = true
|
||||
showCauses = true
|
||||
showStackTraces = true
|
||||
@@ -543,6 +542,13 @@ subprojects {
|
||||
}
|
||||
}
|
||||
|
||||
// Lazy initialization defers bean creation until first use,
|
||||
// reducing dev-mode RSS significantly (heap drops ~40-60%).
|
||||
// Enable with: ./gradlew bootRun -PlazyInit=true
|
||||
if (rootProject.findProperty('lazyInit') == 'true') {
|
||||
runtimeArgs.add("-Dspring.main.lazy-initialization=true")
|
||||
logger.lifecycle("Lazy initialization enabled (-PlazyInit=true)")
|
||||
}
|
||||
jvmArgs = runtimeArgs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "خطأ"
|
||||
fileNotSavedToDisk = "لم يُحفَظ على القرص"
|
||||
fileSavedToDisk = "تم حفظ الملف على القرص"
|
||||
fileSelected = "المحدد: {{filename}}"
|
||||
filesSelected = "الملفات المحددة"
|
||||
filesSelected = "{{count}} ملفات"
|
||||
font = "الخط"
|
||||
fontSizeTooltip = "حجم نص رقم الصفحة بالنقاط. الأرقام الأكبر تنتج نصًا أكبر."
|
||||
fontTypeTooltip = "عائلة الخط لأرقام الصفحات. اختر بناءً على نمط مستندك."
|
||||
@@ -3969,6 +3969,7 @@ back = "رجوع"
|
||||
backToFolder = "الرجوع إلى {{folder}}"
|
||||
backToMyFiles = "الرجوع إلى ملفاتي"
|
||||
breadcrumbs = "مسار المجلد"
|
||||
bulkActions = "الإجراءات"
|
||||
cancel = "إلغاء"
|
||||
classification = "التصنيف"
|
||||
clearSelection = "مسح التحديد"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "الحجم الإجمالي"
|
||||
type = "النوع"
|
||||
versionHistory = "رحلة الإصدارات"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} عوامل تصفية نشطة"
|
||||
clearAll = "مسح عوامل التصفية"
|
||||
label = "عوامل التصفية"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "إلغاء"
|
||||
error = "تعذّر حفظ المجلد. حاول مرة أخرى."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "فرز الملفات"
|
||||
modifiedAsc = "الأقدم أولاً"
|
||||
modifiedDesc = "الأحدث أولاً"
|
||||
nameAsc = "الاسم A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "تم حفظ الكل"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "تعذّر تطبيق التغييرات"
|
||||
extractCsvError = "فشل استخراج CSV"
|
||||
extractXlsxError = "فشل استخراج XLSX"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "تسطيح بعد التعبئة"
|
||||
goToPage = "الانتقال إلى هذه الصفحة"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "الصفحة"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "مطلوب"
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "إعادة فحص الحقول"
|
||||
rescanFormFields = "إعادة فحص حقول النموذج"
|
||||
save = "حفظ"
|
||||
saveShortcut = "Ctrl+S للحفظ"
|
||||
skippedEdits_one = "تعذّر تطبيق تغيير واحد:"
|
||||
skippedEdits_other = "تعذّر تطبيق {{count}} تغييرات:"
|
||||
skippedEditsTruncated = "{{count}} إضافية غير مدرجة."
|
||||
unsavedChanges = "تغييرات غير محفوظة"
|
||||
|
||||
[formFill.create]
|
||||
commit = "إضافة {{count}} من الحقول إلى PDF"
|
||||
empty = "لم يتم رسم أي حقول بعد."
|
||||
failed = "فشل في إضافة الحقول"
|
||||
goToField = "الانتقال إلى هذا الحقل"
|
||||
hint = "اختر نوع الحقل، ثم ارسمه على الصفحة."
|
||||
placing = "ارسم حقل {{type}} على الصفحة. اضغط على Esc للتوقف."
|
||||
preview = "اضغط مطولًا للمعاينة"
|
||||
previewHelp = "اضغط مطولًا لرؤية الحقول كما ستبدو بعد إضافتها، دون حدود التحرير."
|
||||
removeField = "إزالة الحقل"
|
||||
|
||||
[formFill.editor]
|
||||
action = "إجراء الزر"
|
||||
actionHelp = "ما يفعله الزر عند النقر عليه."
|
||||
actionNone = "لا شيء"
|
||||
actionPrint = "طباعة"
|
||||
actionReset = "إعادة تعيين النموذج"
|
||||
actionSubmit = "إرسال إلى URL"
|
||||
actionUri = "فتح URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "العنوان الذي يفتحه الزر أو يرسل إليه."
|
||||
addOption = "إضافة خيار"
|
||||
caption = "نص الزر"
|
||||
captionHelp = "النص المطبوع على واجهة الزر."
|
||||
defaultValue = "القيمة الافتراضية"
|
||||
defaultValueHelp = "ما يحتويه الحقل قبل أن يملأه أي شخص. اتركه فارغًا لحقل فارغ."
|
||||
fontSize = "حجم الخط"
|
||||
fontSizeHelp = "حجم النص داخل الحقل. اتركه فارغًا للسماح للقارئ بتغيير حجمه ليناسب المساحة."
|
||||
label = "التسمية"
|
||||
labelHelp = "النص المعروض لمن يملأ النموذج. اتركه فارغًا للرجوع إلى اسم الحقل."
|
||||
maxLength = "الحد الأقصى للطول (comb)"
|
||||
maxLengthHelp = "يحدد عدد الأحرف التي يمكن احتواؤها، مع رسمها كمربعات متباعدة بالتساوي."
|
||||
multiline = "متعدد الأسطر"
|
||||
multilineHelp = "يسمح بأكثر من سطر واحد من النص ويلتف عند حافة الحقل."
|
||||
multiSelect = "السماح بتحديد متعدد"
|
||||
multiSelectHelp = "يسمح باختيار أكثر من خيار واحد في الوقت نفسه."
|
||||
name = "اسم الحقل"
|
||||
nameHelp = "الاسم الداخلي للحقل. يُستخدم عند تصدير البيانات أو ملء النموذج من نظام آخر، لذا اجعله فريدًا وخاليًا من المسافات."
|
||||
optionGap = "تباعد الخيارات"
|
||||
optionGapHelp = "المسافة بين الأزرار، بالنقاط. اتركها فارغة لتوزيعها بالتساوي على طول المربع."
|
||||
optionPlaceholder = "الخيار {{n}}"
|
||||
options = "الخيارات"
|
||||
optionsEmpty = "أضف خيارًا واحدًا على الأقل."
|
||||
optionsHelp = "الاختيارات المعروضة في القائمة. يتم تخزين كل اختيار كما كُتب، لذا اجعلها قصيرة ومميزة."
|
||||
optionSize = "حجم الخيار"
|
||||
optionSizeHelp = "عرض وارتفاع كل زر، بالنقاط. اتركه فارغًا لملاءمتها مع المربع الذي رسمته."
|
||||
readOnly = "للقراءة فقط"
|
||||
readOnlyHelp = "يعرض قيمة لكنه يمنع أي شخص من تعديلها."
|
||||
removeOption = "إزالة الخيار"
|
||||
required = "مطلوب"
|
||||
requiredHelp = "لا يمكن إرسال النموذج حتى يتم ملء هذا الحقل."
|
||||
signatureNote = "عنصر نائب فقط - لا توقّع هنا. إنه يحدد مكان وجود التوقيع حتى يضع موقّع PDF (Adobe Acrobat، خدمة توقيع، وما إلى ذلك) التوقيع في هذا الموضع عند توقيع المستند."
|
||||
tooltip = "تلميح"
|
||||
tooltipHelp = "التلميح المعروض عندما يمرر شخص المؤشر فوق الحقل في قارئ PDF."
|
||||
type = "النوع"
|
||||
typeHelp = "نوع هذا الحقل. يؤدي تغييره إلى إعادة إنشاء الحقل، لذا لن يتم نقل قيمته الحالية."
|
||||
|
||||
[formFill.mode]
|
||||
create = "إنشاء"
|
||||
fill = "ملء"
|
||||
label = "وضع محرر النماذج"
|
||||
modify = "تعديل"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "حفظ {{count}} من التغييرات"
|
||||
delete = "حذف"
|
||||
empty = "لا يحتوي ملف PDF هذا على أي حقول نموذج بعد."
|
||||
failed = "فشل في حفظ التغييرات"
|
||||
groupSizeHint = "استخدم حجم الخيار"
|
||||
hint = "حدد حقلًا لتعديل خصائصه، أو اسحبه على الصفحة، أو احذفه."
|
||||
restore = "استعادة"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "إغلاق الشريط الجانبي"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "تغيير قيود المستند وأذوناته"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "تغيير الأذونات"
|
||||
|
||||
[home.classify]
|
||||
desc = "تحديد نوع هذا المستند ووضع علامة عليه."
|
||||
title = "تصنيف"
|
||||
|
||||
[home.compare]
|
||||
desc = "يقارن ويظهر الاختلافات بين مستندين PDF"
|
||||
tags = "اختلاف"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "لا يوجد ما يمكن الإبلاغ عنه."
|
||||
handoffUnavailable = "لن يسمح هذا المتصفح للمعالج بتمرير المستند إلى المحرر. افتحه من المحرر بدلًا من ذلك."
|
||||
noDocumentLinked = "هذا الفشل غير مرتبط بمستند محدد، لذا لا يوجد ما يمكن فتحه هنا."
|
||||
notOnThisDevice = "هذا المستند غير موجود على هذا الجهاز، لذا لا يمكن فتحه هنا."
|
||||
occurrences = "{{count}} مرات"
|
||||
open = "الإشعارات"
|
||||
title = "الإشعارات"
|
||||
unread = "غير مقروء"
|
||||
|
||||
[notifications.action]
|
||||
failed = "لم ينجح ذلك. حاول مرة أخرى بعد قليل."
|
||||
unavailable = "غير متاح لهذا الإشعار."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "تم النسخ"
|
||||
copy = "نسخ الخطأ"
|
||||
less = "عرض أقل"
|
||||
more = "عرض الرسالة كاملة"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "سابقًا"
|
||||
new = "جديد"
|
||||
|
||||
[oauth.error]
|
||||
message = "لم تتم المصادقة بنجاح. يمكنك إغلاق هذه النافذة والمحاولة مجدداً."
|
||||
title = "فشلت المصادقة"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "من {{allowance}} ملفات PDF مجانية مستخدمة"
|
||||
capSuffix_other = "من {{allowance}} ملفات PDF مجانية مستخدمة"
|
||||
eyebrow = "تجربة Processor"
|
||||
statusLabel_one = "متبقٍ {{remaining}}"
|
||||
statusLabel_other = "متبقٍ {{remaining}}"
|
||||
statusLabel_one = "تم استخدام {{used}}"
|
||||
statusLabel_other = "تم استخدام {{used}}"
|
||||
sub = "استخدم محرر PDF مجانًا. ادفع لمعالجة ملفات PDF تلقائيًا."
|
||||
title_one = "عالِج {{allowance}} ملف PDF مجانًا"
|
||||
title_other = "عالِج {{allowance}} ملفات PDF مجانًا"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "عرض الملف"
|
||||
viewInProcessor = "عرض في المعالج"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "لم يتم تسجيل هذا الفشل مقابل مستند محدد، لذا لا يوجد شيء هنا لفتحه."
|
||||
unattended = "تم إدخال هذا الملف بواسطة مجلد أو حاوية أو webhook، لذا لا يحتفظ به متصفح أي شخص لفتحه."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "المصادر"
|
||||
connectSource = "توصيل مصدر"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "متقدم"
|
||||
back = "العودة إلى المصادر"
|
||||
backToSource = "العودة إلى إعداد المصدر"
|
||||
backToTypes = "كل أنواع المصادر"
|
||||
cancel = "إلغاء"
|
||||
chooseHint = "اختر من أين تأتي المستندات. الموصلات باللون الرمادي قادمة قريبًا."
|
||||
@@ -8726,7 +8845,6 @@ create = "إنشاء مصدر"
|
||||
createTitle = "اتصال بمصدر"
|
||||
delete = "حذف"
|
||||
editTitle = "تعديل المصدر"
|
||||
enabled = "مفعل"
|
||||
save = "حفظ التغييرات"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "استهلاك: معالجة كل ملف مرة واحدة"
|
||||
snapshot = "لقطة: إعادة قراءة المجلد في كل تشغيل"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "قم بتضمين المجلدات الفرعية إذا كانت ملفاتك منظمة داخل مجلدات متداخلة، أو راقب المستوى الأعلى فقط."
|
||||
label = "عمق المجلد"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "الحجم والتاريخ والتحقق من المحتوى"
|
||||
stat = "الحجم وتاريخ التعديل"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "ما إذا كان سيتم ترك الملف الأصلي في مكانه بعد معالجته. إذا تُرك الملف الأصلي في مكانه، فستتم إعادة معالجته في المرة التالية التي يفحص فيها مسار المعالجة المصدر."
|
||||
label = "وضع القراءة"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "استهلاك: معالجة كل ملف مرة واحدة"
|
||||
snapshot = "لقطة: إعادة قراءة المجلد في كل تشغيل"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "قم بتضمين المجلدات الفرعية إذا كانت ملفاتك منظمة داخل مجلدات متداخلة، أو راقب المستوى الأعلى فقط."
|
||||
label = "عمق المجلد"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "خادم Stirling-PDF الخاص بك غير متصل
|
||||
expired = "لقد انتهت جلستك. يرجى تحديث الصفحة والمحاولة مرة أخرى"
|
||||
|
||||
[settings]
|
||||
backToSections = "كل الإعدادات"
|
||||
close = "إغلاق"
|
||||
title = "الإعدادات"
|
||||
|
||||
[settings.ai]
|
||||
documents = "المستندات و RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "بحث"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "ابحث عن الأدوات..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "تنسيق متقدم"
|
||||
ai = "AI"
|
||||
automation = "أتمتة"
|
||||
developerTools = "أدوات المطوّر"
|
||||
documentReview = "مراجعة المستند"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "العمود {{index}}"
|
||||
convertToPdf = "التحويل إلى PDF"
|
||||
csvStats = "{{rows}} صفوف · {{columns}} أعمدة · {{size}}"
|
||||
emptyFile = "ملف فارغ"
|
||||
htmlHidePreview = "إخفاء المعاينة"
|
||||
htmlPreview = "معاينة HTML"
|
||||
htmlPreviewMobileHidden = "تم تصميم صفحات HTML لعرض سطح المكتب، لذلك تكون المعاينة معطلة افتراضيًا هنا."
|
||||
htmlPreviewWarning = "معاينة HTML — قد لا يتم تحميل الموارد الخارجية · {{size}}"
|
||||
htmlShowPreview = "إظهار المعاينة على أي حال"
|
||||
invalidJson = "JSON غير صالح — عرض المحتوى الخام"
|
||||
lineNumbers = "أرقام الأسطر"
|
||||
loading = "جارٍ التحميل..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "تصدير PDF"
|
||||
exportSelected = "تصدير الصفحات المحددة"
|
||||
formFill = "تعبئة النموذج"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "مزيد من الإجراءات"
|
||||
multiTool = "الأداة المتعددة"
|
||||
panMode = "وضع التحريك"
|
||||
print = "طباعة PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "تحديد الكل"
|
||||
selectByNumber = "تحديد حسب أرقام الصفحات"
|
||||
selectLanguage = "اختر اللغة"
|
||||
share = "مشاركة"
|
||||
showAllTools = "إظهار كل الأدوات"
|
||||
showFewerTools = "طي شريط الأدوات"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "تبديل ظهور الشروح"
|
||||
toggleAttachments = "تبديل عرض المرفقات"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Yanlış"
|
||||
fileNotSavedToDisk = "Diskə yadda saxlanılmadı"
|
||||
fileSavedToDisk = "Fayl diskə yadda saxlanıldı"
|
||||
fileSelected = "Seçilən: {{filename}}"
|
||||
filesSelected = "seçilmiş fayllar"
|
||||
filesSelected = "{{count}} fayl"
|
||||
font = "Şrift"
|
||||
fontSizeTooltip = "Səhifə nömrəsi mətninin ölçüsü (pt). Daha böyük rəqəmlər daha böyük mətn yaradır."
|
||||
fontTypeTooltip = "Səhifə nömrələri üçün şrift ailəsi. Sənəd üslubunuza uyğun seçin."
|
||||
@@ -3969,6 +3969,7 @@ back = "Geri"
|
||||
backToFolder = "{{folder}} qovluğuna geri"
|
||||
backToMyFiles = "Mənim Fayllarıma geri"
|
||||
breadcrumbs = "Qovluq yolu"
|
||||
bulkActions = "Əməliyyatlar"
|
||||
cancel = "Ləğv et"
|
||||
classification = "Təsnifat"
|
||||
clearSelection = "Seçimi təmizlə"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Ümumi ölçü"
|
||||
type = "Növ"
|
||||
versionHistory = "Versiya yolu"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} filtr aktivdir"
|
||||
clearAll = "Filtrləri təmizlə"
|
||||
label = "Filtrlər"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Ləğv et"
|
||||
error = "Qovluğu saxlamaq mümkün olmadı. Yenidən cəhd edin."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Faylları sırala"
|
||||
modifiedAsc = "Ən köhnə əvvəlcə"
|
||||
modifiedDesc = "Ən yenilər əvvəlcə"
|
||||
nameAsc = "Ad A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Hamısı saxlanıldı"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Dəyişiklikləri tətbiq etmək mümkün olmadı"
|
||||
extractCsvError = "CSV çıxarmaq alınmadı"
|
||||
extractXlsxError = "XLSX çıxarmaq alınmadı"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Doldurduqdan sonra yastılaşdır"
|
||||
goToPage = "Bu səhifəyə keç"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Səhifə"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "təl."
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Sahələri yenidən skan et"
|
||||
rescanFormFields = "Forma sahələrini yenidən skan et"
|
||||
save = "Saxla"
|
||||
saveShortcut = "Saxlamaq üçün Ctrl+S"
|
||||
skippedEdits_one = "1 dəyişiklik tətbiq edilə bilmədi:"
|
||||
skippedEdits_other = "{{count}} dəyişiklik tətbiq edilə bilmədi:"
|
||||
skippedEditsTruncated = "{{count}} əlavə qeyd göstərilmir."
|
||||
unsavedChanges = "Saxlanılmamış dəyişikliklər"
|
||||
|
||||
[formFill.create]
|
||||
commit = "PDF-ə {{count}} sahə əlavə et"
|
||||
empty = "Hələ heç bir sahə çəkilməyib."
|
||||
failed = "Sahələri əlavə etmək alınmadı"
|
||||
goToField = "Bu sahəyə keç"
|
||||
hint = "Sahə növünü seçin, sonra onu səhifədə çəkin."
|
||||
placing = "Səhifədə {{type}} sahəsi çəkin. Dayandırmaq üçün Esc düyməsini basın."
|
||||
preview = "Önizləmə üçün basılı saxlayın"
|
||||
previewHelp = "Sahələrin əlavə edildikdən sonra necə görünəcəyini redaktə konturları olmadan görmək üçün basılı saxlayın."
|
||||
removeField = "Sahəni sil"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Düymə əməliyyatı"
|
||||
actionHelp = "Düyməyə kliklənəndə nə baş verəcəyi."
|
||||
actionNone = "Heç nə"
|
||||
actionPrint = "Çap et"
|
||||
actionReset = "Formanı sıfırla"
|
||||
actionSubmit = "URL-ə göndər"
|
||||
actionUri = "URL aç"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "Düymənin açdığı və ya göndərdiyi ünvan."
|
||||
addOption = "Seçim əlavə et"
|
||||
caption = "Düymə başlığı"
|
||||
captionHelp = "Düymənin üzərində göstərilən mətn."
|
||||
defaultValue = "Standart dəyər"
|
||||
defaultValueHelp = "Kimsə doldurmazdan əvvəl sahədə olan məzmun. Boş sahə üçün boş buraxın."
|
||||
fontSize = "Şrift ölçüsü"
|
||||
fontSizeHelp = "Sahə daxilində mətn ölçüsü. Oxuyucunun uyğunlaşdırmasına imkan vermək üçün boş buraxın."
|
||||
label = "Etiket"
|
||||
labelHelp = "Formanı dolduran şəxsə göstərilən mətn. Sahə adına qayıtmaq üçün boş buraxın."
|
||||
maxLength = "Maks. uzunluq (comb)"
|
||||
maxLengthHelp = "Neçə simvolun sığacağını məhdudlaşdırır, bərabər aralıqlı xanalar kimi çəkilir."
|
||||
multiline = "Çoxsətirli"
|
||||
multilineHelp = "Birdən çox mətn sətrinə icazə verir və sahənin kənarında sətirə keçirir."
|
||||
multiSelect = "Çoxlu seçimə icazə ver"
|
||||
multiSelectHelp = "Eyni anda birdən çox seçimin seçilməsinə imkan verir."
|
||||
name = "Sahə adı"
|
||||
nameHelp = "Sahənin daxili adı. Məlumat ixrac edilərkən və ya forma başqa sistemdən doldurularkən istifadə olunur, buna görə unikal və boşluqsuz saxlayın."
|
||||
optionGap = "Seçim aralığı"
|
||||
optionGapHelp = "Düymələr arasındakı məsafə, punktla. Onları qutuda bərabər yaymaq üçün boş buraxın."
|
||||
optionPlaceholder = "Seçim {{n}}"
|
||||
options = "Seçimlər"
|
||||
optionsEmpty = "Ən azı bir seçim əlavə edin."
|
||||
optionsHelp = "Siyahıda təqdim olunan seçimlər. Hər biri yazıldığı kimi saxlanılır, buna görə qısa və fərqli saxlayın."
|
||||
optionSize = "Seçim ölçüsü"
|
||||
optionSizeHelp = "Hər düymənin eni və hündürlüyü, punktla. Çəkdiyiniz qutuya uyğunlaşdırmaq üçün boş buraxın."
|
||||
readOnly = "Yalnız oxuma"
|
||||
readOnlyHelp = "Dəyəri göstərir, lakin heç kimin onu redaktə etməsinə imkan vermir."
|
||||
removeOption = "Seçimi sil"
|
||||
required = "Məcburi"
|
||||
requiredHelp = "Bu sahə doldurulmayana qədər forma göndərilə bilməz."
|
||||
signatureNote = "Yalnız yer tutucudur - burada imza atmırsınız. Bu, imzanın harada yerləşməli olduğunu göstərir ki, sənəd imzalananda PDF imzalayıcı (Adobe Acrobat, imzalama xidməti və s.) imzanı bu yerə yerləşdirsin."
|
||||
tooltip = "İpucu"
|
||||
tooltipHelp = "Kimsə PDF oxuyucusunda sahənin üzərinə gəldikdə göstərilən ipucu."
|
||||
type = "Növ"
|
||||
typeHelp = "Bu sahənin hansı növdə olduğunu göstərir. Dəyişdirildikdə sahə yenidən qurulur, buna görə cari dəyəri saxlanılmır."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Yarat"
|
||||
fill = "Doldur"
|
||||
label = "Forma redaktoru rejimi"
|
||||
modify = "Dəyişdir"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "{{count}} dəyişikliyi saxla"
|
||||
delete = "Sil"
|
||||
empty = "Bu PDF-də hələ forma sahələri yoxdur."
|
||||
failed = "Dəyişiklikləri saxlamaq alınmadı"
|
||||
groupSizeHint = "Seçim ölçüsündən istifadə edin"
|
||||
hint = "Xüsusiyyətlərini redaktə etmək üçün sahə seçin, onu səhifədə sürükləyin və ya silin."
|
||||
restore = "Bərpa et"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Yan paneli bağla"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Sənəd məhdudiyyətlərini və icazələrini dəyişin"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "İcazələri Dəyişdir"
|
||||
|
||||
[home.classify]
|
||||
desc = "Bunun hansı növ sənəd olduğunu müəyyənləşdirin və etiketləyin."
|
||||
title = "Təsnif et"
|
||||
|
||||
[home.compare]
|
||||
desc = "2 PDF Sənədini müqayisə edir və fərqləri göstərir"
|
||||
tags = "fərq"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Bildiriləcək heç nə yoxdur."
|
||||
handoffUnavailable = "Bu brauzer emalçıya sənədi redaktora ötürməyə imkan verməyəcək. Bunun əvəzinə onu redaktordan açın."
|
||||
noDocumentLinked = "Bu xəta konkret sənədlə əlaqəli deyil, buna görə burada açılacaq heç nə yoxdur."
|
||||
notOnThisDevice = "Bu sənəd bu cihazda deyil, buna görə burada açıla bilməz."
|
||||
occurrences = "{{count}} dəfə"
|
||||
open = "Bildirişlər"
|
||||
title = "Bildirişlər"
|
||||
unread = "Oxunmamış"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Bu alınmadı. Bir azdan yenidən cəhd edin."
|
||||
unavailable = "Bu bildiriş üçün əlçatan deyil."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Kopyalandı"
|
||||
copy = "Xətanı kopyala"
|
||||
less = "Daha az göstər"
|
||||
more = "Tam mesajı göstər"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Əvvəlkilər"
|
||||
new = "Yeni"
|
||||
|
||||
[oauth.error]
|
||||
message = "Təsdiqləmə uğurlu olmadı. Bu pəncərəni bağlayıb yenidən cəhd edə bilərsiniz."
|
||||
title = "Təsdiqləmə uğursuz oldu"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "{{allowance}} pulsuz PDF-dən istifadə olunub"
|
||||
capSuffix_other = "{{allowance}} pulsuz PDF-dən istifadə olunub"
|
||||
eyebrow = "Processor sınağı"
|
||||
statusLabel_one = "{{remaining}} qalıb"
|
||||
statusLabel_other = "{{remaining}} qalıb"
|
||||
statusLabel_one = "{{used}} istifadə olunub"
|
||||
statusLabel_other = "{{used}} istifadə olunub"
|
||||
sub = "PDF Editor-dan pulsuz istifadə edin. PDF-ləri avtomatik emal etmək üçün ödəyin."
|
||||
title_one = "{{allowance}} PDF-i pulsuz emal edin"
|
||||
title_other = "{{allowance}} PDF-i pulsuz emal edin"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Fayla bax"
|
||||
viewInProcessor = "Emalçıda bax"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Bu xəta konkret sənədə aid qeydə alınmayıb, buna görə burada açılacaq heç nə yoxdur."
|
||||
unattended = "Bu fayl qovluq, bucket və ya webhook tərəfindən ötürülüb, buna görə onu açmaq üçün heç kimin brauzerində saxlanılmır."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Mənbələr"
|
||||
connectSource = "Mənbə qoş"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Qabaqcıl"
|
||||
back = "Mənbələrə qayıt"
|
||||
backToSource = "Mənbə qurulumuna qayıt"
|
||||
backToTypes = "Bütün mənbə növləri"
|
||||
cancel = "Ləğv et"
|
||||
chooseHint = "Sənədlərin haradan gələcəyini seçin. Bozlaşdırılmış connector-lar yoldadır."
|
||||
@@ -8726,7 +8845,6 @@ create = "Mənbə yarat"
|
||||
createTitle = "Mənbə qoş"
|
||||
delete = "Sil"
|
||||
editTitle = "Mənbəni redaktə et"
|
||||
enabled = "Aktivləşdirilib"
|
||||
save = "Dəyişiklikləri saxla"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: hər faylı bir dəfə emal et"
|
||||
snapshot = "Snapshot: hər icrada qovluğu yenidən oxu"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Fayllarınız iç-içə qovluqlarda təşkil olunubsa alt qovluqları daxil edin, ya da yalnız üst səviyyəni izləyin."
|
||||
label = "Qovluq dərinliyi"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Ölçü, tarix və məzmun yoxlaması"
|
||||
stat = "Ölçü və dəyişdirilmə tarixi"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Emal edildikdən sonra orijinal faylın yerində saxlanılıb-saxlanılmayacağı. Orijinal fayl yerində saxlanılarsa, pipeline növbəti dəfə mənbəni skan etdikdə yenidən emal olunacaq."
|
||||
label = "Oxuma rejimi"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "İstehlak: hər faylı bir dəfə emal et"
|
||||
snapshot = "Snapshot: hər işə salmada qovluğu yenidən oxu"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Fayllarınız iç-içə qovluqlarda təşkil olunubsa alt qovluqları daxil edin, ya da yalnız üst səviyyəni izləyin."
|
||||
label = "Qovluq dərinliyi"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Sizin Stirling-PDF serveriniz oflayndır və \"{{endp
|
||||
expired = "Sessiyanızın vaxtı bitdi. Səhifəni yeniləyin və yenidən cəhd edin."
|
||||
|
||||
[settings]
|
||||
backToSections = "Bütün ayarlar"
|
||||
close = "Bağla"
|
||||
title = "Ayarlar"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Sənədlər və RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Axtar"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Alətlərdə axtar..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Qabaqcıl Formatlama"
|
||||
ai = "AI"
|
||||
automation = "Avtomatlaşdırma"
|
||||
developerTools = "Tərtibatçı Alətləri"
|
||||
documentReview = "Sənəd Baxışı"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Sütun {{index}}"
|
||||
convertToPdf = "PDF-ə çevir"
|
||||
csvStats = "{{rows}} sətir · {{columns}} sütun · {{size}}"
|
||||
emptyFile = "Boş fayl"
|
||||
htmlHidePreview = "Önizləməni gizlət"
|
||||
htmlPreview = "HTML ön baxış"
|
||||
htmlPreviewMobileHidden = "HTML səhifələri masaüstü enləri üçün tərtib edilib, buna görə burada önizləmə standart olaraq söndürülüb."
|
||||
htmlPreviewWarning = "HTML ön baxış — xarici resurslar yüklənməyə bilər · {{size}}"
|
||||
htmlShowPreview = "Yenə də önizləməni göstər"
|
||||
invalidJson = "Etibarsız JSON — xam məzmun göstərilir"
|
||||
lineNumbers = "Sətir nömrələri"
|
||||
loading = "Yüklənir..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "PDF-i ixrac et"
|
||||
exportSelected = "Seçilmiş səhifələri ixrac et"
|
||||
formFill = "Formu doldur"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Daha çox əməliyyat"
|
||||
multiTool = "Çoxfunksiyalı alət"
|
||||
panMode = "Sürüşdürmə rejimi"
|
||||
print = "PDF-i çap et"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Hamısını seç"
|
||||
selectByNumber = "Səhifə nömrələrinə görə seç"
|
||||
selectLanguage = "Dili seçin"
|
||||
share = "Paylaş"
|
||||
showAllTools = "Bütün alətləri göstər"
|
||||
showFewerTools = "Alətlər panelini yığ"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Annotasiyaların görünməsini dəyiş"
|
||||
toggleAttachments = "Qoşmaları aç/bağla"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Невярно"
|
||||
fileNotSavedToDisk = "Не е записан на диска"
|
||||
fileSavedToDisk = "Файлът е записан на диска"
|
||||
fileSelected = "Избрано: {{filename}}"
|
||||
filesSelected = "избрани файлове"
|
||||
filesSelected = "{{count}} файла"
|
||||
font = "Шрифт"
|
||||
fontSizeTooltip = "Размер на текста за номера на страници в точки. По-големите числа правят по-голям текст."
|
||||
fontTypeTooltip = "Семейство шрифтове за номерата на страниците. Изберете спрямо стила на документа."
|
||||
@@ -3969,6 +3969,7 @@ back = "Назад"
|
||||
backToFolder = "Назад към {{folder}}"
|
||||
backToMyFiles = "Назад към Моите файлове"
|
||||
breadcrumbs = "Път на папката"
|
||||
bulkActions = "Действия"
|
||||
cancel = "Отказ"
|
||||
classification = "Класификация"
|
||||
clearSelection = "Изчистете избора"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Общ размер"
|
||||
type = "Тип"
|
||||
versionHistory = "Път на версиите"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} активни филтъра"
|
||||
clearAll = "Изчистване на филтрите"
|
||||
label = "Филтри"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Отказ"
|
||||
error = "Папката не можа да бъде записана. Опитайте отново."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Сортиране на файлове"
|
||||
modifiedAsc = "Най-старите първо"
|
||||
modifiedDesc = "Най-новите първо"
|
||||
nameAsc = "Име A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Всичко е запазено"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Промените не можаха да бъдат приложени"
|
||||
extractCsvError = "Неуспешно извличане на CSV"
|
||||
extractXlsxError = "Неуспешно извличане на XLSX"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Фиксиране след попълване"
|
||||
goToPage = "Отидете на тази страница"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Страница"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "зад."
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Пресканирайте полетата"
|
||||
rescanFormFields = "Пресканирайте полетата на формуляра"
|
||||
save = "Запазете"
|
||||
saveShortcut = "Ctrl+S за запазване"
|
||||
skippedEdits_one = "1 промяна не можа да бъде приложена:"
|
||||
skippedEdits_other = "{{count}} промени не можаха да бъдат приложени:"
|
||||
skippedEditsTruncated = "Още {{count}} не са показани."
|
||||
unsavedChanges = "Незапазени промени"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Добавяне на {{count}} поле(та) към PDF"
|
||||
empty = "Все още няма начертани полета."
|
||||
failed = "Неуспешно добавяне на полета"
|
||||
goToField = "Отидете на това поле"
|
||||
hint = "Изберете тип поле, след което го начертайте на страницата."
|
||||
placing = "Начертайте поле {{type}} на страницата. Натиснете Esc, за да спрете."
|
||||
preview = "Задръжте за преглед"
|
||||
previewHelp = "Задръжте, за да видите как ще изглеждат полетата след добавяне, без контурите за редактиране."
|
||||
removeField = "Премахване на поле"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Действие на бутона"
|
||||
actionHelp = "Какво прави бутонът при щракване."
|
||||
actionNone = "Няма"
|
||||
actionPrint = "Печат"
|
||||
actionReset = "Нулиране на формуляра"
|
||||
actionSubmit = "Изпращане към URL"
|
||||
actionUri = "Отваряне на URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "Адресът, който бутонът отваря или към който изпраща."
|
||||
addOption = "Добавяне на опция"
|
||||
caption = "Надпис на бутона"
|
||||
captionHelp = "Текстът, отпечатан върху лицето на бутона."
|
||||
defaultValue = "Стойност по подразбиране"
|
||||
defaultValueHelp = "Какво съдържа полето, преди някой да го попълни. Оставете празно за празно поле."
|
||||
fontSize = "Размер на шрифта"
|
||||
fontSizeHelp = "Размер на текста в полето. Оставете празно, за да позволите на четеца да го оразмери според мястото."
|
||||
label = "Етикет"
|
||||
labelHelp = "Текстът, показван на този, който попълва формуляра. Оставете празно, за да се използва името на полето."
|
||||
maxLength = "Максимална дължина (comb)"
|
||||
maxLengthHelp = "Ограничава броя знаци, които се побират, начертани като равномерно разположени кутийки."
|
||||
multiline = "Многоредово"
|
||||
multilineHelp = "Позволява повече от един ред текст и пренасяне в края на полето."
|
||||
multiSelect = "Разрешаване на множествен избор"
|
||||
multiSelectHelp = "Позволява да бъде избрана повече от една опция едновременно."
|
||||
name = "Име на поле"
|
||||
nameHelp = "Вътрешното име на полето. Използва се при експортиране на данни или попълване на формуляра от друга система, затова го направете уникално и без интервали."
|
||||
optionGap = "Разстояние между опциите"
|
||||
optionGapHelp = "Разстояние между бутоните, в точки. Оставете празно, за да се разпределят равномерно надолу в полето."
|
||||
optionPlaceholder = "Опция {{n}}"
|
||||
options = "Опции"
|
||||
optionsEmpty = "Добавете поне една опция."
|
||||
optionsHelp = "Изборите, предлагани в списъка. Всеки се съхранява както е въведен, затова ги дръжте кратки и различими."
|
||||
optionSize = "Размер на опцията"
|
||||
optionSizeHelp = "Ширина и височина на всеки бутон, в точки. Оставете празно, за да се поберат в полето, което сте начертали."
|
||||
readOnly = "Само за четене"
|
||||
readOnlyHelp = "Показва стойност, но не позволява редактиране."
|
||||
removeOption = "Премахване на опция"
|
||||
required = "Задължително"
|
||||
requiredHelp = "Формулярът не може да бъде изпратен, докато това поле не бъде попълнено."
|
||||
signatureNote = "Само заместител - тук не подписвате. Той маркира къде трябва да бъде подписът, така че подписващ PDF инструмент (Adobe Acrobat, услуга за подписване и др.) да постави подписа на това място при подписване на документа."
|
||||
tooltip = "Подсказка"
|
||||
tooltipHelp = "Подсказката, показвана когато някой задържи курсора върху полето в PDF четец."
|
||||
type = "Тип"
|
||||
typeHelp = "Какъв вид поле е това. Промяната му изгражда полето наново, затова текущата му стойност не се пренася."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Създаване"
|
||||
fill = "Попълване"
|
||||
label = "Режим на редактора на формуляри"
|
||||
modify = "Промяна"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Запазване на {{count}} промяна(и)"
|
||||
delete = "Изтриване"
|
||||
empty = "Този PDF все още няма полета на формуляр."
|
||||
failed = "Неуспешно запазване на промените"
|
||||
groupSizeHint = "Използвайте размер на опцията"
|
||||
hint = "Изберете поле, за да редактирате свойствата му, плъзнете го по страницата или го изтрийте."
|
||||
restore = "Възстановяване"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Затворете страничната лента"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Промяна на ограниченията и разрешеният
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Промяна на правата"
|
||||
|
||||
[home.classify]
|
||||
desc = "Определете какъв вид документ е това и го маркирайте."
|
||||
title = "Класифициране"
|
||||
|
||||
[home.compare]
|
||||
desc = "Сравнява и показва разликите между 2 PDF документа"
|
||||
tags = "разлики"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Няма нищо за докладване."
|
||||
handoffUnavailable = "Този браузър няма да позволи на процесора да предаде документа на редактора. Вместо това го отворете от редактора."
|
||||
noDocumentLinked = "Тази грешка не е свързана с конкретен документ, така че тук няма какво да се отвори."
|
||||
notOnThisDevice = "Този документ не е на това устройство, така че не може да бъде отворен тук."
|
||||
occurrences = "{{count}} пъти"
|
||||
open = "Известия"
|
||||
title = "Известия"
|
||||
unread = "Непрочетени"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Това не проработи. Опитайте отново след малко."
|
||||
unavailable = "Не е налично за това известие."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Копирано"
|
||||
copy = "Копиране на грешка"
|
||||
less = "Показване на по-малко"
|
||||
more = "Показване на пълното съобщение"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "По-рано"
|
||||
new = "Нови"
|
||||
|
||||
[oauth.error]
|
||||
message = "Удостоверяването беше неуспешно. Можете да затворите този прозорец и да опитате отново."
|
||||
title = "Удостоверяването неуспешно"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "от {{allowance}} безплатни PDF използвани"
|
||||
capSuffix_other = "от {{allowance}} безплатни PDF използвани"
|
||||
eyebrow = "Пробен Processor"
|
||||
statusLabel_one = "остават {{remaining}}"
|
||||
statusLabel_other = "остават {{remaining}}"
|
||||
statusLabel_one = "{{used}} използван"
|
||||
statusLabel_other = "{{used}} използвани"
|
||||
sub = "Използвайте PDF Editor безплатно. Платете за автоматична обработка на PDF."
|
||||
title_one = "Обработете {{allowance}} PDF безплатно"
|
||||
title_other = "Обработете {{allowance}} PDF безплатно"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Преглед на файла"
|
||||
viewInProcessor = "Преглед в процесора"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Тази грешка не е записана за конкретен документ, така че тук няма какво да се отвори."
|
||||
unattended = "Този файл е подаден от папка, bucket или webhook, така че ничий браузър не го държи, за да го отвори."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Източници"
|
||||
connectSource = "Свързване на източник"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Разширени"
|
||||
back = "Назад към източниците"
|
||||
backToSource = "Назад към настройката на източника"
|
||||
backToTypes = "Всички типове източници"
|
||||
cancel = "Отказ"
|
||||
chooseHint = "Изберете откъде идват документите. Конекторите в сиво са в процес на разработка."
|
||||
@@ -8726,7 +8845,6 @@ create = "Създаване на източник"
|
||||
createTitle = "Свързване на източник"
|
||||
delete = "Изтриване"
|
||||
editTitle = "Редактиране на източник"
|
||||
enabled = "Активирано"
|
||||
save = "Запазване на промените"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: обработване на всеки файл веднъж
|
||||
snapshot = "Snapshot: повторно четене на папката при всяко изпълнение"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Включете подпапки, ако файловете ви са организирани във вложени папки, или наблюдавайте само най-горното ниво."
|
||||
label = "Дълбочина на папката"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Размер, дата и проверка на съдържанието
|
||||
stat = "Размер и дата на промяна"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Дали оригиналният файл да остане на място, след като бъде обработен. Ако оригиналният файл остане на място, той ще бъде обработен отново следващия път, когато работният поток сканира източника."
|
||||
label = "Режим на четене"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Consume: обработи всеки файл веднъж"
|
||||
snapshot = "Snapshot: препрочитай папката при всяко изпълнение"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Включете подпапки, ако файловете ви са организирани във вложени папки, или наблюдавайте само най-горното ниво."
|
||||
label = "Дълбочина на папката"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Вашият Stirling-PDF сървър е офлай
|
||||
expired = "Вашата сесия е изтекла. Моля, опреснете страницата и опитайте отново."
|
||||
|
||||
[settings]
|
||||
backToSections = "Всички настройки"
|
||||
close = "Затворете"
|
||||
title = "Настройки"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Документи и RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Търсене"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Търсене на инструменти..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Разширено форматиране"
|
||||
ai = "AI"
|
||||
automation = "Автоматизация"
|
||||
developerTools = "Инструменти за разработчици"
|
||||
documentReview = "Преглед на документ"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Колона {{index}}"
|
||||
convertToPdf = "Конвертирай в PDF"
|
||||
csvStats = "{{rows}} реда · {{columns}} колони · {{size}}"
|
||||
emptyFile = "Празен файл"
|
||||
htmlHidePreview = "Скриване на прегледа"
|
||||
htmlPreview = "HTML визуализация"
|
||||
htmlPreviewMobileHidden = "HTML страниците са оформени за ширини на настолен компютър, затова прегледът тук е изключен по подразбиране."
|
||||
htmlPreviewWarning = "HTML визуализация — външните ресурси може да не се заредят · {{size}}"
|
||||
htmlShowPreview = "Показване на прегледа въпреки това"
|
||||
invalidJson = "Невалиден JSON — показване на суровото съдържание"
|
||||
lineNumbers = "Номера на редовете"
|
||||
loading = "Зареждане..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Експортиране на PDF"
|
||||
exportSelected = "Експортиране на избраните страници"
|
||||
formFill = "Попълване на формуляр"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Още действия"
|
||||
multiTool = "Мултиинструмент"
|
||||
panMode = "Режим на преместване"
|
||||
print = "Печат на PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Избиране на всички"
|
||||
selectByNumber = "Избор по номера на страници"
|
||||
selectLanguage = "Избор на език"
|
||||
share = "Сподели"
|
||||
showAllTools = "Показване на всички инструменти"
|
||||
showFewerTools = "Свиване на лентата с инструменти"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Превключване на видимостта на анотациите"
|
||||
toggleAttachments = "Превключване на прикачените файлове"
|
||||
|
||||
@@ -3969,6 +3969,7 @@ back = "ཕྱིར་ལོག"
|
||||
backToFolder = "{{folder}} ལ་ཕྱིར་ལོག"
|
||||
backToMyFiles = "ངའི་ཡིག་ཆར་ཕྱིར་ལོག"
|
||||
breadcrumbs = "སྣོད་ཁུངས་ལམ"
|
||||
bulkActions = "བྱ་སྤྱོད།"
|
||||
cancel = "འདོར"
|
||||
classification = "རིགས་དབྱེ"
|
||||
clearSelection = "འདེམས་བཤོལ"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "སྤྱི་ཆེ་ཆུང"
|
||||
type = "རིགས"
|
||||
versionHistory = "པར་གཞིའི་འགྲོ་ལམ"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "འཚག་ཆས་{{count}} སྤྱོད་བཞིན།"
|
||||
clearAll = "འཚག་ཆས་ཚང་མ་གསལ་བཟོ།"
|
||||
label = "འཚག་ཆས།"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "འདོར"
|
||||
error = "སྣོད་འགེངས་སྲུངས་མ་ཐུབ། བསྐྱར་འདོད།"
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "ཡིག་ཆ་རིམ་སྒྲིག"
|
||||
modifiedAsc = "དུས་རྙིང་བ་སོང་མ"
|
||||
modifiedDesc = "ཉེ་ཆར་སྔོན"
|
||||
nameAsc = "མིང A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "གྷི་ཊི་ཧབ།"
|
||||
[formFill]
|
||||
allSaved = "ཚང་མ་ཉར་ཚགས་བྱས་ཟིན"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "བཟོ་བཅོས་ལག་བསྟར་བྱ་མ་ཐུབ།"
|
||||
extractCsvError = "CSV ཕྱིར་འདོན་མ་ཐུབ"
|
||||
extractXlsxError = "XLSX ཕྱིར་འདོན་མ་ཐུབ"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "བཀངས་རྗེས་རྡོག་བཟོ"
|
||||
goToPage = "ཤོག་ངོས་འདིར་སྐྱོད།"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "ཤོག་ངོས།"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "དགོས"
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "ས་ཁོངས་བསྐྱར་ཞིབ"
|
||||
rescanFormFields = "ཐོ་འགོད་ས་ཁོངས་བསྐྱར་ཞིབ"
|
||||
save = "སྲུངས"
|
||||
saveShortcut = "Ctrl+S སྤྱད་ནས་ཉར་ཚགས"
|
||||
skippedEdits_one = "བཟོ་བཅོས་1 ལག་བསྟར་བྱ་མ་ཐུབ།"
|
||||
skippedEdits_other = "བཟོ་བཅོས་{{count}} ལག་བསྟར་བྱ་མ་ཐུབ།"
|
||||
skippedEditsTruncated = "ད་དུང་{{count}} ཐོ་འགོད་བྱས་མེད།"
|
||||
unsavedChanges = "ཉར་ཚགས་མ་བྱས་པའི་འགྱུར་བ"
|
||||
|
||||
[formFill.create]
|
||||
commit = "PDF ལ་{{count}} ཡིག་ཚན་ཁ་སྣོན་བྱེད།"
|
||||
empty = "ད་དུང་ཡིག་ཚན་བྲིས་མེད།"
|
||||
failed = "ཡིག་ཚན་ཁ་སྣོན་བྱ་མ་ཐུབ།"
|
||||
goToField = "ཡིག་ཚན་འདིར་སྐྱོད།"
|
||||
hint = "ཡིག་ཚན་རིགས་ཤིག་འདེམས་ནས་དེ་ཤོག་ངོས་སུ་བྲིས།"
|
||||
placing = "ཤོག་ངོས་སུ་{{type}} ཡིག་ཚན་བྲིས། མཚམས་འཇོག་པར་Esc མནན།"
|
||||
preview = "སྔོན་ལྟ་བྱེད་པར་མནན་ནས་འཛིན།"
|
||||
previewHelp = "རྩོམ་སྒྲིག་གི་མཐའ་ཐིག་མེད་པར་ཁ་སྣོན་བྱས་རྗེས་ཡིག་ཚན་ཇི་ལྟར་མངོན་པ་ལྟ་བར་མནན་ནས་འཛིན།"
|
||||
removeField = "ཡིག་ཚན་སུབ།"
|
||||
|
||||
[formFill.editor]
|
||||
action = "མཐེབ་གནོན་བྱ་སྤྱོད།"
|
||||
actionHelp = "མཐེབ་གནོན་ལ་གནོན་སྐབས་ཅི་བྱེད་པ།"
|
||||
actionNone = "མེད།"
|
||||
actionPrint = "པར་འདེབས།"
|
||||
actionReset = "འགེངས་ཤོག་སླར་སྒྲིག"
|
||||
actionSubmit = "URL ལ་སྤྲོད།"
|
||||
actionUri = "URL ཁ་ཕྱེ།"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "མཐེབ་གནོན་གྱིས་ཁ་ཕྱེ་བའམ་སྤྲོད་སའི་ཁ་བྱང་།"
|
||||
addOption = "གདམ་གསེས་ཁ་སྣོན།"
|
||||
caption = "མཐེབ་གནོན་ཁ་བྱང་།"
|
||||
captionHelp = "མཐེབ་གནོན་ངོས་སུ་པར་འདེབས་བྱེད་པའི་ཡི་གེ།"
|
||||
defaultValue = "སྔོན་སྒྲིག་རིན་ཐང་།"
|
||||
defaultValueHelp = "སུ་ཞིག་གིས་འགེངས་མ་བྱས་གོང་ཡིག་ཚན་ནང་ཡོད་པ། ཡིག་ཚན་སྟོང་པར་འཇོག་དགོས་ན་སྟོང་པར་བཞག"
|
||||
fontSize = "ཡིག་གཟུགས་ཆེ་ཆུང་།"
|
||||
fontSizeHelp = "ཡིག་ཚན་ནང་གི་ཡི་གེའི་ཆེ་ཆུང་། ཀློག་ཆས་ཀྱིས་འཚམ་པར་ཆེ་ཆུང་སྒྲིག་པར་སྟོང་པར་བཞག"
|
||||
label = "ཤོ་བྱང་།"
|
||||
labelHelp = "འགེངས་ཤོག་འགེངས་མཁན་ལ་མངོན་པའི་ཚིག སྟོང་པར་བཞག་ན་ཡིག་ཚན་མིང་ལ་ཕྱིར་ལོག་བྱེད།"
|
||||
maxLength = "རིང་ཚད་མཐོ་ཤོས། (comb)"
|
||||
maxLengthHelp = "ཡིག་འབྲུ་ག་ཚོད་ཤོང་བ་ཚད་འཛིན་བྱེད་པ་དང་། དེ་དག་བར་སྟོང་མཉམ་པའི་སྒམ་ལྟར་བྲིས་ཡོད།"
|
||||
multiline = "ཐིག་ཕྲེང་མང་པོ།"
|
||||
multilineHelp = "ཡིག་ཕྲེང་གཅིག་ལས་མང་བའི་ཡི་གེ་འཇུག་ཆོག་པ་དང་། ཡིག་ཚན་གྱི་མཐའ་ལ་སླེབས་ན་ཕྲེང་བརྗེ་བྱེད།"
|
||||
multiSelect = "གདམ་གསེས་མང་པོ་འདེམས་ཆོག"
|
||||
multiSelectHelp = "གདམ་གསེས་གཅིག་ལས་མང་བ་དུས་གཅིག་ཏུ་འདེམས་ཆོག"
|
||||
name = "ཡིག་ཚན་མིང་།"
|
||||
nameHelp = "ཡིག་ཚན་གྱི་ནང་ཁུལ་མིང་། གཞི་གྲངས་ཕྱིར་འདོན་ནམ་མ་ལག་གཞན་ནས་འགེངས་ཤོག་འགེངས་སྐབས་སྤྱོད་པས། དེ་ཁྱད་འཕགས་དང་བར་སྟོང་མེད་པར་ཉར།"
|
||||
optionGap = "གདམ་གསེས་བར་སྟོང་།"
|
||||
optionGapHelp = "མཐེབ་གནོན་བར་གྱི་བར་སྟོང་། ཚེག་ཐིག་གིས་ཚད་འཇལ། སྒམ་ནང་དུ་འོག་ཕྱོགས་སུ་མཉམ་པར་བཀྲམ་པར་སྟོང་པར་བཞག"
|
||||
optionPlaceholder = "གདམ་གསེས་{{n}}"
|
||||
options = "གདམ་གསེས།"
|
||||
optionsEmpty = "ཉུང་མཐར་གདམ་གསེས་གཅིག་ཁ་སྣོན་བྱེད།"
|
||||
optionsHelp = "ཐོ་ཡིག་ནང་སྤྲོད་པའི་གདམ་གསེས་རྣམས། རེ་རེ་འབྲི་བ་ལྟར་ཉར་ཚགས་བྱེད་པས། ཐུང་ཞིང་ཁ་གསལ་པོར་ཉར།"
|
||||
optionSize = "གདམ་གསེས་ཆེ་ཆུང་།"
|
||||
optionSizeHelp = "མཐེབ་གནོན་རེ་རེའི་ཞེང་ཚད་དང་མཐོ་ཚད། ཚེག་ཐིག་གིས་ཚད་འཇལ། ཁྱེད་ཀྱིས་བྲིས་པའི་སྒམ་ལ་འཚམ་པར་སྟོང་པར་བཞག"
|
||||
readOnly = "ཀློག་ཙམ།"
|
||||
readOnlyHelp = "རིན་ཐང་ཞིག་སྟོན་ཡང་སུ་ཞིག་གིས་ཀྱང་རྩོམ་སྒྲིག་བྱ་མི་ཆོག"
|
||||
removeOption = "གདམ་གསེས་སུབ།"
|
||||
required = "ངེས་པར་དགོས་པ།"
|
||||
requiredHelp = "ཡིག་ཚན་འདི་མ་བཀང་བར་འགེངས་ཤོག་སྤྲོད་མི་ཆོག"
|
||||
signatureNote = "ཚབ་འཇོག་ས་ཙམ་ཡིན་ - འདིར་མིང་རྟགས་མི་འགོད། ཡིག་ཆར་མིང་རྟགས་འགོད་སྐབས་PDF མིང་རྟགས་འགོད་ཆས་(Adobe Acrobat, མིང་རྟགས་ཞབས་ཞུ་སོགས་) ཀྱིས་ས་འདིར་མིང་རྟགས་འཇོག་པའི་མཚོན་རྟགས་ཡིན།"
|
||||
tooltip = "སྣེ་སྟོན།"
|
||||
tooltipHelp = "PDF ཀློག་ཆས་ནང་མི་ཞིག་གིས་ཡིག་ཚན་སྟེང་ཙིག་རྟགས་འཇོག་སྐབས་མངོན་པའི་སྣེ་སྟོན།"
|
||||
type = "རིགས།"
|
||||
typeHelp = "འདི་ཡིག་ཚན་རིགས་གང་ཡིན་པ། དེ་བསྒྱུར་ན་ཡིག་ཚན་སླར་བཟོ་བྱེད་པས། ད་ལྟའི་རིན་ཐང་མུ་མཐུད་འཁྱེར་མི་ཐུབ།"
|
||||
|
||||
[formFill.mode]
|
||||
create = "གསར་བཟོ།"
|
||||
fill = "འགེངས།"
|
||||
label = "འགེངས་ཤོག་རྩོམ་སྒྲིག་རྣམ་པ།"
|
||||
modify = "བཟོ་བཅོས།"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "བཟོ་བཅོས་{{count}} ཉར།"
|
||||
delete = "སུབ།"
|
||||
empty = "PDF འདིར་ད་དུང་འགེངས་ཤོག་ཡིག་ཚན་མེད།"
|
||||
failed = "བཟོ་བཅོས་ཉར་མ་ཐུབ།"
|
||||
groupSizeHint = "གདམ་གསེས་ཆེ་ཆུང་སྤྱོད།"
|
||||
hint = "ཡིག་ཚན་ཞིག་འདེམས་ནས་ཁྱད་ཆོས་རྩོམ་སྒྲིག་བྱེད་པའམ། ཤོག་ངོས་སུ་འདྲུད་པ། ཡང་ན་སུབ།"
|
||||
restore = "སླར་གསོ།"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "ཟུར་ཚན་ཁ་རྒྱག"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "ཡིག་ཆའི་ཚད་བཀག་དང་ཆོག་མཆ
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "འཕོ་འགྱུར།"
|
||||
|
||||
[home.classify]
|
||||
desc = "ཡིག་ཆ་འདི་རིགས་གང་ཡིན་པ་ངོས་འཛིན་བྱས་ནས་ཤོ་བྱང་སྦྱོར།"
|
||||
title = "རིགས་དབྱེ།"
|
||||
|
||||
[home.compare]
|
||||
desc = "PDFཡིག་ཆ་2ཀྱི་ཁྱད་པར་བསྡུར་བ་དང་བསྡུར་བ།"
|
||||
tags = "ཁྱད་པར"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "ཡར་ཞུ་བྱ་རྒྱུ་མེད།"
|
||||
handoffUnavailable = "བཤར་ཆས་འདིས་སྒྲིག་སྦྱོར་ཆས་ནས་ཡིག་ཆ་རྩོམ་སྒྲིག་ཆས་ལ་སྤྲོད་དུ་མི་འཇུག དེའི་ཚབ་ཏུ་རྩོམ་སྒྲིག་ཆས་ནས་ཁ་ཕྱེ།"
|
||||
noDocumentLinked = "ཕམ་ཉེས་འདི་ཡིག་ཆ་དམིགས་བསལ་ཞིག་དང་སྦྲེལ་མེད་པས། འདིར་ཁ་ཕྱེ་རྒྱུ་མེད།"
|
||||
notOnThisDevice = "ཡིག་ཆ་འདི་སྒྲིག་ཆས་འདིར་མེད་པས། འདིར་ཁ་ཕྱེ་མི་ཐུབ།"
|
||||
occurrences = "ཐེངས་{{count}}"
|
||||
open = "བརྡ་ཐོ་ཁ་ཕྱེ།"
|
||||
title = "བརྡ་ཐོ།"
|
||||
unread = "ཀློག་མེད།"
|
||||
|
||||
[notifications.action]
|
||||
failed = "དེ་ལས་མ་ཐུབ། ཅུང་ཙམ་རྗེས་ནས་ཡང་བསྐྱར་ཚོད་ལྟ་བྱོས།"
|
||||
unavailable = "བརྡ་ཐོ་འདིར་སྤྱོད་མི་རུང་།"
|
||||
|
||||
[notifications.detail]
|
||||
copied = "འདྲ་བཤུས་བྱས་ཟིན།"
|
||||
copy = "ནོར་འཁྲུལ་འདྲ་བཤུས།"
|
||||
less = "ཉུང་ཙམ་སྟོན།"
|
||||
more = "འཕྲིན་ཐུང་ཆ་ཚང་སྟོན།"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "སྔོན་མ།"
|
||||
new = "གསར་པ།"
|
||||
|
||||
[oauth.error]
|
||||
message = "བདེན་དཔང་དེ་གྲུབ་འབྲས་ཐོབ་མེད། ཁྱོད་ཀྱིས་སྒེའུ་ཁུང་འདི་ཁ་རྒྱག་ནས་བསྐྱར་དུ་ཚོད་ལྟ་བྱེད་ཐུབ།"
|
||||
title = "བདེན་དཔང་མ་ཐུབ་པ།"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "རིན་མེད PDFs {{allowance}} ནས་སྤྱད་པ་"
|
||||
capSuffix_other = "རིན་མེད PDFs {{allowance}} ནས་སྤྱད་པ་"
|
||||
eyebrow = "སྒྲིག་གཅོད་ཆས་ཚོད་སྤྱོད་"
|
||||
statusLabel_one = "{{remaining}} ལྷག་"
|
||||
statusLabel_other = "{{remaining}} ལྷག་"
|
||||
statusLabel_one = "{{used}} སྤྱད་ཟིན།"
|
||||
statusLabel_other = "{{used}} སྤྱད་ཟིན།"
|
||||
sub = "PDF རྩོམ་སྒྲིག་ཆས་རིན་མེད་སྤྱོད། PDFs རང་འགུལ་སྒྲིག་གཅོད་ལ་དངུལ་སྤྲོད།"
|
||||
title_one = "PDFs {{allowance}} རིན་མེད་སྒྲིག་གཅོད་"
|
||||
title_other = "PDFs {{allowance}} རིན་མེད་སྒྲིག་གཅོད་"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "ཡིག་ཆ་ལྟ།"
|
||||
viewInProcessor = "སྒྲིག་སྦྱོར་ཆས་ནང་ལྟ།"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "ཕམ་ཉེས་འདི་ཡིག་ཆ་དམིགས་བསལ་ཞིག་ལ་ཟིན་ཐོ་བཀོད་མེད་པས། འདིར་ཁ་ཕྱེ་རྒྱུ་མེད།"
|
||||
unattended = "ཡིག་ཆ་འདི་ཡིག་ཁུག སྣོད། ཡང་ན་webhook ནས་བཏང་བས། དེ་ཁ་ཕྱེ་བར་སུའི་བཤར་ཆས་ཀྱང་ཉར་མེད།"
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "ཁུངས"
|
||||
connectSource = "ཁུངས་འབྲེལ་མཐུད"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "མཐོ་རིམ།"
|
||||
back = "ཁུངས་ལ་ཕྱིར་ལོག"
|
||||
backToSource = "འབྱུང་ཁུངས་སྒྲིག་འགོད་ལ་ཕྱིར་ལོག"
|
||||
backToTypes = "ཁུངས་རིགས་ཚང་མ"
|
||||
cancel = "འདོར"
|
||||
chooseHint = "ཡིག་ཆ་གང་ནས་ཡོང་བ་འདེམས། སྐྱ་མདོག་གི connectors རྣམས་འོང་བཞིན་པ་ཡིན།"
|
||||
@@ -8726,7 +8845,6 @@ create = "ཁུངས་བཟོ"
|
||||
createTitle = "ཁུངས་ཞིག་འབྲེལ་མཐུད"
|
||||
delete = "སུབ"
|
||||
editTitle = "ཁུངས་རྩོམ་སྒྲིག"
|
||||
enabled = "ནུས་ཡོད"
|
||||
save = "འགྱུར་བ་ཉར"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: ཡིག་ཆ་རེ་རེ་ཐེངས་གཅི
|
||||
snapshot = "Snapshot: འཁོར་རེར་ཡིག་སྣོད་སླར་ཀློག"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "ཁྱེད་ཀྱི་ཡིག་ཆ་ནང་སྦས་ཡིག་ཁུག་ཏུ་སྒྲིག་ཡོད་ན། ཡན་ལག་ཡིག་ཁུག་ཚུད་པར་བྱེད་པའམ། རིམ་པ་གོང་མ་ཁོ་ན་ལ་ལྟ་ཞིབ་བྱེད།"
|
||||
label = "ཡིག་སྣོད་གཏིང་ཚད"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "ཆེ་ཆུང་། ཚེས་གྲངས་དང་ནང་
|
||||
stat = "ཆེ་ཆུང་དང་བཟོ་བཅོས་ཚེས་གྲངས"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "སྒྲིག་སྦྱོར་བྱས་རྗེས་མ་ཡིག་ཆ་རང་སར་འཇོག་མིན། མ་ཡིག་ཆ་རང་སར་བཞག་ན། pipeline གྱིས་རྗེས་མ་འབྱུང་ཁུངས་བཤེར་སྐབས་དེ་ཡང་བསྐྱར་སྒྲིག་སྦྱོར་བྱེད།"
|
||||
label = "ཀློག་སྟངས"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Consume: ཡིག་ཆ་རེ་རེ་ཐེངས་གཅི
|
||||
snapshot = "Snapshot: འཁོར་རེར་ཡིག་སྣོད་སླར་ཀློག"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "ཁྱེད་ཀྱི་ཡིག་ཆ་ནང་སྦས་ཡིག་ཁུག་ཏུ་སྒྲིག་ཡོད་ན། ཡན་ལག་ཡིག་ཁུག་ཚུད་པར་བྱེད་པའམ། རིམ་པ་གོང་མ་ཁོ་ན་ལ་ལྟ་ཞིབ་བྱེད།"
|
||||
label = "ཡིག་སྣོད་གཏིང་ཚད"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "ཁྱེད་ཀྱི Stirling-PDF ཞབས་ཞ
|
||||
expired = "ཁྱེད་རང་གི་ཚོགས་ཐུན་དུས་ཚོད་རྫོགས་ཡོད། ཤོག་བུ་གསར་བཟོ་བྱས་ནས་བསྐྱར་དུ་ཚོད་ལྟ་གནང་རོགས།"
|
||||
|
||||
[settings]
|
||||
backToSections = "སྒྲིག་འགོད་ཚང་མ།"
|
||||
close = "ཁ་རྒྱག"
|
||||
title = "སྒྲིག་འགོད།"
|
||||
|
||||
[settings.ai]
|
||||
documents = "ཡིག་ཆ་ & RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "འཚོལ།"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "འཚོལ་ཞིབ་ལག་ཆ།"
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "ཡར་ཐོན་རྣམ་གཞག་བཟོ་བ།"
|
||||
ai = "AI"
|
||||
automation = "རང་འགུལ་ཅན།"
|
||||
developerTools = "གོང་འཕེལ་གཏོང་མཁན་གྱི་ལག་ཆ།"
|
||||
documentReview = "ཡིག་ཆ་བསྐྱར་ཞིབ།"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "གྲལ་ཐིག {{index}}"
|
||||
convertToPdf = "PDF ལ་བསྒྱུར"
|
||||
csvStats = "{{rows}} གྲལ · {{columns}} སྟར · {{size}}"
|
||||
emptyFile = "ཡིག་ཆ་སྟོང་"
|
||||
htmlHidePreview = "སྔོན་ལྟ་སྦས།"
|
||||
htmlPreview = "HTML སྔོན་ལྟ"
|
||||
htmlPreviewMobileHidden = "HTML ཤོག་ངོས་རྣམས་ཅོག་ངོས་ཞེང་ཚད་ལ་བཀོད་སྒྲིག་བྱས་པས། འདིར་སྔོན་ལྟ་སྔོན་སྒྲིག་གིས་སྒོ་བརྒྱབ་ཡོད།"
|
||||
htmlPreviewWarning = "HTML སྔོན་ལྟ — ཕྱི་ཐོན་ཁུངས་ཚུད་མི་སྲིད · {{size}}"
|
||||
htmlShowPreview = "གང་ལྟར་སྔོན་ལྟ་སྟོན།"
|
||||
invalidJson = "JSON ནུས་མེད — ནང་དོར་ཐོག་མངོན"
|
||||
lineNumbers = "ཕྲེང་གྲངས"
|
||||
loading = "སྒྲུབ་བརྒྱབ..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "PDF ཕྱིར་འདྲེན"
|
||||
exportSelected = "འདེམས་ཟིན་པའི་ཤོག་ངོས་ཕྱིར་འདྲེན"
|
||||
formFill = "རྣམ་གཞག་ཁ་སྐོང་"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "བྱ་སྤྱོད་མང་བ།"
|
||||
multiTool = "སྣ་ཚོགས་ལག་ཆས"
|
||||
panMode = "སྤོ་བཤུད་ཐབས་ལམ"
|
||||
print = "PDF དཔར་བསྐྲུན"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "ཚང་མ་འདེམས"
|
||||
selectByNumber = "ཤོག་ངོས་གྲངས་ཀྱིས་འདེམས"
|
||||
selectLanguage = "སྐད་ཡིག་འདེམས"
|
||||
share = "མཉམ་སྤྱོད"
|
||||
showAllTools = "ལག་ཆ་ཚང་མ་སྟོན།"
|
||||
showFewerTools = "ལག་ཆ་ཚང་བརྡར་འགྲིལ།"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "མཆན་བཀོད་མཐོང་རུང་བ་བསྒྱུར"
|
||||
toggleAttachments = "ཟུང་འདྲེན་བསྒྱུར"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Fals"
|
||||
fileNotSavedToDisk = "No s'ha desat al disc"
|
||||
fileSavedToDisk = "Fitxer desat al disc"
|
||||
fileSelected = "Seleccionat: {{filename}}"
|
||||
filesSelected = "fitxers seleccionats"
|
||||
filesSelected = "{{count}} fitxers"
|
||||
font = "Tipus de lletra"
|
||||
fontSizeTooltip = "Mida del text del número de pàgina en punts. Valors més alts generen text més gran."
|
||||
fontTypeTooltip = "Família del tipus de lletra per als números de pàgina. Tria-la segons l'estil del teu document."
|
||||
@@ -3969,6 +3969,7 @@ back = "Enrere"
|
||||
backToFolder = "Torna a {{folder}}"
|
||||
backToMyFiles = "Torna a Els meus fitxers"
|
||||
breadcrumbs = "Ruta de la carpeta"
|
||||
bulkActions = "Accions"
|
||||
cancel = "Cancel·la"
|
||||
classification = "Classificació"
|
||||
clearSelection = "Neteja la selecció"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Mida total"
|
||||
type = "Tipus"
|
||||
versionHistory = "Recorregut de versions"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} filtres actius"
|
||||
clearAll = "Esborra els filtres"
|
||||
label = "Filtres"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Cancel·la"
|
||||
error = "No s'ha pogut desar la carpeta. Torna-ho a provar."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Ordena els fitxers"
|
||||
modifiedAsc = "Més antic primer"
|
||||
modifiedDesc = "Més recent primer"
|
||||
nameAsc = "Nom A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Tot desat"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "No s'han pogut aplicar els canvis"
|
||||
extractCsvError = "No s'ha pogut extreure el CSV"
|
||||
extractXlsxError = "No s'ha pogut extreure l'XLSX"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Aplana després d'emplenar"
|
||||
goToPage = "Ves a aquesta pàgina"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Pàgina"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "obl."
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Torna a analitzar els camps"
|
||||
rescanFormFields = "Torna a analitzar els camps del formulari"
|
||||
save = "Desa"
|
||||
saveShortcut = "Ctrl+S per desar"
|
||||
skippedEdits_one = "No s'ha pogut aplicar 1 canvi:"
|
||||
skippedEdits_other = "No s'han pogut aplicar {{count}} canvis:"
|
||||
skippedEditsTruncated = "{{count}} més no es mostren."
|
||||
unsavedChanges = "Canvis no desats"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Afegeix {{count}} camp(s) al PDF"
|
||||
empty = "No s'ha dibuixat cap camp encara."
|
||||
failed = "No s'han pogut afegir els camps"
|
||||
goToField = "Ves a aquest camp"
|
||||
hint = "Tria un tipus de camp i després dibuixa'l a la pàgina."
|
||||
placing = "Dibuixa un camp de tipus {{type}} a la pàgina. Prem Esc per aturar-te."
|
||||
preview = "Mantén premut per previsualitzar"
|
||||
previewHelp = "Mantén premut per veure els camps tal com es mostraran un cop afegits, sense els contorns d'edició."
|
||||
removeField = "Elimina el camp"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Acció del botó"
|
||||
actionHelp = "Què fa el botó quan s'hi fa clic."
|
||||
actionNone = "Cap"
|
||||
actionPrint = "Imprimeix"
|
||||
actionReset = "Restableix el formulari"
|
||||
actionSubmit = "Envia a URL"
|
||||
actionUri = "Obre URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "L'adreça que el botó obre o a la qual envia."
|
||||
addOption = "Afegeix una opció"
|
||||
caption = "Text del botó"
|
||||
captionHelp = "El text imprès a la cara del botó."
|
||||
defaultValue = "Valor per defecte"
|
||||
defaultValueHelp = "Què conté el camp abans que ningú l'empleni. Deixa-ho en blanc per a un camp buit."
|
||||
fontSize = "Mida de la lletra"
|
||||
fontSizeHelp = "Mida del text dins del camp. Deixa-ho en blanc perquè el lector n'ajusti la mida perquè hi càpiga."
|
||||
label = "Etiqueta"
|
||||
labelHelp = "El text que es mostra a qui emplena el formulari. Deixa-ho en blanc per fer servir el nom del camp per defecte."
|
||||
maxLength = "Longitud màxima (comb)"
|
||||
maxLengthHelp = "Limita el nombre de caràcters que hi caben, dibuixats com a caselles espaiades uniformement."
|
||||
multiline = "Diverses línies"
|
||||
multilineHelp = "Permet més d'una línia de text i fa salt de línia a la vora del camp."
|
||||
multiSelect = "Permet la selecció múltiple"
|
||||
multiSelectHelp = "Permet triar més d'una opció alhora."
|
||||
name = "Nom del camp"
|
||||
nameHelp = "El nom intern del camp. S'utilitza en exportar dades o en emplenar el formulari des d'un altre sistema, així que mantén-lo únic i sense espais."
|
||||
optionGap = "Espaiat de les opcions"
|
||||
optionGapHelp = "Espai entre botons, en punts. Deixa-ho en blanc per distribuir-los uniformement dins del quadre."
|
||||
optionPlaceholder = "Opció {{n}}"
|
||||
options = "Opcions"
|
||||
optionsEmpty = "Afegeix almenys una opció."
|
||||
optionsHelp = "Les opcions que s'ofereixen a la llista. Cadascuna s'emmagatzema tal com s'escriu, així que mantén-les curtes i diferenciades."
|
||||
optionSize = "Mida de l'opció"
|
||||
optionSizeHelp = "Amplada i alçada de cada botó, en punts. Deixa-ho en blanc per ajustar-los al quadre que has dibuixat."
|
||||
readOnly = "Només de lectura"
|
||||
readOnlyHelp = "Mostra un valor però impedeix que algú l'editi."
|
||||
removeOption = "Elimina l'opció"
|
||||
required = "Obligatori"
|
||||
requiredHelp = "El formulari no es pot enviar fins que aquest camp s'hagi emplenat."
|
||||
signatureNote = "Només és un marcador de posició - no signeu aquí. Marca on correspon una signatura perquè un signador de PDF (Adobe Acrobat, un servei de signatura, etc.) col·loqui la signatura en aquest lloc quan se signi el document."
|
||||
tooltip = "Descripció emergent"
|
||||
tooltipHelp = "L'ajuda que es mostra quan algú passa el cursor per sobre del camp en un lector de PDF."
|
||||
type = "Tipus"
|
||||
typeHelp = "Quin tipus de camp és. En canviar-lo es reconstrueix el camp, de manera que el seu valor actual no es conserva."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Crea"
|
||||
fill = "Emplena"
|
||||
label = "Mode de l'editor de formularis"
|
||||
modify = "Modifica"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Desa {{count}} canvi(s)"
|
||||
delete = "Suprimeix"
|
||||
empty = "Aquest PDF encara no té camps de formulari."
|
||||
failed = "No s'han pogut desar els canvis"
|
||||
groupSizeHint = "Utilitza la mida de l'opció"
|
||||
hint = "Selecciona un camp per editar-ne les propietats, arrossega'l a la pàgina o suprimeix-lo."
|
||||
restore = "Restaura"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Tanca la barra lateral"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Canvia les restriccions i els permisos del document"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Canviar Permissos"
|
||||
|
||||
[home.classify]
|
||||
desc = "Identifica quin tipus de document és i etiqueta'l."
|
||||
title = "Classifica"
|
||||
|
||||
[home.compare]
|
||||
desc = "Compara i mostra les diferències entre 2 documents PDF"
|
||||
tags = "diferència"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "No hi ha res a informar."
|
||||
handoffUnavailable = "Aquest navegador no permet que el processador passi el document a l'editor. Obre'l des de l'editor."
|
||||
noDocumentLinked = "Aquesta fallada no està vinculada a cap document específic, així que no hi ha res per obrir aquí."
|
||||
notOnThisDevice = "Aquest document no es troba en aquest dispositiu, així que no es pot obrir aquí."
|
||||
occurrences = "{{count}} vegades"
|
||||
open = "Notificacions"
|
||||
title = "Notificacions"
|
||||
unread = "No llegides"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Això no ha funcionat. Torna-ho a provar d'aquí a un moment."
|
||||
unavailable = "No disponible per a aquesta notificació."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Copiat"
|
||||
copy = "Copia l'error"
|
||||
less = "Mostra menys"
|
||||
more = "Mostra el missatge complet"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Anteriors"
|
||||
new = "Noves"
|
||||
|
||||
[oauth.error]
|
||||
message = "L'autenticació no ha estat correcta. Pots tancar aquesta finestra i tornar-ho a provar."
|
||||
title = "Ha fallat l'autenticació"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "de {{allowance}} PDF gratuïts utilitzats"
|
||||
capSuffix_other = "de {{allowance}} PDF gratuïts utilitzats"
|
||||
eyebrow = "Prova de Processor"
|
||||
statusLabel_one = "en queda {{remaining}}"
|
||||
statusLabel_other = "en queden {{remaining}}"
|
||||
statusLabel_one = "{{used}} utilitzat"
|
||||
statusLabel_other = "{{used}} utilitzats"
|
||||
sub = "Utilitza el PDF Editor de franc. Paga per processar PDF automàticament."
|
||||
title_one = "Processa {{allowance}} PDF de franc"
|
||||
title_other = "Processa {{allowance}} PDF de franc"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Visualitza el fitxer"
|
||||
viewInProcessor = "Visualitza al processador"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Aquesta fallada no s'ha registrat per a cap document específic, així que no hi ha res per obrir aquí."
|
||||
unattended = "Aquest fitxer ha arribat des d'una carpeta, un bucket o un webhook, així que cap navegador el manté disponible per obrir-lo."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Fonts"
|
||||
connectSource = "Connecta una font"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Avançat"
|
||||
back = "Torna a les fonts"
|
||||
backToSource = "Torna a la configuració de l'origen"
|
||||
backToTypes = "Tots els tipus de font"
|
||||
cancel = "Cancel·la"
|
||||
chooseHint = "Tria d'on venen els documents. Els connectors en gris estan en camí."
|
||||
@@ -8726,7 +8845,6 @@ create = "Crea una font"
|
||||
createTitle = "Connecta una font"
|
||||
delete = "Suprimeix"
|
||||
editTitle = "Edita la font"
|
||||
enabled = "Activada"
|
||||
save = "Desa els canvis"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: processa cada fitxer una vegada"
|
||||
snapshot = "Snapshot: torna a llegir la carpeta a cada execució"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Inclou subcarpetes si els fitxers estan organitzats en carpetes imbricades, o supervisa només el nivell superior."
|
||||
label = "Profunditat de carpeta"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Mida, data i comprovació de contingut"
|
||||
stat = "Mida i data de modificació"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Indica si s'ha de deixar el fitxer original al seu lloc després que s'hagi processat. Si el fitxer original es deixa al seu lloc, es tornarà a processar la propera vegada que el pipeline analitzi l'origen."
|
||||
label = "Mode de lectura"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Consumeix: processa cada fitxer una vegada"
|
||||
snapshot = "Instantània: torna a llegir la carpeta en cada execució"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Inclou subcarpetes si els fitxers estan organitzats en carpetes imbricades, o supervisa només el nivell superior."
|
||||
label = "Profunditat de carpeta"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "El teu servidor de Stirling-PDF és fora de línia i
|
||||
expired = "La teva sessió ha expirat. Si us plau, actualitza la pàgina i torna a intentar-ho."
|
||||
|
||||
[settings]
|
||||
backToSections = "Tota la configuració"
|
||||
close = "Tanca"
|
||||
title = "Configuració"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Documents i RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Cerca"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Cerca eines..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Formatació avançada"
|
||||
ai = "IA"
|
||||
automation = "Automatització"
|
||||
developerTools = "Eines per a desenvolupadors"
|
||||
documentReview = "Revisió del document"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Columna {{index}}"
|
||||
convertToPdf = "Converteix a PDF"
|
||||
csvStats = "{{rows}} files · {{columns}} columnes · {{size}}"
|
||||
emptyFile = "Fitxer buit"
|
||||
htmlHidePreview = "Amaga la previsualització"
|
||||
htmlPreview = "Previsualització HTML"
|
||||
htmlPreviewMobileHidden = "Les pàgines HTML estan dissenyades per a amplades d'escriptori, per tant aquí la previsualització està desactivada per defecte."
|
||||
htmlPreviewWarning = "Previsualització HTML — els recursos externs poden no carregar-se · {{size}}"
|
||||
htmlShowPreview = "Mostra la previsualització igualment"
|
||||
invalidJson = "JSON no vàlid — es mostra el contingut en brut"
|
||||
lineNumbers = "Números de línia"
|
||||
loading = "Carregant..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Exporta el PDF"
|
||||
exportSelected = "Exporta les pàgines seleccionades"
|
||||
formFill = "Omple el formulari"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Més accions"
|
||||
multiTool = "Eina múltiple"
|
||||
panMode = "Mode de desplaçament"
|
||||
print = "Imprimeix el PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Selecciona-ho tot"
|
||||
selectByNumber = "Selecciona per números de pàgina"
|
||||
selectLanguage = "Selecciona l’idioma"
|
||||
share = "Comparteix"
|
||||
showAllTools = "Mostra totes les eines"
|
||||
showFewerTools = "Replega la barra d'eines"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Mostra/oculta les anotacions"
|
||||
toggleAttachments = "Mostra/oculta els adjunts"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Ne"
|
||||
fileNotSavedToDisk = "Neuloženo na disk"
|
||||
fileSavedToDisk = "Soubor uložen na disk"
|
||||
fileSelected = "Vybráno: {{filename}}"
|
||||
filesSelected = "vybraných souborů"
|
||||
filesSelected = "{{count}} souborů"
|
||||
font = "Písmo"
|
||||
fontSizeTooltip = "Velikost textu čísla stránky v bodech. Větší čísla znamenají větší text."
|
||||
fontTypeTooltip = "Rodina písma pro čísla stránek. Zvolte podle stylu dokumentu."
|
||||
@@ -3969,6 +3969,7 @@ back = "Zpět"
|
||||
backToFolder = "Zpět do {{folder}}"
|
||||
backToMyFiles = "Zpět na Moje soubory"
|
||||
breadcrumbs = "Cesta ke složce"
|
||||
bulkActions = "Akce"
|
||||
cancel = "Zrušit"
|
||||
classification = "Klasifikace"
|
||||
clearSelection = "Zrušit výběr"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Celková velikost"
|
||||
type = "Typ"
|
||||
versionHistory = "Vývoj verzí"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} aktivních filtrů"
|
||||
clearAll = "Vymazat filtry"
|
||||
label = "Filtry"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Zrušit"
|
||||
error = "Složku se nepodařilo uložit. Zkuste to znovu."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Seřadit soubory"
|
||||
modifiedAsc = "Od nejstarších"
|
||||
modifiedDesc = "Od nejnovějších"
|
||||
nameAsc = "Název A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Vše uloženo"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Změny se nepodařilo použít"
|
||||
extractCsvError = "Nepodařilo se extrahovat CSV"
|
||||
extractXlsxError = "Nepodařilo se extrahovat XLSX"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Zploštit po vyplnění"
|
||||
goToPage = "Přejít na tuto stránku"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Stránka"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "pov."
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Znovu detekovat pole"
|
||||
rescanFormFields = "Znovu detekovat pole formuláře"
|
||||
save = "Uložit"
|
||||
saveShortcut = "Ctrl+S pro uložení"
|
||||
skippedEdits_one = "1 změnu se nepodařilo použít:"
|
||||
skippedEdits_other = "{{count}} změn se nepodařilo použít:"
|
||||
skippedEditsTruncated = "{{count}} dalších není uvedeno."
|
||||
unsavedChanges = "Neuložené změny"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Přidat {{count}} polí do PDF"
|
||||
empty = "Zatím nejsou nakreslena žádná pole."
|
||||
failed = "Pole se nepodařilo přidat"
|
||||
goToField = "Přejít na toto pole"
|
||||
hint = "Vyberte typ pole a poté ho nakreslete na stránku."
|
||||
placing = "Nakreslete pole {{type}} na stránku. Stisknutím Esc skončíte."
|
||||
preview = "Podržte pro náhled"
|
||||
previewHelp = "Podržením zobrazíte pole tak, jak budou vypadat po přidání, bez obrysů pro úpravy."
|
||||
removeField = "Odebrat pole"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Akce tlačítka"
|
||||
actionHelp = "Co tlačítko provede po kliknutí."
|
||||
actionNone = "Žádná"
|
||||
actionPrint = "Tisk"
|
||||
actionReset = "Resetovat formulář"
|
||||
actionSubmit = "Odeslat na URL"
|
||||
actionUri = "Otevřít URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "Adresa, kterou tlačítko otevře nebo na ni odešle data."
|
||||
addOption = "Přidat možnost"
|
||||
caption = "Popisek tlačítka"
|
||||
captionHelp = "Text vytištěný na čelní straně tlačítka."
|
||||
defaultValue = "Výchozí hodnota"
|
||||
defaultValueHelp = "Co pole obsahuje, než ho někdo vyplní. Pro prázdné pole nechte prázdné."
|
||||
fontSize = "Velikost písma"
|
||||
fontSizeHelp = "Velikost textu uvnitř pole. Nechte prázdné, aby ji prohlížeč přizpůsobil."
|
||||
label = "Štítek"
|
||||
labelHelp = "Text zobrazený osobě, která formulář vyplňuje. Nechte prázdné pro použití názvu pole."
|
||||
maxLength = "Maximální délka (comb)"
|
||||
maxLengthHelp = "Omezuje počet znaků, které se vejdou, vykreslené jako rovnoměrně rozmístěná políčka."
|
||||
multiline = "Víceřádkové"
|
||||
multilineHelp = "Umožňuje více než jeden řádek textu a zalamuje ho na okraji pole."
|
||||
multiSelect = "Povolit vícenásobný výběr"
|
||||
multiSelectHelp = "Umožňuje vybrat více než jednu možnost najednou."
|
||||
name = "Název pole"
|
||||
nameHelp = "Interní název pole. Používá se při exportu dat nebo vyplňování formuláře z jiného systému, proto musí být jedinečný a bez mezer."
|
||||
optionGap = "Mezera mezi možnostmi"
|
||||
optionGapHelp = "Mezera mezi tlačítky v bodech. Nechte prázdné, aby se rovnoměrně rozmístila po rámečku."
|
||||
optionPlaceholder = "Možnost {{n}}"
|
||||
options = "Možnosti"
|
||||
optionsEmpty = "Přidejte alespoň jednu možnost."
|
||||
optionsHelp = "Volby nabízené v seznamu. Každá se uloží tak, jak je zadána, proto je udržujte krátké a odlišné."
|
||||
optionSize = "Velikost možnosti"
|
||||
optionSizeHelp = "Šířka a výška každého tlačítka v bodech. Nechte prázdné, aby se přizpůsobily nakreslenému rámečku."
|
||||
readOnly = "Pouze pro čtení"
|
||||
readOnlyHelp = "Zobrazuje hodnotu, ale brání jejím úpravám."
|
||||
removeOption = "Odebrat možnost"
|
||||
required = "Povinné"
|
||||
requiredHelp = "Formulář nelze odeslat, dokud není toto pole vyplněno."
|
||||
signatureNote = "Pouze zástupné místo - zde nepodepisujete. Označuje, kam patří podpis, aby nástroj pro podepisování PDF (Adobe Acrobat, podpisová služba atd.) při podpisu dokumentu umístil podpis na toto místo."
|
||||
tooltip = "Tooltip"
|
||||
tooltipHelp = "Nápověda zobrazená, když někdo najede na pole v prohlížeči PDF."
|
||||
type = "Typ"
|
||||
typeHelp = "Jaký druh pole to je. Změna pole znovu vytvoří, takže jeho aktuální hodnota nebude zachována."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Vytvořit"
|
||||
fill = "Vyplnit"
|
||||
label = "Režim editoru formuláře"
|
||||
modify = "Upravit"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Uložit {{count}} změn"
|
||||
delete = "Smazat"
|
||||
empty = "Toto PDF zatím nemá žádná formulářová pole."
|
||||
failed = "Změny se nepodařilo uložit"
|
||||
groupSizeHint = "Použít velikost možnosti"
|
||||
hint = "Vyberte pole pro úpravu jeho vlastností, přetáhněte ho na stránce nebo ho smažte."
|
||||
restore = "Obnovit"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Zavřít postranní panel"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Změnit omezení a oprávnění dokumentu"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Změnit oprávnění"
|
||||
|
||||
[home.classify]
|
||||
desc = "Určete, o jaký druh dokumentu jde, a označte ho štítkem."
|
||||
title = "Klasifikovat"
|
||||
|
||||
[home.compare]
|
||||
desc = "Porovná a zobrazí rozdíly mezi 2 PDF dokumenty"
|
||||
tags = "rozdíl"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Není co hlásit."
|
||||
handoffUnavailable = "Tento prohlížeč nedovolí zpracovateli předat dokument editoru. Otevřete ho místo toho z editoru."
|
||||
noDocumentLinked = "Toto selhání není propojeno s konkrétním dokumentem, takže zde není co otevřít."
|
||||
notOnThisDevice = "Tento dokument není na tomto zařízení, takže ho zde nelze otevřít."
|
||||
occurrences = "{{count}}krát"
|
||||
open = "Oznámení"
|
||||
title = "Oznámení"
|
||||
unread = "Nepřečtené"
|
||||
|
||||
[notifications.action]
|
||||
failed = "To se nepodařilo. Zkuste to za chvíli znovu."
|
||||
unavailable = "Pro toto oznámení není k dispozici."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Zkopírováno"
|
||||
copy = "Kopírovat chybu"
|
||||
less = "Zobrazit méně"
|
||||
more = "Zobrazit celou zprávu"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Dříve"
|
||||
new = "Nové"
|
||||
|
||||
[oauth.error]
|
||||
message = "Ověření nebylo úspěšné. Toto okno můžete zavřít a zkusit to znovu."
|
||||
title = "Ověření selhalo"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "z {{allowance}} bezplatných PDFs použito"
|
||||
capSuffix_other = "z {{allowance}} bezplatných PDFs použito"
|
||||
eyebrow = "Zkušební Processor"
|
||||
statusLabel_one = "zbývá {{remaining}}"
|
||||
statusLabel_other = "zbývá {{remaining}}"
|
||||
statusLabel_one = "{{used}} využito"
|
||||
statusLabel_other = "{{used}} využito"
|
||||
sub = "Používejte PDF Editor zdarma. Plaťte za automatické zpracování PDFs."
|
||||
title_one = "Zpracujte {{allowance}} PDFs zdarma"
|
||||
title_other = "Zpracujte {{allowance}} PDFs zdarma"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Zobrazit soubor"
|
||||
viewInProcessor = "Zobrazit ve zpracovateli"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Toto selhání nebylo zaznamenáno u konkrétního dokumentu, takže zde není co otevřít."
|
||||
unattended = "Tento soubor byl dodán složkou, bucketem nebo webhookem, takže ho žádný prohlížeč neudržuje k otevření."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Zdroje"
|
||||
connectSource = "Připojit zdroj"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Pokročilé"
|
||||
back = "Zpět na zdroje"
|
||||
backToSource = "Zpět na nastavení zdroje"
|
||||
backToTypes = "Všechny typy zdrojů"
|
||||
cancel = "Zrušit"
|
||||
chooseHint = "Vyberte, odkud dokumenty pocházejí. Zašedlé konektory jsou na cestě."
|
||||
@@ -8726,7 +8845,6 @@ create = "Vytvořit zdroj"
|
||||
createTitle = "Připojit zdroj"
|
||||
delete = "Smazat"
|
||||
editTitle = "Upravit zdroj"
|
||||
enabled = "Povoleno"
|
||||
save = "Uložit změny"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: zpracovat každý soubor jednou"
|
||||
snapshot = "Snapshot: znovu načíst složku při každém spuštění"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Zahrňte podsložky, pokud jsou soubory uspořádány do vnořených složek, nebo sledujte pouze nejvyšší úroveň."
|
||||
label = "Hloubka složky"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Velikost, datum a kontrola obsahu"
|
||||
stat = "Velikost a datum změny"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Zda ponechat původní soubor na místě po jeho zpracování. Pokud původní soubor zůstane na místě, bude při dalším skenování zdroje pipeline zpracován znovu."
|
||||
label = "Režim čtení"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Spotřebovat: zpracovat každý soubor jednou"
|
||||
snapshot = "Snímek: znovu načíst složku při každém spuštění"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Zahrňte podsložky, pokud jsou soubory uspořádány do vnořených složek, nebo sledujte pouze nejvyšší úroveň."
|
||||
label = "Hloubka složky"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Váš server Stirling-PDF je offline a \"{{endpoint}}
|
||||
expired = "Vaše relace vypršela. Obnovte prosím stránku a zkuste to znovu."
|
||||
|
||||
[settings]
|
||||
backToSections = "Všechna nastavení"
|
||||
close = "Zavřít"
|
||||
title = "Nastavení"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Dokumenty a RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Hledat"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Hledat nástroje..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Pokročilé formátování"
|
||||
ai = "AI"
|
||||
automation = "Automatizace"
|
||||
developerTools = "Nástroje pro vývojáře"
|
||||
documentReview = "Kontrola dokumentu"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Sloupec {{index}}"
|
||||
convertToPdf = "Převést do PDF"
|
||||
csvStats = "{{rows}} řádků · {{columns}} sloupců · {{size}}"
|
||||
emptyFile = "Prázdný soubor"
|
||||
htmlHidePreview = "Skrýt náhled"
|
||||
htmlPreview = "Náhled HTML"
|
||||
htmlPreviewMobileHidden = "HTML stránky jsou rozvrženy pro šířky desktopu, takže zde je náhled ve výchozím nastavení vypnutý."
|
||||
htmlPreviewWarning = "Náhled HTML — externí zdroje se nemusí načíst · {{size}}"
|
||||
htmlShowPreview = "Přesto zobrazit náhled"
|
||||
invalidJson = "Neplatný JSON — zobrazen nezpracovaný obsah"
|
||||
lineNumbers = "Čísla řádků"
|
||||
loading = "Načítání..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Exportovat PDF"
|
||||
exportSelected = "Exportovat vybrané stránky"
|
||||
formFill = "Vyplnit formulář"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Další akce"
|
||||
multiTool = "Multinástroj"
|
||||
panMode = "Režim posunu"
|
||||
print = "Tisk PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Vybrat vše"
|
||||
selectByNumber = "Vybrat podle čísel stránek"
|
||||
selectLanguage = "Vyberte jazyk"
|
||||
share = "Sdílet"
|
||||
showAllTools = "Zobrazit všechny nástroje"
|
||||
showFewerTools = "Sbalit panel nástrojů"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Přepnout viditelnost anotací"
|
||||
toggleAttachments = "Přepnout přílohy"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Falsk"
|
||||
fileNotSavedToDisk = "Ikke gemt på disken"
|
||||
fileSavedToDisk = "Fil gemt på disken"
|
||||
fileSelected = "Valgt: {{filename}}"
|
||||
filesSelected = "Filer valgt"
|
||||
filesSelected = "{{count}} filer"
|
||||
font = "Skrifttype"
|
||||
fontSizeTooltip = "Størrelse på sidetalsteksten i punkter. Større tal giver større tekst."
|
||||
fontTypeTooltip = "Skrifttypefamilie til sidetal. Vælg efter din dokuments stil."
|
||||
@@ -3969,6 +3969,7 @@ back = "Tilbage"
|
||||
backToFolder = "Tilbage til {{folder}}"
|
||||
backToMyFiles = "Tilbage til Mine filer"
|
||||
breadcrumbs = "Mappesti"
|
||||
bulkActions = "Handlinger"
|
||||
cancel = "Annuller"
|
||||
classification = "Klassifikation"
|
||||
clearSelection = "Ryd markering"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Samlet størrelse"
|
||||
type = "Type"
|
||||
versionHistory = "Versionsrejse"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} filtre aktive"
|
||||
clearAll = "Ryd filtre"
|
||||
label = "Filtre"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Annuller"
|
||||
error = "Kunne ikke gemme mappe. Prøv igen."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Sortér filer"
|
||||
modifiedAsc = "Ældst først"
|
||||
modifiedDesc = "Nyest først"
|
||||
nameAsc = "Navn A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Alt gemt"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Kunne ikke anvende ændringerne"
|
||||
extractCsvError = "Kunne ikke udtrække CSV"
|
||||
extractXlsxError = "Kunne ikke udtrække XLSX"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Fladgør efter udfyldning"
|
||||
goToPage = "Gå til denne side"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Side"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "obl"
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Scan felter igen"
|
||||
rescanFormFields = "Scan formularfelter igen"
|
||||
save = "Gem"
|
||||
saveShortcut = "Ctrl+S for at gemme"
|
||||
skippedEdits_one = "1 ændring kunne ikke anvendes:"
|
||||
skippedEdits_other = "{{count}} ændringer kunne ikke anvendes:"
|
||||
skippedEditsTruncated = "{{count}} mere ikke vist."
|
||||
unsavedChanges = "Ikke-gemte ændringer"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Tilføj {{count}} felt(er) til PDF"
|
||||
empty = "Der er endnu ikke tegnet nogen felter."
|
||||
failed = "Kunne ikke tilføje felter"
|
||||
goToField = "Gå til dette felt"
|
||||
hint = "Vælg en felttype, og tegn den derefter på siden."
|
||||
placing = "Tegn et {{type}}-felt på siden. Tryk på Esc for at stoppe."
|
||||
preview = "Hold nede for at forhåndsvise"
|
||||
previewHelp = "Hold nede for at se felterne, som de vil se ud, når de er tilføjet, uden redigeringskonturerne."
|
||||
removeField = "Fjern felt"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Knaphandling"
|
||||
actionHelp = "Hvad knappen gør, når der klikkes på den."
|
||||
actionNone = "Ingen"
|
||||
actionPrint = "Udskriv"
|
||||
actionReset = "Nulstil formular"
|
||||
actionSubmit = "Send til URL"
|
||||
actionUri = "Åbn URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "Den adresse, knappen åbner eller sender til."
|
||||
addOption = "Tilføj valgmulighed"
|
||||
caption = "Knaptekst"
|
||||
captionHelp = "Teksten, der vises på knappen."
|
||||
defaultValue = "Standardværdi"
|
||||
defaultValueHelp = "Hvad feltet indeholder, før nogen udfylder det. Lad det stå tomt for et tomt felt."
|
||||
fontSize = "Skriftstørrelse"
|
||||
fontSizeHelp = "Tekststørrelse inde i feltet. Lad det stå tomt for at lade læseren tilpasse størrelsen."
|
||||
label = "Etiket"
|
||||
labelHelp = "Teksten, der vises til den, som udfylder formularen. Lad den stå tom for at bruge feltnavnet i stedet."
|
||||
maxLength = "Maks. længde (comb)"
|
||||
maxLengthHelp = "Begrænser, hvor mange tegn der kan være, tegnet som jævnt fordelte bokse."
|
||||
multiline = "Flere linjer"
|
||||
multilineHelp = "Tillader mere end én tekstlinje og ombryder ved feltets kant."
|
||||
multiSelect = "Tillad flere valg"
|
||||
multiSelectHelp = "Gør det muligt at vælge mere end én valgmulighed ad gangen."
|
||||
name = "Feltnavn"
|
||||
nameHelp = "Feltets interne navn. Bruges ved eksport af data eller udfyldning af formularen fra et andet system, så hold det unikt og uden mellemrum."
|
||||
optionGap = "Afstand mellem valgmuligheder"
|
||||
optionGapHelp = "Afstand mellem knapper, i punkter. Lad det stå tomt for at fordele dem jævnt ned gennem boksen."
|
||||
optionPlaceholder = "Valgmulighed {{n}}"
|
||||
options = "Valgmuligheder"
|
||||
optionsEmpty = "Tilføj mindst én valgmulighed."
|
||||
optionsHelp = "De valg, der tilbydes på listen. Hvert valg gemmes, som det indtastes, så hold dem korte og tydelige."
|
||||
optionSize = "Størrelse på valgmulighed"
|
||||
optionSizeHelp = "Bredde og højde for hver knap, i punkter. Lad det stå tomt for at tilpasse dem til den boks, du tegnede."
|
||||
readOnly = "Skrivebeskyttet"
|
||||
readOnlyHelp = "Viser en værdi, men forhindrer redigering."
|
||||
removeOption = "Fjern valgmulighed"
|
||||
required = "Påkrævet"
|
||||
requiredHelp = "Formularen kan ikke sendes, før dette felt er udfyldt."
|
||||
signatureNote = "Kun pladsholder - du underskriver ikke her. Den markerer, hvor en signatur hører til, så en PDF-underskriver (Adobe Acrobat, en underskriftstjeneste osv.) placerer signaturen på dette sted, når dokumentet underskrives."
|
||||
tooltip = "Værktøjstip"
|
||||
tooltipHelp = "Det tip, der vises, når nogen holder musen over feltet i en PDF-læser."
|
||||
type = "Type"
|
||||
typeHelp = "Hvilken type felt dette er. Hvis du ændrer det, genopbygges feltet, så dets aktuelle værdi videreføres ikke."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Opret"
|
||||
fill = "Udfyld"
|
||||
label = "Formularredigeringstilstand"
|
||||
modify = "Rediger"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Gem {{count}} ændring(er)"
|
||||
delete = "Slet"
|
||||
empty = "Denne PDF har endnu ingen formularfelter."
|
||||
failed = "Kunne ikke gemme ændringer"
|
||||
groupSizeHint = "Brug Størrelse på valgmulighed"
|
||||
hint = "Vælg et felt for at redigere dets egenskaber, træk det på siden, eller slet det."
|
||||
restore = "Gendan"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Luk sidepanel"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Ændr dokumentbegrænsninger og tilladelser"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Ændre Tilladelser"
|
||||
|
||||
[home.classify]
|
||||
desc = "Identificér, hvilken slags dokument dette er, og mærk det."
|
||||
title = "Klassificér"
|
||||
|
||||
[home.compare]
|
||||
desc = "Sammenligner og viser forskellene mellem 2 PDF-dokumenter"
|
||||
tags = "forskel"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Intet at rapportere."
|
||||
handoffUnavailable = "Denne browser vil ikke lade processoren sende dokumentet videre til redigeringsværktøjet. Åbn det fra redigeringsværktøjet i stedet."
|
||||
noDocumentLinked = "Denne fejl er ikke knyttet til et specifikt dokument, så der er intet at åbne her."
|
||||
notOnThisDevice = "Dette dokument findes ikke på denne enhed, så det kan ikke åbnes her."
|
||||
occurrences = "{{count}} gange"
|
||||
open = "Notifikationer"
|
||||
title = "Notifikationer"
|
||||
unread = "Ulæst"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Det virkede ikke. Prøv igen om et øjeblik."
|
||||
unavailable = "Ikke tilgængelig for denne notifikation."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Kopieret"
|
||||
copy = "Kopiér fejl"
|
||||
less = "Vis mindre"
|
||||
more = "Vis hele beskeden"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Tidligere"
|
||||
new = "Ny"
|
||||
|
||||
[oauth.error]
|
||||
message = "Godkendelsen var ikke vellykket. Du kan lukke dette vindue og prøve igen."
|
||||
title = "Godkendelse mislykkedes"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "af {{allowance}} gratis PDFs brugt"
|
||||
capSuffix_other = "af {{allowance}} gratis PDFs brugt"
|
||||
eyebrow = "Processor-prøveperiode"
|
||||
statusLabel_one = "{{remaining}} tilbage"
|
||||
statusLabel_other = "{{remaining}} tilbage"
|
||||
statusLabel_one = "{{used}} brugt"
|
||||
statusLabel_other = "{{used}} brugt"
|
||||
sub = "Brug PDF Editor gratis. Betal for at behandle PDFs automatisk."
|
||||
title_one = "Behandl {{allowance}} PDFs gratis"
|
||||
title_other = "Behandl {{allowance}} PDFs gratis"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Vis fil"
|
||||
viewInProcessor = "Vis i processoren"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Denne fejl blev ikke registreret for et specifikt dokument, så der er intet her at åbne."
|
||||
unattended = "Denne fil blev tilført af en mappe, bucket eller webhook, så ingen browser holder den klar til åbning."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Kilder"
|
||||
connectSource = "Forbind kilde"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Avanceret"
|
||||
back = "Tilbage til kilder"
|
||||
backToSource = "Tilbage til kildeopsætning"
|
||||
backToTypes = "Alle kildetyper"
|
||||
cancel = "Annuller"
|
||||
chooseHint = "Vælg, hvor dokumenter kommer fra. Nedtonede connectors er på vej."
|
||||
@@ -8726,7 +8845,6 @@ create = "Opret kilde"
|
||||
createTitle = "Forbind en kilde"
|
||||
delete = "Slet"
|
||||
editTitle = "Rediger kilde"
|
||||
enabled = "Aktiveret"
|
||||
save = "Gem ændringer"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: behandl hver fil én gang"
|
||||
snapshot = "Snapshot: genlæs mappen ved hver kørsel"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Medtag undermapper, hvis dine filer er organiseret i indlejrede mapper, eller overvåg kun det øverste niveau."
|
||||
label = "Mappedybde"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Størrelse, dato og indholdskontrol"
|
||||
stat = "Størrelse og ændringsdato"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Om den oprindelige fil skal blive liggende, efter den er blevet behandlet. Hvis den oprindelige fil bliver liggende, behandles den igen, næste gang pipelinen scanner kilden."
|
||||
label = "Læsetilstand"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Forbrug: behandl hver fil én gang"
|
||||
snapshot = "Snapshot: genlæs mappen ved hver kørsel"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Medtag undermapper, hvis dine filer er organiseret i indlejrede mapper, eller overvåg kun det øverste niveau."
|
||||
label = "Mappedybde"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Din Stirling-PDF-server er offline, og \"{{endpoint}}
|
||||
expired = "Din sesions tid har udløbet. Genlad siden og prøv igen."
|
||||
|
||||
[settings]
|
||||
backToSections = "Alle indstillinger"
|
||||
close = "Luk"
|
||||
title = "Indstillinger"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Dokumenter & RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Søg"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Søg i værktøjer..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Avanceret formatering"
|
||||
ai = "AI"
|
||||
automation = "Automatisering"
|
||||
developerTools = "Udviklerværktøjer"
|
||||
documentReview = "Dokumentgennemgang"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Kolonne {{index}}"
|
||||
convertToPdf = "Konverter til PDF"
|
||||
csvStats = "{{rows}} rækker · {{columns}} kolonner · {{size}}"
|
||||
emptyFile = "Tom fil"
|
||||
htmlHidePreview = "Skjul forhåndsvisning"
|
||||
htmlPreview = "HTML-forhåndsvisning"
|
||||
htmlPreviewMobileHidden = "HTML-sider er layoutet til desktopbredder, så forhåndsvisningen er som standard slået fra her."
|
||||
htmlPreviewWarning = "HTML-forhåndsvisning — eksterne ressourcer indlæses muligvis ikke · {{size}}"
|
||||
htmlShowPreview = "Vis forhåndsvisning alligevel"
|
||||
invalidJson = "Ugyldig JSON — viser råt indhold"
|
||||
lineNumbers = "Linjenumre"
|
||||
loading = "Indlæser..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Eksporter PDF"
|
||||
exportSelected = "Eksporter valgte sider"
|
||||
formFill = "Udfyld formular"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Flere handlinger"
|
||||
multiTool = "Multiværktøj"
|
||||
panMode = "Panoreringstilstand"
|
||||
print = "Udskriv PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Vælg alle"
|
||||
selectByNumber = "Vælg efter sidetal"
|
||||
selectLanguage = "Vælg sprog"
|
||||
share = "Del"
|
||||
showAllTools = "Vis alle værktøjer"
|
||||
showFewerTools = "Skjul værktøjslinje"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Slå anmærkninger til/fra"
|
||||
toggleAttachments = "Slå vedhæftede filer til/fra"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Falsch"
|
||||
fileNotSavedToDisk = "Not saved to disk"
|
||||
fileSavedToDisk = "File saved to disk"
|
||||
fileSelected = "Ausgewählt: {{filename}}"
|
||||
filesSelected = "Dateien ausgewählt"
|
||||
filesSelected = "{{count}} Dateien"
|
||||
font = "Schriftart"
|
||||
fontSizeTooltip = "Größe des Seitenzahl-Texts in Punkten. Größere Werte erzeugen größeren Text."
|
||||
fontTypeTooltip = "Schriftfamilie für die Seitenzahlen. Wählen Sie passend zum Dokumentstil."
|
||||
@@ -3969,6 +3969,7 @@ back = "Zurück"
|
||||
backToFolder = "Zurück zu {{folder}}"
|
||||
backToMyFiles = "Zurück zu Meine Dateien"
|
||||
breadcrumbs = "Ordnerpfad"
|
||||
bulkActions = "Aktionen"
|
||||
cancel = "Abbrechen"
|
||||
classification = "Klassifizierung"
|
||||
clearSelection = "Auswahl aufheben"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Gesamtgröße"
|
||||
type = "Typ"
|
||||
versionHistory = "Versionsverlauf"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} Filter aktiv"
|
||||
clearAll = "Filter löschen"
|
||||
label = "Filter"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Abbrechen"
|
||||
error = "Ordner konnte nicht gespeichert werden. Versuchen Sie es erneut."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Dateien sortieren"
|
||||
modifiedAsc = "Älteste zuerst"
|
||||
modifiedDesc = "Neueste zuerst"
|
||||
nameAsc = "Name A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Alles gespeichert"
|
||||
analyzingFields = "Analysiere Formularfelder..."
|
||||
applyFailed = "Die Änderungen konnten nicht angewendet werden"
|
||||
extractCsvError = "CSV konnte nicht extrahiert werden"
|
||||
extractXlsxError = "XLSX konnte nicht extrahiert werden"
|
||||
filled = "ausgefüllt"
|
||||
flattenAfterFilling = "Nach dem Ausfüllen ebnen"
|
||||
goToPage = "Zu dieser Seite wechseln"
|
||||
noFields = "Keine ausfüllbaren Formularfelder gefunden."
|
||||
page = "Seite"
|
||||
placeholderEnter = "Eingabe:"
|
||||
placeholderSelect = "Auswahl:"
|
||||
requiredAbbreviation = "erf"
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Felder erneut scannen"
|
||||
rescanFormFields = "Formularfelder erneut scannen"
|
||||
save = "Speichern"
|
||||
saveShortcut = "Strg+S zum Speichern"
|
||||
skippedEdits_one = "1 Änderung konnte nicht angewendet werden:"
|
||||
skippedEdits_other = "{{count}} Änderungen konnten nicht angewendet werden:"
|
||||
skippedEditsTruncated = "{{count}} weitere nicht aufgeführt."
|
||||
unsavedChanges = "Nicht gespeicherte Änderungen"
|
||||
|
||||
[formFill.create]
|
||||
commit = "{{count}} Feld(er) zu PDF hinzufügen"
|
||||
empty = "Noch keine Felder gezeichnet."
|
||||
failed = "Felder konnten nicht hinzugefügt werden"
|
||||
goToField = "Zu diesem Feld wechseln"
|
||||
hint = "Wählen Sie einen Feldtyp aus und zeichnen Sie ihn dann auf die Seite."
|
||||
placing = "Zeichnen Sie ein {{type}}-Feld auf die Seite. Drücken Sie Esc, um zu stoppen."
|
||||
preview = "Gedrückt halten für Vorschau"
|
||||
previewHelp = "Gedrückt halten, um die Felder so zu sehen, wie sie nach dem Hinzufügen aussehen, ohne Bearbeitungsumrisse."
|
||||
removeField = "Feld entfernen"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Button-Aktion"
|
||||
actionHelp = "Was der Button beim Anklicken ausführt."
|
||||
actionNone = "Keine"
|
||||
actionPrint = "Drucken"
|
||||
actionReset = "Formular zurücksetzen"
|
||||
actionSubmit = "An URL übermitteln"
|
||||
actionUri = "URL öffnen"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "Die Adresse, die der Button öffnet oder an die er übermittelt."
|
||||
addOption = "Option hinzufügen"
|
||||
caption = "Button-Beschriftung"
|
||||
captionHelp = "Der Text, der auf dem Button angezeigt wird."
|
||||
defaultValue = "Standardwert"
|
||||
defaultValueHelp = "Was das Feld enthält, bevor es von jemandem ausgefüllt wird. Für ein leeres Feld leer lassen."
|
||||
fontSize = "Schriftgröße"
|
||||
fontSizeHelp = "Textgröße innerhalb des Felds. Leer lassen, damit der Reader die Größe passend anpasst."
|
||||
label = "Beschriftung"
|
||||
labelHelp = "Der Text, der der Person angezeigt wird, die das Formular ausfüllt. Leer lassen, um auf den Feldnamen zurückzugreifen."
|
||||
maxLength = "Max. Länge (Comb)"
|
||||
maxLengthHelp = "Begrenzt, wie viele Zeichen hineinpassen, dargestellt als gleichmäßig verteilte Kästchen."
|
||||
multiline = "Mehrzeilig"
|
||||
multilineHelp = "Erlaubt mehr als eine Textzeile und bricht am Feldrand um."
|
||||
multiSelect = "Mehrfachauswahl erlauben"
|
||||
multiSelectHelp = "Erlaubt, mehr als eine Option gleichzeitig auszuwählen."
|
||||
name = "Feldname"
|
||||
nameHelp = "Der interne Name des Felds. Wird beim Exportieren von Daten oder beim Ausfüllen des Formulars aus einem anderen System verwendet; halten Sie ihn daher eindeutig und frei von Leerzeichen."
|
||||
optionGap = "Optionsabstand"
|
||||
optionGapHelp = "Abstand zwischen Buttons in Punkten. Leer lassen, um sie gleichmäßig im Feld zu verteilen."
|
||||
optionPlaceholder = "Option {{n}}"
|
||||
options = "Optionen"
|
||||
optionsEmpty = "Fügen Sie mindestens eine Option hinzu."
|
||||
optionsHelp = "Die in der Liste angebotenen Auswahlmöglichkeiten. Jede wird genau wie eingegeben gespeichert; halten Sie sie daher kurz und eindeutig."
|
||||
optionSize = "Optionsgröße"
|
||||
optionSizeHelp = "Breite und Höhe jedes Buttons in Punkten. Leer lassen, um sie an das gezeichnete Feld anzupassen."
|
||||
readOnly = "Schreibgeschützt"
|
||||
readOnlyHelp = "Zeigt einen Wert an, verhindert aber, dass ihn jemand bearbeitet."
|
||||
removeOption = "Option entfernen"
|
||||
required = "Erforderlich"
|
||||
requiredHelp = "Das Formular kann erst übermittelt werden, wenn dieses Feld ausgefüllt ist."
|
||||
signatureNote = "Nur Platzhalter – Sie signieren hier nicht. Er markiert, wo eine Signatur hingehört, damit ein PDF-Signierer (Adobe Acrobat, ein Signaturdienst usw.) die Signatur an dieser Stelle platziert, wenn das Dokument signiert wird."
|
||||
tooltip = "Tooltip"
|
||||
tooltipHelp = "Der Hinweis, der angezeigt wird, wenn jemand in einem PDF-Reader mit der Maus über das Feld fährt."
|
||||
type = "Typ"
|
||||
typeHelp = "Welche Art von Feld dies ist. Eine Änderung erstellt das Feld neu, daher wird sein aktueller Wert nicht übernommen."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Erstellen"
|
||||
fill = "Ausfüllen"
|
||||
label = "Formulareditor-Modus"
|
||||
modify = "Ändern"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "{{count}} Änderung(en) speichern"
|
||||
delete = "Löschen"
|
||||
empty = "Dieses PDF hat noch keine Formularfelder."
|
||||
failed = "Änderungen konnten nicht gespeichert werden"
|
||||
groupSizeHint = "Optionsgröße verwenden"
|
||||
hint = "Wählen Sie ein Feld aus, um seine Eigenschaften zu bearbeiten, ziehen Sie es auf der Seite oder löschen Sie es."
|
||||
restore = "Wiederherstellen"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Seitenleiste schließen"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Dokumentbeschränkungen und -berechtigungen ändern"
|
||||
tags = "berechtigungen,einschränkungen,rechte,zugriffskontrolle,erlauben,verweigern,drucken,kopieren,bearbeiten,berechtigungen ändern,sicherheitseinstellungen,benutzerrechte"
|
||||
title = "Berechtigungen ändern"
|
||||
|
||||
[home.classify]
|
||||
desc = "Ermitteln, um welche Art von Dokument es sich handelt, und es mit Tags versehen."
|
||||
title = "Klassifizieren"
|
||||
|
||||
[home.compare]
|
||||
desc = "Vergleicht und zeigt die Unterschiede zwischen zwei PDF-Dokumenten an"
|
||||
tags = "unterschied"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Nichts zu berichten."
|
||||
handoffUnavailable = "Dieser Browser erlaubt nicht, dass der Prozessor das Dokument an den Editor übergibt. Öffnen Sie es stattdessen über den Editor."
|
||||
noDocumentLinked = "Dieser Fehler ist keinem bestimmten Dokument zugeordnet, daher gibt es hier nichts zu öffnen."
|
||||
notOnThisDevice = "Dieses Dokument befindet sich nicht auf diesem Gerät, daher kann es hier nicht geöffnet werden."
|
||||
occurrences = "{{count}} Mal"
|
||||
open = "Benachrichtigungen"
|
||||
title = "Benachrichtigungen"
|
||||
unread = "Ungelesen"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Das hat nicht funktioniert. Versuchen Sie es in einem Moment erneut."
|
||||
unavailable = "Für diese Benachrichtigung nicht verfügbar."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Kopiert"
|
||||
copy = "Fehler kopieren"
|
||||
less = "Weniger anzeigen"
|
||||
more = "Vollständige Nachricht anzeigen"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Früher"
|
||||
new = "Neu"
|
||||
|
||||
[oauth.error]
|
||||
message = "Die Authentifizierung war nicht erfolgreich. Sie können dieses Fenster schließen und es erneut versuchen."
|
||||
title = "Authentifizierung fehlgeschlagen"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "von {{allowance}} kostenlosen PDFs genutzt"
|
||||
capSuffix_other = "von {{allowance}} kostenlosen PDFs genutzt"
|
||||
eyebrow = "Processor-Testversion"
|
||||
statusLabel_one = "{{remaining}} übrig"
|
||||
statusLabel_other = "{{remaining}} übrig"
|
||||
statusLabel_one = "{{used}} verbraucht"
|
||||
statusLabel_other = "{{used}} verbraucht"
|
||||
sub = "Nutzen Sie den PDF Editor kostenlos. Zahlen Sie für die automatische Verarbeitung von PDFs."
|
||||
title_one = "{{allowance}} PDFs kostenlos verarbeiten"
|
||||
title_other = "{{allowance}} PDFs kostenlos verarbeiten"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Datei anzeigen"
|
||||
viewInProcessor = "Im Prozessor anzeigen"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Dieser Fehler wurde keinem bestimmten Dokument zugeordnet, daher gibt es hier nichts zu öffnen."
|
||||
unattended = "Diese Datei wurde von einem Ordner, Bucket oder Webhook eingespeist, daher hält kein Browser sie zum Öffnen bereit."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Quellen"
|
||||
connectSource = "Quelle verbinden"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Erweitert"
|
||||
back = "Zurück zu Quellen"
|
||||
backToSource = "Zur Quelleneinrichtung zurück"
|
||||
backToTypes = "Alle Quellentypen"
|
||||
cancel = "Abbrechen"
|
||||
chooseHint = "Wählen Sie aus, woher Dokumente kommen. Ausgegraute Connectors sind in Vorbereitung."
|
||||
@@ -8726,7 +8845,6 @@ create = "Quelle erstellen"
|
||||
createTitle = "Quelle verbinden"
|
||||
delete = "Löschen"
|
||||
editTitle = "Quelle bearbeiten"
|
||||
enabled = "Aktiviert"
|
||||
save = "Änderungen speichern"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Verbrauchen: Jede Datei einmal verarbeiten"
|
||||
snapshot = "Snapshot: Ordner bei jedem Lauf erneut lesen"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Unterordner einschließen, wenn Ihre Dateien in verschachtelten Ordnern organisiert sind, oder nur die oberste Ebene überwachen."
|
||||
label = "Ordnertiefe"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Größe, Datum und Inhaltsprüfung"
|
||||
stat = "Größe und Änderungsdatum"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Ob die Originaldatei nach der Verarbeitung an ihrem Ort bleiben soll. Wenn die Originaldatei an ihrem Ort bleibt, wird sie beim nächsten Scan der Quelle durch die Pipeline erneut verarbeitet."
|
||||
label = "Lesemodus"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Verbrauchen: Jede Datei einmal verarbeiten"
|
||||
snapshot = "Snapshot: Ordner bei jedem Lauf erneut lesen"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Unterordner einschließen, wenn Ihre Dateien in verschachtelten Ordnern organisiert sind, oder nur die oberste Ebene überwachen."
|
||||
label = "Ordnertiefe"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Ihr Stirling-PDF-Server ist offline und „{{endpoint
|
||||
expired = "Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut."
|
||||
|
||||
[settings]
|
||||
backToSections = "Alle Einstellungen"
|
||||
close = "Schließen"
|
||||
title = "Einstellungen"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Dokumente & RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Suchen"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Werkzeuge suchen..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Erweiterte Formatierung"
|
||||
ai = "AI"
|
||||
automation = "Automatisierung"
|
||||
developerTools = "Entwicklerwerkzeuge"
|
||||
documentReview = "Dokumentenprüfung"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Spalte {{index}}"
|
||||
convertToPdf = "In PDF konvertieren"
|
||||
csvStats = "{{rows}} Zeilen · {{columns}} Spalten · {{size}}"
|
||||
emptyFile = "Leere Datei"
|
||||
htmlHidePreview = "Vorschau ausblenden"
|
||||
htmlPreview = "HTML-Vorschau"
|
||||
htmlPreviewMobileHidden = "HTML-Seiten sind für Desktop-Breiten ausgelegt, daher ist die Vorschau hier standardmäßig deaktiviert."
|
||||
htmlPreviewWarning = "HTML-Vorschau — externe Ressourcen werden möglicherweise nicht geladen · {{size}}"
|
||||
htmlShowPreview = "Vorschau trotzdem anzeigen"
|
||||
invalidJson = "Ungültiges JSON — Rohinhalt wird angezeigt"
|
||||
lineNumbers = "Zeilennummern"
|
||||
loading = "Laden..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "PDF exportieren"
|
||||
exportSelected = "Ausgewählte Seiten exportieren"
|
||||
formFill = "Formular ausfüllen"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Weitere Aktionen"
|
||||
multiTool = "Multitool"
|
||||
panMode = "Verschiebemodus"
|
||||
print = "PDF drucken"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Alle auswählen"
|
||||
selectByNumber = "Nach Seitenzahlen auswählen"
|
||||
selectLanguage = "Sprache auswählen"
|
||||
share = "Teilen"
|
||||
showAllTools = "Alle Tools anzeigen"
|
||||
showFewerTools = "Symbolleiste einklappen"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Anmerkungen ein-/ausblenden"
|
||||
toggleAttachments = "Anhänge ein-/ausblenden"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Ψευδές"
|
||||
fileNotSavedToDisk = "Δεν αποθηκεύτηκε στον δίσκο"
|
||||
fileSavedToDisk = "Το αρχείο αποθηκεύτηκε στον δίσκο"
|
||||
fileSelected = "Επιλέχθηκε: {{filename}}"
|
||||
filesSelected = "αρχεία επιλεγμένα"
|
||||
filesSelected = "{{count}} αρχεία"
|
||||
font = "Γραμματοσειρά"
|
||||
fontSizeTooltip = "Μέγεθος του κειμένου του αριθμού σελίδας σε σημεία. Μεγαλύτεροι αριθμοί δημιουργούν μεγαλύτερο κείμενο."
|
||||
fontTypeTooltip = "Οικογένεια γραμματοσειράς για τους αριθμούς σελίδων. Επιλέξτε σύμφωνα με το στυλ του εγγράφου σας."
|
||||
@@ -3969,6 +3969,7 @@ back = "Πίσω"
|
||||
backToFolder = "Πίσω στον φάκελο {{folder}}"
|
||||
backToMyFiles = "Πίσω στα Αρχεία μου"
|
||||
breadcrumbs = "Διαδρομή φακέλου"
|
||||
bulkActions = "Ενέργειες"
|
||||
cancel = "Άκυρο"
|
||||
classification = "Ταξινόμηση"
|
||||
clearSelection = "Εκκαθάριση επιλογής"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Συνολικό μέγεθος"
|
||||
type = "Τύπος"
|
||||
versionHistory = "Πορεία εκδόσεων"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} ενεργά φίλτρα"
|
||||
clearAll = "Εκκαθάριση φίλτρων"
|
||||
label = "Φίλτρα"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Άκυρο"
|
||||
error = "Δεν ήταν δυνατή η αποθήκευση του φακέλου. Δοκιμάστε ξανά."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Ταξινόμηση αρχείων"
|
||||
modifiedAsc = "Παλαιότερα πρώτα"
|
||||
modifiedDesc = "Πιο πρόσφατα πρώτα"
|
||||
nameAsc = "Όνομα A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Όλα αποθηκεύτηκαν"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Δεν ήταν δυνατή η εφαρμογή των αλλαγών"
|
||||
extractCsvError = "Απέτυχε η εξαγωγή CSV"
|
||||
extractXlsxError = "Απέτυχε η εξαγωγή XLSX"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Επιπεδοποίηση μετά τη συμπλήρωση"
|
||||
goToPage = "Μετάβαση σε αυτήν τη σελίδα"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Σελίδα"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "απαιτ."
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Επανασάρωση πεδίων"
|
||||
rescanFormFields = "Επανασάρωση πεδίων φόρμας"
|
||||
save = "Αποθήκευση"
|
||||
saveShortcut = "Ctrl+S για αποθήκευση"
|
||||
skippedEdits_one = "Δεν ήταν δυνατή η εφαρμογή 1 αλλαγής:"
|
||||
skippedEdits_other = "Δεν ήταν δυνατή η εφαρμογή {{count}} αλλαγών:"
|
||||
skippedEditsTruncated = "{{count}} ακόμη δεν εμφανίζονται."
|
||||
unsavedChanges = "Μη αποθηκευμένες αλλαγές"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Προσθήκη {{count}} πεδίου/πεδίων στο PDF"
|
||||
empty = "Δεν έχουν σχεδιαστεί πεδία ακόμη."
|
||||
failed = "Απέτυχε η προσθήκη πεδίων"
|
||||
goToField = "Μετάβαση σε αυτό το πεδίο"
|
||||
hint = "Επιλέξτε έναν τύπο πεδίου και, στη συνέχεια, σχεδιάστε τον στη σελίδα."
|
||||
placing = "Σχεδιάστε ένα πεδίο {{type}} στη σελίδα. Πατήστε Esc για διακοπή."
|
||||
preview = "Κρατήστε πατημένο για προεπισκόπηση"
|
||||
previewHelp = "Κρατήστε πατημένο για να δείτε τα πεδία όπως θα φαίνονται όταν προστεθούν, χωρίς τα περιγράμματα επεξεργασίας."
|
||||
removeField = "Αφαίρεση πεδίου"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Ενέργεια κουμπιού"
|
||||
actionHelp = "Τι κάνει το κουμπί όταν πατηθεί."
|
||||
actionNone = "Καμία"
|
||||
actionPrint = "Εκτύπωση"
|
||||
actionReset = "Επαναφορά φόρμας"
|
||||
actionSubmit = "Υποβολή σε URL"
|
||||
actionUri = "Άνοιγμα URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "Η διεύθυνση που ανοίγει ή στην οποία υποβάλλει το κουμπί."
|
||||
addOption = "Προσθήκη επιλογής"
|
||||
caption = "Λεζάντα κουμπιού"
|
||||
captionHelp = "Το κείμενο που εκτυπώνεται στην επιφάνεια του κουμπιού."
|
||||
defaultValue = "Προεπιλεγμένη τιμή"
|
||||
defaultValueHelp = "Τι περιέχει το πεδίο πριν το συμπληρώσει κάποιος. Αφήστε κενό για κενό πεδίο."
|
||||
fontSize = "Μέγεθος γραμματοσειράς"
|
||||
fontSizeHelp = "Μέγεθος κειμένου μέσα στο πεδίο. Αφήστε κενό για να προσαρμόσει ο αναγνώστης το μέγεθος ώστε να χωράει."
|
||||
label = "Ετικέτα"
|
||||
labelHelp = "Το κείμενο που εμφανίζεται σε όποιον συμπληρώνει τη φόρμα. Αφήστε το κενό για να χρησιμοποιηθεί το όνομα του πεδίου."
|
||||
maxLength = "Μέγιστο μήκος (comb)"
|
||||
maxLengthHelp = "Περιορίζει πόσοι χαρακτήρες χωρούν, σχεδιασμένοι ως ομοιόμορφα διατεταγμένα κουτιά."
|
||||
multiline = "Πολλαπλών γραμμών"
|
||||
multilineHelp = "Επιτρέπει περισσότερες από μία γραμμές κειμένου και αναδίπλωση στην άκρη του πεδίου."
|
||||
multiSelect = "Να επιτρέπεται πολλαπλή επιλογή"
|
||||
multiSelectHelp = "Επιτρέπει την επιλογή περισσότερων από μία επιλογών ταυτόχρονα."
|
||||
name = "Όνομα πεδίου"
|
||||
nameHelp = "Το εσωτερικό όνομα του πεδίου. Χρησιμοποιείται κατά την εξαγωγή δεδομένων ή τη συμπλήρωση της φόρμας από άλλο σύστημα, οπότε κρατήστε το μοναδικό και χωρίς κενά."
|
||||
optionGap = "Απόσταση επιλογών"
|
||||
optionGapHelp = "Κενό μεταξύ κουμπιών, σε στιγμές. Αφήστε κενό για να κατανεμηθούν ομοιόμορφα στο κουτί."
|
||||
optionPlaceholder = "Επιλογή {{n}}"
|
||||
options = "Επιλογές"
|
||||
optionsEmpty = "Προσθέστε τουλάχιστον μία επιλογή."
|
||||
optionsHelp = "Οι επιλογές που προσφέρονται στη λίστα. Κάθε μία αποθηκεύεται όπως πληκτρολογείται, οπότε κρατήστε τις σύντομες και διακριτές."
|
||||
optionSize = "Μέγεθος επιλογής"
|
||||
optionSizeHelp = "Πλάτος και ύψος κάθε κουμπιού, σε στιγμές. Αφήστε κενό για να προσαρμοστούν στο κουτί που σχεδιάσατε."
|
||||
readOnly = "Μόνο για ανάγνωση"
|
||||
readOnlyHelp = "Εμφανίζει μια τιμή αλλά εμποδίζει την επεξεργασία της."
|
||||
removeOption = "Αφαίρεση επιλογής"
|
||||
required = "Υποχρεωτικό"
|
||||
requiredHelp = "Η φόρμα δεν μπορεί να υποβληθεί μέχρι να συμπληρωθεί αυτό το πεδίο."
|
||||
signatureNote = "Μόνο σύμβολο κράτησης θέσης - δεν υπογράφετε εδώ. Επισημαίνει πού ανήκει μια υπογραφή, ώστε ένας υπογράφων PDF (Adobe Acrobat, υπηρεσία υπογραφής κ.λπ.) να τοποθετήσει την υπογραφή σε αυτό το σημείο όταν υπογραφεί το έγγραφο."
|
||||
tooltip = "Tooltip"
|
||||
tooltipHelp = "Η υπόδειξη που εμφανίζεται όταν κάποιος περνά τον δείκτη πάνω από το πεδίο σε έναν αναγνώστη PDF."
|
||||
type = "Τύπος"
|
||||
typeHelp = "Τι είδους πεδίο είναι αυτό. Η αλλαγή του αναδημιουργεί το πεδίο, οπότε η τρέχουσα τιμή του δεν μεταφέρεται."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Δημιουργία"
|
||||
fill = "Συμπλήρωση"
|
||||
label = "Λειτουργία επεξεργαστή φόρμας"
|
||||
modify = "Τροποποίηση"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Αποθήκευση {{count}} αλλαγής/αλλαγών"
|
||||
delete = "Διαγραφή"
|
||||
empty = "Αυτό το PDF δεν έχει ακόμη πεδία φόρμας."
|
||||
failed = "Απέτυχε η αποθήκευση αλλαγών"
|
||||
groupSizeHint = "Χρήση μεγέθους επιλογής"
|
||||
hint = "Επιλέξτε ένα πεδίο για να επεξεργαστείτε τις ιδιότητές του, σύρετέ το στη σελίδα ή διαγράψτε το."
|
||||
restore = "Επαναφορά"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Κλείσιμο πλευρικής γραμμής"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Αλλαγή περιορισμών και δικαιωμάτων εγγ
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Αλλαγή δικαιωμάτων"
|
||||
|
||||
[home.classify]
|
||||
desc = "Προσδιορίστε τι είδους έγγραφο είναι αυτό και προσθέστε του ετικέτα."
|
||||
title = "Ταξινόμηση"
|
||||
|
||||
[home.compare]
|
||||
desc = "Σύγκριση και εμφάνιση διαφορών μεταξύ 2 εγγράφων PDF"
|
||||
tags = "διαφορά"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Δεν υπάρχει τίποτα προς αναφορά."
|
||||
handoffUnavailable = "Αυτό το πρόγραμμα περιήγησης δεν θα επιτρέψει στον επεξεργαστή να περάσει το έγγραφο στον επεξεργαστή φόρμας. Ανοίξτε το από τον επεξεργαστή φόρμας αντ' αυτού."
|
||||
noDocumentLinked = "Αυτή η αποτυχία δεν συνδέεται με συγκεκριμένο έγγραφο, επομένως δεν υπάρχει κάτι να ανοίξετε εδώ."
|
||||
notOnThisDevice = "Αυτό το έγγραφο δεν βρίσκεται σε αυτήν τη συσκευή, επομένως δεν μπορεί να ανοιχτεί εδώ."
|
||||
occurrences = "{{count}} φορές"
|
||||
open = "Ειδοποιήσεις"
|
||||
title = "Ειδοποιήσεις"
|
||||
unread = "Μη αναγνωσμένα"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Αυτό δεν λειτούργησε. Δοκιμάστε ξανά σε λίγο."
|
||||
unavailable = "Δεν είναι διαθέσιμο για αυτήν την ειδοποίηση."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Αντιγράφηκε"
|
||||
copy = "Αντιγραφή σφάλματος"
|
||||
less = "Εμφάνιση λιγότερων"
|
||||
more = "Εμφάνιση πλήρους μηνύματος"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Παλαιότερα"
|
||||
new = "Νέα"
|
||||
|
||||
[oauth.error]
|
||||
message = "Η ταυτοποίηση δεν ήταν επιτυχής. Μπορείτε να κλείσετε αυτό το παράθυρο και να δοκιμάσετε ξανά."
|
||||
title = "Αποτυχία ελέγχου ταυτότητας"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "από {{allowance}} δωρεάν PDFs χρησιμοποιήθηκε"
|
||||
capSuffix_other = "από {{allowance}} δωρεάν PDFs χρησιμοποιήθηκαν"
|
||||
eyebrow = "Δοκιμή Processor"
|
||||
statusLabel_one = "{{remaining}} απομένει"
|
||||
statusLabel_other = "{{remaining}} απομένουν"
|
||||
statusLabel_one = "{{used}} χρησιμοποιήθηκε"
|
||||
statusLabel_other = "{{used}} χρησιμοποιήθηκαν"
|
||||
sub = "Χρησιμοποιήστε τον PDF Editor δωρεάν. Πληρώστε για αυτόματη επεξεργασία PDFs."
|
||||
title_one = "Επεξεργαστείτε {{allowance}} PDFs δωρεάν"
|
||||
title_other = "Επεξεργαστείτε {{allowance}} PDFs δωρεάν"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Προβολή αρχείου"
|
||||
viewInProcessor = "Προβολή στον επεξεργαστή"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Αυτή η αποτυχία δεν καταγράφηκε για συγκεκριμένο έγγραφο, επομένως δεν υπάρχει κάτι να ανοίξετε εδώ."
|
||||
unattended = "Αυτό το αρχείο τροφοδοτήθηκε από φάκελο, bucket ή webhook, επομένως κανένα πρόγραμμα περιήγησης δεν το έχει για άνοιγμα."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Πηγές"
|
||||
connectSource = "Σύνδεση πηγής"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Σύνθετα"
|
||||
back = "Πίσω στις πηγές"
|
||||
backToSource = "Επιστροφή στη ρύθμιση προέλευσης"
|
||||
backToTypes = "Όλοι οι τύποι πηγών"
|
||||
cancel = "Ακύρωση"
|
||||
chooseHint = "Επιλέξτε από πού προέρχονται τα έγγραφα. Οι γκριζαρισμένοι connectors έρχονται σύντομα."
|
||||
@@ -8726,7 +8845,6 @@ create = "Δημιουργία πηγής"
|
||||
createTitle = "Σύνδεση πηγής"
|
||||
delete = "Διαγραφή"
|
||||
editTitle = "Επεξεργασία πηγής"
|
||||
enabled = "Ενεργοποιημένο"
|
||||
save = "Αποθήκευση αλλαγών"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Κατανάλωση: επεξεργασία κάθε αρχείου
|
||||
snapshot = "Snapshot: επανάγνωση του φακέλου σε κάθε εκτέλεση"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Συμπεριλάβετε υποφακέλους αν τα αρχεία σας είναι οργανωμένα σε ένθετους φακέλους ή παρακολουθήστε μόνο το ανώτερο επίπεδο."
|
||||
label = "Βάθος φακέλου"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Έλεγχος μεγέθους, ημερομηνίας και περι
|
||||
stat = "Μέγεθος και ημερομηνία τροποποίησης"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Αν θα παραμείνει το αρχικό αρχείο στη θέση του μετά την επεξεργασία του. Αν το αρχικό αρχείο παραμείνει στη θέση του, θα επανεπεξεργαστεί την επόμενη φορά που η διοχέτευση θα σαρώσει την προέλευση."
|
||||
label = "Λειτουργία ανάγνωσης"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Κατανάλωση: επεξεργασία κάθε αρχείου
|
||||
snapshot = "Στιγμιότυπο: επανανάγνωση του φακέλου σε κάθε εκτέλεση"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Συμπεριλάβετε υποφακέλους αν τα αρχεία σας είναι οργανωμένα σε ένθετους φακέλους ή παρακολουθήστε μόνο το ανώτερο επίπεδο."
|
||||
label = "Βάθος φακέλου"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Ο διακομιστής σας Stirling-PDF είν
|
||||
expired = "Η συνεδρία σας έληξε. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά."
|
||||
|
||||
[settings]
|
||||
backToSections = "Όλες οι ρυθμίσεις"
|
||||
close = "Κλείσιμο"
|
||||
title = "Ρυθμίσεις"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Έγγραφα & RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Αναζήτηση"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Αναζήτηση εργαλείων..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Προχωρημένη μορφοποίηση"
|
||||
ai = "AI"
|
||||
automation = "Αυτοματοποίηση"
|
||||
developerTools = "Εργαλεία προγραμματιστή"
|
||||
documentReview = "Έλεγχος εγγράφου"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Στήλη {{index}}"
|
||||
convertToPdf = "Μετατροπή σε PDF"
|
||||
csvStats = "{{rows}} γραμμές · {{columns}} στήλες · {{size}}"
|
||||
emptyFile = "Κενό αρχείο"
|
||||
htmlHidePreview = "Απόκρυψη προεπισκόπησης"
|
||||
htmlPreview = "Προεπισκόπηση HTML"
|
||||
htmlPreviewMobileHidden = "Οι σελίδες HTML είναι διαμορφωμένες για πλάτη επιφάνειας εργασίας, οπότε η προεπισκόπηση είναι απενεργοποιημένη από προεπιλογή εδώ."
|
||||
htmlPreviewWarning = "Προεπισκόπηση HTML — ενδέχεται να μη φορτωθούν εξωτερικοί πόροι · {{size}}"
|
||||
htmlShowPreview = "Εμφάνιση προεπισκόπησης ούτως ή άλλως"
|
||||
invalidJson = "Μη έγκυρο JSON — εμφάνιση ακατέργαστου περιεχομένου"
|
||||
lineNumbers = "Αριθμοί γραμμών"
|
||||
loading = "Φόρτωση..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Εξαγωγή PDF"
|
||||
exportSelected = "Εξαγωγή επιλεγμένων σελίδων"
|
||||
formFill = "Συμπλήρωση φόρμας"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Περισσότερες ενέργειες"
|
||||
multiTool = "Πολυεργαλείο"
|
||||
panMode = "Λειτουργία μετακίνησης"
|
||||
print = "Εκτύπωση PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Επιλογή όλων"
|
||||
selectByNumber = "Επιλογή βάσει αριθμών σελίδων"
|
||||
selectLanguage = "Επιλέξτε γλώσσα"
|
||||
share = "Κοινή χρήση"
|
||||
showAllTools = "Εμφάνιση όλων των εργαλείων"
|
||||
showFewerTools = "Σύμπτυξη γραμμής εργαλείων"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Εναλλαγή ορατότητας σχολιασμών"
|
||||
toggleAttachments = "Εναλλαγή συνημμένων"
|
||||
|
||||
@@ -3969,6 +3969,7 @@ back = "Back"
|
||||
backToFolder = "Back to {{folder}}"
|
||||
backToMyFiles = "Back to My Files"
|
||||
breadcrumbs = "Folder path"
|
||||
bulkActions = "Actions"
|
||||
cancel = "Cancel"
|
||||
classification = "Classification"
|
||||
clearSelection = "Clear selection"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Total size"
|
||||
type = "Type"
|
||||
versionHistory = "Version journey"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} filters active"
|
||||
clearAll = "Clear filters"
|
||||
label = "Filters"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Cancel"
|
||||
error = "Could not save folder. Try again."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Sort files"
|
||||
modifiedAsc = "Oldest first"
|
||||
modifiedDesc = "Recent first"
|
||||
nameAsc = "Name A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "All saved"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Could not apply the changes"
|
||||
extractCsvError = "Failed to extract CSV"
|
||||
extractXlsxError = "Failed to extract XLSX"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Flatten after filling"
|
||||
goToPage = "Go to this page"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Page"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "req"
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Re-scan fields"
|
||||
rescanFormFields = "Re-scan form fields"
|
||||
save = "Save"
|
||||
saveShortcut = "Ctrl+S to save"
|
||||
skippedEdits_one = "1 change could not be applied:"
|
||||
skippedEdits_other = "{{count}} changes could not be applied:"
|
||||
skippedEditsTruncated = "{{count}} more not listed."
|
||||
unsavedChanges = "Unsaved changes"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Add {{count}} field(s) to PDF"
|
||||
empty = "No fields drawn yet."
|
||||
failed = "Failed to add fields"
|
||||
goToField = "Go to this field"
|
||||
hint = "Pick a field type, then draw it on the page."
|
||||
placing = "Draw a {{type}} field on the page. Press Esc to stop."
|
||||
preview = "Hold to preview"
|
||||
previewHelp = "Hold to see the fields as they will look once added, without the editing outlines."
|
||||
removeField = "Remove field"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Button action"
|
||||
actionHelp = "What the button does when clicked."
|
||||
actionNone = "None"
|
||||
actionPrint = "Print"
|
||||
actionReset = "Reset form"
|
||||
actionSubmit = "Submit to URL"
|
||||
actionUri = "Open URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "The address the button opens or submits to."
|
||||
addOption = "Add option"
|
||||
caption = "Button caption"
|
||||
captionHelp = "The text printed on the button face."
|
||||
defaultValue = "Default value"
|
||||
defaultValueHelp = "What the field contains before anyone fills it in. Leave blank for an empty field."
|
||||
fontSize = "Font size"
|
||||
fontSizeHelp = "Text size inside the field. Leave blank to let the reader size it to fit."
|
||||
label = "Label"
|
||||
labelHelp = "The wording shown to whoever fills the form. Leave it blank to fall back to the field name."
|
||||
maxLength = "Max length (comb)"
|
||||
maxLengthHelp = "Caps how many characters fit, drawn as evenly spaced boxes."
|
||||
multiline = "Multi-line"
|
||||
multilineHelp = "Allows more than one line of text and wraps at the field's edge."
|
||||
multiSelect = "Allow multiple selection"
|
||||
multiSelectHelp = "Lets more than one option be chosen at once."
|
||||
name = "Field name"
|
||||
nameHelp = "The field's internal name. Used when exporting data or filling the form from another system, so keep it unique and free of spaces."
|
||||
optionGap = "Option spacing"
|
||||
optionGapHelp = "Gap between buttons, in points. Leave blank to spread them evenly down the box."
|
||||
optionPlaceholder = "Option {{n}}"
|
||||
options = "Options"
|
||||
optionsEmpty = "Add at least one option."
|
||||
optionsHelp = "The choices offered in the list. Each one is stored as typed, so keep them short and distinct."
|
||||
optionSize = "Option size"
|
||||
optionSizeHelp = "Width and height of each button, in points. Leave blank to fit them to the box you drew."
|
||||
readOnly = "Read-only"
|
||||
readOnlyHelp = "Shows a value but stops anyone editing it."
|
||||
removeOption = "Remove option"
|
||||
required = "Required"
|
||||
requiredHelp = "The form cannot be submitted until this field is filled in."
|
||||
signatureNote = "Placeholder only - you don't sign here. It marks where a signature belongs so a PDF signer (Adobe Acrobat, a signing service, etc.) places the signature in this spot when the document is signed."
|
||||
tooltip = "Tooltip"
|
||||
tooltipHelp = "The hint shown when someone hovers the field in a PDF reader."
|
||||
type = "Type"
|
||||
typeHelp = "What kind of field this is. Changing it rebuilds the field, so its current value is not carried over."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Create"
|
||||
fill = "Fill"
|
||||
label = "Form editor mode"
|
||||
modify = "Modify"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Save {{count}} change(s)"
|
||||
delete = "Delete"
|
||||
empty = "This PDF has no form fields yet."
|
||||
failed = "Failed to save changes"
|
||||
groupSizeHint = "Use Option size"
|
||||
hint = "Select a field to edit its properties, drag it on the page, or delete it."
|
||||
restore = "Restore"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Close sidebar"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Change document restrictions and permissions"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Change Permissions"
|
||||
|
||||
[home.classify]
|
||||
desc = "Identify what kind of document this is and tag it."
|
||||
title = "Classify"
|
||||
|
||||
[home.compare]
|
||||
desc = "Compares and shows the differences between 2 PDF Documents"
|
||||
tags = "difference,compare,diff,compare PDFs,compare documents,find differences,show differences,changes,what changed,track changes,revisions,version compare,side by side,contrast,delta"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Nothing to report."
|
||||
handoffUnavailable = "This browser will not let the processor pass the document to the editor. Open it from the editor instead."
|
||||
noDocumentLinked = "This failure is not linked to a specific document, so there is nothing to open here."
|
||||
notOnThisDevice = "This document is not on this device, so it cannot be opened here."
|
||||
occurrences = "{{count}} times"
|
||||
open = "Notifications"
|
||||
title = "Notifications"
|
||||
unread = "Unread"
|
||||
|
||||
[notifications.action]
|
||||
failed = "That did not work. Try again in a moment."
|
||||
unavailable = "Not available for this notification."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Copied"
|
||||
copy = "Copy error"
|
||||
less = "Show less"
|
||||
more = "Show full message"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Earlier"
|
||||
new = "New"
|
||||
|
||||
[oauth.error]
|
||||
message = "Authentication was not successful. You can close this window and try again."
|
||||
title = "Authentication Failed"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs used"
|
||||
capSuffix_one = "of {{allowance}} free PDFs used"
|
||||
capSuffix_other = "of {{allowance}} free PDFs used"
|
||||
eyebrow = "Processor trial"
|
||||
statusLabel_one = "{{remaining}} left"
|
||||
statusLabel_other = "{{remaining}} left"
|
||||
statusLabel_one = "{{used}} used"
|
||||
statusLabel_other = "{{used}} used"
|
||||
sub = "Use the PDF Editor for free. Pay to process PDFs automatically."
|
||||
title_one = "Process {{allowance}} PDFs free"
|
||||
title_other = "Process {{allowance}} PDFs free"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "View file"
|
||||
viewInProcessor = "View in processor"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "This failure was not recorded against a specific document, so there is nothing here to open."
|
||||
unattended = "This file was fed by a folder, bucket or webhook, so nobody's browser is holding it to open."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Sources"
|
||||
connectSource = "Connect source"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Advanced"
|
||||
back = "Back to sources"
|
||||
backToSource = "Back to source setup"
|
||||
backToTypes = "All source types"
|
||||
cancel = "Cancel"
|
||||
chooseHint = "Choose where documents come from. Greyed-out connectors are on the way."
|
||||
@@ -8726,7 +8845,6 @@ create = "Create source"
|
||||
createTitle = "Connect a source"
|
||||
delete = "Delete"
|
||||
editTitle = "Edit source"
|
||||
enabled = "Enabled"
|
||||
save = "Save changes"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: process each file once"
|
||||
snapshot = "Snapshot: re-read the folder every run"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Include subfolders if your files are organised into nested folders, or watch only the top level."
|
||||
label = "Folder depth"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Size, date and content check"
|
||||
stat = "Size and date modified"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Whether to leave the original file in place after it has been processed. If the original file is left in place, it will be re-processed next time the pipeline scans the source."
|
||||
label = "Read mode"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Consume: process each file once"
|
||||
snapshot = "Snapshot: re-read the folder every run"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Include subfolders if your files are organised into nested folders, or watch only the top level."
|
||||
label = "Folder depth"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Your Stirling-PDF server is offline and \"{{endpoint}
|
||||
expired = "Your session has expired. Please refresh the page and try again."
|
||||
|
||||
[settings]
|
||||
backToSections = "All settings"
|
||||
close = "Close"
|
||||
title = "Settings"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Documents & RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Search"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Search tools..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Advanced Formatting"
|
||||
ai = "AI"
|
||||
automation = "Automation"
|
||||
developerTools = "Developer Tools"
|
||||
documentReview = "Document Review"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Column {{index}}"
|
||||
convertToPdf = "Convert to PDF"
|
||||
csvStats = "{{rows}} rows · {{columns}} columns · {{size}}"
|
||||
emptyFile = "Empty file"
|
||||
htmlHidePreview = "Hide preview"
|
||||
htmlPreview = "HTML preview"
|
||||
htmlPreviewMobileHidden = "HTML pages are laid out for desktop widths, so the preview is off by default here."
|
||||
htmlPreviewWarning = "HTML preview - external resources may not load · {{size}}"
|
||||
htmlShowPreview = "Show preview anyway"
|
||||
invalidJson = "Invalid JSON - showing raw content"
|
||||
lineNumbers = "Line numbers"
|
||||
loading = "Loading..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Export PDF"
|
||||
exportSelected = "Export Selected Pages"
|
||||
formFill = "Fill Form"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "More actions"
|
||||
multiTool = "Multi-Tool"
|
||||
panMode = "Pan Mode"
|
||||
print = "Print PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Select All"
|
||||
selectByNumber = "Select by Page Numbers"
|
||||
selectLanguage = "Select language"
|
||||
share = "Share"
|
||||
showAllTools = "Show all tools"
|
||||
showFewerTools = "Collapse toolbar"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Toggle Annotations Visibility"
|
||||
toggleAttachments = "Toggle Attachments"
|
||||
|
||||
@@ -3001,6 +3001,293 @@ summary_one = "Ran 1 tool"
|
||||
summary_other = "Ran {{count}} tools"
|
||||
unknownTool = "Unknown tool"
|
||||
|
||||
[classification.families]
|
||||
correspondence = "Correspondence"
|
||||
education = "Education"
|
||||
engineering = "Engineering"
|
||||
finance = "Financial"
|
||||
forms = "Forms"
|
||||
government = "Government"
|
||||
health = "Medical"
|
||||
hr = "HR"
|
||||
legal = "Legal"
|
||||
operations = "Operations"
|
||||
projects = "Projects"
|
||||
property = "Property"
|
||||
reports = "Reports"
|
||||
sales = "Marketing"
|
||||
travel = "Travel"
|
||||
|
||||
[classification.labels]
|
||||
academic-record = "Academic record"
|
||||
action-plan = "Action plan"
|
||||
addendum = "Addendum"
|
||||
advertisement = "Advertisement"
|
||||
affidavit = "Affidavit"
|
||||
agenda = "Agenda"
|
||||
amendment = "Amendment"
|
||||
analytics-report = "Analytics report"
|
||||
announcement = "Announcement"
|
||||
annual-report = "Annual report"
|
||||
api-documentation = "API documentation"
|
||||
application-form = "Application form"
|
||||
appraisal-report = "Appraisal report"
|
||||
architecture-document = "Architecture document"
|
||||
articles-of-incorporation = "Articles of incorporation"
|
||||
assignment-brief = "Assignment brief"
|
||||
audit-report = "Audit report"
|
||||
balance-sheet = "Balance sheet"
|
||||
bank-statement = "Bank statement"
|
||||
benefits-summary = "Benefits summary"
|
||||
bill-of-lading = "Bill of lading"
|
||||
bill-of-materials = "Bill of materials"
|
||||
blueprint = "Blueprint"
|
||||
board-report = "Board report"
|
||||
board-resolution = "Board resolution"
|
||||
booking-confirmation = "Booking confirmation"
|
||||
brochure = "Brochure"
|
||||
budget = "Budget"
|
||||
business-plan = "Business plan"
|
||||
business-proposal = "Business proposal"
|
||||
bylaws = "Bylaws"
|
||||
campaign-brief = "Campaign brief"
|
||||
case-study = "Case study"
|
||||
cash-flow-statement = "Cash flow statement"
|
||||
catalog = "Catalog"
|
||||
cease-and-desist = "Cease and desist"
|
||||
certificate = "Certificate"
|
||||
certificate-of-completion = "Certificate of completion"
|
||||
change-log = "Change log"
|
||||
checklist = "Checklist"
|
||||
claim-form = "Claim form"
|
||||
closing-statement = "Closing statement"
|
||||
complaint-letter = "Complaint letter"
|
||||
compliance-document = "Compliance document"
|
||||
confirmation-letter = "Confirmation letter"
|
||||
consent-form = "Consent form"
|
||||
contract = "Contract"
|
||||
course-syllabus = "Course syllabus"
|
||||
court-filing = "Court filing"
|
||||
cover-letter = "Cover letter"
|
||||
credit-note = "Credit note"
|
||||
customs-declaration = "Customs declaration"
|
||||
customs-form = "Customs form"
|
||||
cv = "CV"
|
||||
datasheet = "Datasheet"
|
||||
debit-note = "Debit note"
|
||||
deed = "Deed"
|
||||
delivery-note = "Delivery note"
|
||||
demand-letter = "Demand letter"
|
||||
design-document = "Design document"
|
||||
diploma = "Diploma"
|
||||
discharge-summary = "Discharge summary"
|
||||
dissertation = "Dissertation"
|
||||
donation-receipt = "Donation receipt"
|
||||
dunning-letter = "Dunning letter"
|
||||
email-thread = "Email thread"
|
||||
employee-handbook = "Employee handbook"
|
||||
employment-contract = "Employment contract"
|
||||
estimate = "Estimate"
|
||||
event-agenda = "Event agenda"
|
||||
event-program = "Event program"
|
||||
eviction-notice = "Eviction notice"
|
||||
exam-paper = "Exam paper"
|
||||
expense-report = "Expense report"
|
||||
expense-summary = "Expense summary"
|
||||
explanation-of-benefits = "Explanation of benefits"
|
||||
fact-sheet = "Fact sheet"
|
||||
faq-document = "FAQ document"
|
||||
feasibility-study = "Feasibility study"
|
||||
feedback-form = "Feedback form"
|
||||
financial-forecast = "Financial forecast"
|
||||
financial-statement = "Financial statement"
|
||||
floor-plan = "Floor plan"
|
||||
flyer = "Flyer"
|
||||
form = "Form"
|
||||
franchise-agreement = "Franchise agreement"
|
||||
freight-document = "Freight document"
|
||||
gift-certificate = "Gift certificate"
|
||||
glossary = "Glossary"
|
||||
government-notice = "Government notice"
|
||||
grade-report = "Grade report"
|
||||
grant-agreement = "Grant agreement"
|
||||
grant-application = "Grant application"
|
||||
hoa-document = "HOA document"
|
||||
home-inspection-report = "Home inspection report"
|
||||
hr-memo = "HR memo"
|
||||
hr-policy = "HR policy"
|
||||
immigration-document = "Immigration document"
|
||||
immunization-record = "Immunization record"
|
||||
incident-report = "Incident report"
|
||||
income-statement = "Income statement"
|
||||
index = "Index"
|
||||
inspection-report = "Inspection report"
|
||||
insurance-certificate = "Insurance certificate"
|
||||
insurance-claim = "Insurance claim"
|
||||
insurance-policy = "Insurance policy"
|
||||
intake-form = "Intake form"
|
||||
inventory-list = "Inventory list"
|
||||
investment-summary = "Investment summary"
|
||||
invitation = "Invitation"
|
||||
invoice = "Invoice"
|
||||
itinerary = "Itinerary"
|
||||
job-application = "Job application"
|
||||
job-description = "Job description"
|
||||
lab-report = "Lab report"
|
||||
lease-agreement = "Lease agreement"
|
||||
leave-request = "Leave request"
|
||||
legal-brief = "Legal brief"
|
||||
legal-filing = "Legal filing"
|
||||
legal-notice = "Legal notice"
|
||||
legal-opinion = "Legal opinion"
|
||||
lesson-plan = "Lesson plan"
|
||||
letter = "Letter"
|
||||
letter-of-intent = "Letter of intent"
|
||||
license = "License"
|
||||
license-agreement = "License agreement"
|
||||
loan-agreement = "Loan agreement"
|
||||
loan-document = "Loan document"
|
||||
maintenance-log = "Maintenance log"
|
||||
manual = "Manual"
|
||||
market-research = "Market research"
|
||||
marketing-plan = "Marketing plan"
|
||||
media-kit = "Media kit"
|
||||
medical-invoice = "Medical invoice"
|
||||
medical-report = "Medical report"
|
||||
meeting-agenda = "Meeting agenda"
|
||||
meeting-minutes = "Meeting minutes"
|
||||
meeting-notes = "Meeting notes"
|
||||
membership-document = "Membership document"
|
||||
memo = "Memo"
|
||||
memorandum-of-understanding = "Memorandum of understanding"
|
||||
mortgage-document = "Mortgage document"
|
||||
nda = "NDA"
|
||||
newsletter = "Newsletter"
|
||||
non-compete-agreement = "Non-compete agreement"
|
||||
notice = "Notice"
|
||||
offer-letter = "Offer letter"
|
||||
onboarding-document = "Onboarding document"
|
||||
order-confirmation = "Order confirmation"
|
||||
order-form = "Order form"
|
||||
organization-chart = "Organization chart"
|
||||
packing-slip = "Packing slip"
|
||||
partnership-agreement = "Partnership agreement"
|
||||
patent = "Patent"
|
||||
pathology-report = "Pathology report"
|
||||
payment-reminder = "Payment reminder"
|
||||
payroll-document = "Payroll document"
|
||||
payslip = "Payslip"
|
||||
performance-review = "Performance review"
|
||||
permit = "Permit"
|
||||
petition = "Petition"
|
||||
pitch-deck = "Pitch deck"
|
||||
power-of-attorney = "Power of attorney"
|
||||
prescription = "Prescription"
|
||||
presentation = "Presentation"
|
||||
press-release = "Press release"
|
||||
price-list = "Price list"
|
||||
pricing-sheet = "Pricing sheet"
|
||||
privacy-policy = "Privacy policy"
|
||||
product-sheet = "Product sheet"
|
||||
proforma-invoice = "Proforma invoice"
|
||||
progress-report = "Progress report"
|
||||
project-charter = "Project charter"
|
||||
project-plan = "Project plan"
|
||||
promotional-material = "Promotional material"
|
||||
property-listing = "Property listing"
|
||||
proposal = "Proposal"
|
||||
public-notice = "Public notice"
|
||||
purchase-agreement = "Purchase agreement"
|
||||
purchase-order = "Purchase order"
|
||||
quality-report = "Quality report"
|
||||
quarterly-report = "Quarterly report"
|
||||
questionnaire = "Questionnaire"
|
||||
quick-start-guide = "Quick start guide"
|
||||
quote = "Quote"
|
||||
radiology-report = "Radiology report"
|
||||
receipt = "Receipt"
|
||||
recommendation-letter = "Recommendation letter"
|
||||
reference-letter = "Reference letter"
|
||||
referral-letter = "Referral letter"
|
||||
registration-confirmation = "Registration confirmation"
|
||||
registration-form = "Registration form"
|
||||
regulatory-filing = "Regulatory filing"
|
||||
release-notes = "Release notes"
|
||||
remittance-advice = "Remittance advice"
|
||||
rental-agreement = "Rental agreement"
|
||||
report = "Report"
|
||||
request-for-proposal = "Request for proposal"
|
||||
request-for-quotation = "Request for quotation"
|
||||
requirements-document = "Requirements document"
|
||||
research-abstract = "Research abstract"
|
||||
research-paper = "Research paper"
|
||||
reservation = "Reservation"
|
||||
resignation-letter = "Resignation letter"
|
||||
resume = "Resume"
|
||||
retrospective = "Retrospective"
|
||||
return-authorization = "Return authorization"
|
||||
risk-assessment = "Risk assessment"
|
||||
roadmap = "Roadmap"
|
||||
safety-data-sheet = "Safety data sheet"
|
||||
safety-procedure = "Safety procedure"
|
||||
sales-proposal = "Sales proposal"
|
||||
sales-report = "Sales report"
|
||||
schematic = "Schematic"
|
||||
scope-of-work = "Scope of work"
|
||||
service-agreement = "Service agreement"
|
||||
service-report = "Service report"
|
||||
settlement-agreement = "Settlement agreement"
|
||||
shareholder-agreement = "Shareholder agreement"
|
||||
shipping-confirmation = "Shipping confirmation"
|
||||
specification = "Specification"
|
||||
sponsorship-agreement = "Sponsorship agreement"
|
||||
standard-operating-procedure = "Standard operating procedure"
|
||||
statement-of-account = "Statement of account"
|
||||
statement-of-work = "Statement of work"
|
||||
status-report = "Status report"
|
||||
stock-report = "Stock report"
|
||||
study-guide = "Study guide"
|
||||
subpoena = "Subpoena"
|
||||
subscription-confirmation = "Subscription confirmation"
|
||||
supply-order = "Supply order"
|
||||
survey-form = "Survey form"
|
||||
survey-results = "Survey results"
|
||||
sustainability-report = "Sustainability report"
|
||||
table-of-contents = "Table of contents"
|
||||
tax-form = "Tax form"
|
||||
tax-return = "Tax return"
|
||||
tax-statement = "Tax statement"
|
||||
technical-drawing = "Technical drawing"
|
||||
technical-specification = "Technical specification"
|
||||
tenancy-agreement = "Tenancy agreement"
|
||||
tender-document = "Tender document"
|
||||
termination-letter = "Termination letter"
|
||||
terms-and-conditions = "Terms and conditions"
|
||||
terms-of-service = "Terms of service"
|
||||
test-plan = "Test plan"
|
||||
test-report = "Test report"
|
||||
thesis = "Thesis"
|
||||
ticket = "Ticket"
|
||||
timeline = "Timeline"
|
||||
timesheet = "Timesheet"
|
||||
title-document = "Title document"
|
||||
training-material = "Training material"
|
||||
transcript = "Transcript"
|
||||
travel-itinerary = "Travel itinerary"
|
||||
trust-document = "Trust document"
|
||||
user-guide = "User guide"
|
||||
utility-bill = "Utility bill"
|
||||
vendor-agreement = "Vendor agreement"
|
||||
visa-document = "Visa document"
|
||||
waiver = "Waiver"
|
||||
warehouse-receipt = "Warehouse receipt"
|
||||
warranty-document = "Warranty document"
|
||||
waybill = "Waybill"
|
||||
white-paper = "White paper"
|
||||
will = "Will"
|
||||
work-instruction = "Work instruction"
|
||||
work-order = "Work order"
|
||||
|
||||
[cloudBadge]
|
||||
tooltip = "This operation will use your cloud credits"
|
||||
|
||||
@@ -3240,7 +3527,7 @@ enterEmailConfirm = "To confirm deletion, please type your email address ({{emai
|
||||
guestDescription = "You are signed in as a guest. Consider upgrading your account above."
|
||||
label = "Overview"
|
||||
manageAccountPreferences = "Manage your account preferences"
|
||||
signedInAs = "Signed in as"
|
||||
signedInAs = "Account"
|
||||
title = "Account Settings"
|
||||
|
||||
[config.account.profilePicture]
|
||||
@@ -3351,6 +3638,39 @@ integration = "Integration Configuration"
|
||||
security = "Security Configuration"
|
||||
system = "System Configuration"
|
||||
|
||||
[connect]
|
||||
loading = "Checking this request."
|
||||
redirecting = "Returning you to your server."
|
||||
|
||||
[connect.confirm]
|
||||
acknowledge = "I recognise this address and want to connect it to my team"
|
||||
approve = "Connect server"
|
||||
deny = "Decline"
|
||||
lead = "A Stirling server is asking to connect to your team. Check the address below is yours before you approve."
|
||||
originLabel = "Address"
|
||||
signedInAs = "Signed in as"
|
||||
switchAccount = "Use a different account"
|
||||
title = "Connect this server?"
|
||||
unknownAccount = "an unknown account"
|
||||
|
||||
[connect.confirm.insecure]
|
||||
body = "This address does not use HTTPS, so your sign-in will be sent over an unencrypted connection. Only approve it on a network you trust."
|
||||
label = "Not an encrypted address"
|
||||
|
||||
[connect.declined]
|
||||
body = "Nothing was connected. You can close this page."
|
||||
title = "Request declined"
|
||||
|
||||
[connect.error]
|
||||
failed = "That did not go through. Only a team owner can connect a server."
|
||||
|
||||
[connect.meta]
|
||||
title = "Connect a server"
|
||||
|
||||
[connect.notFound]
|
||||
body = "This connection request is not valid. It may have expired, or already been used. Start another one from your server."
|
||||
title = "Request not valid"
|
||||
|
||||
[convert]
|
||||
autoRotate = "Auto Rotate"
|
||||
autoRotateDescription = "Automatically rotate images to better fit the PDF page"
|
||||
@@ -6487,6 +6807,34 @@ after = "to enable account linking against the hosted Stirling account. In dev y
|
||||
before = "Set"
|
||||
title = "SaaS login not configured"
|
||||
|
||||
[portal.accountLink.connect.callback]
|
||||
continue = "Continue"
|
||||
linkedNotSignedIn = "You are not signed in to Stirling in this browser, so usage and billing will ask you to sign in."
|
||||
modalTitle = "Connecting this server"
|
||||
retry = "Try again"
|
||||
signedInAnyway = "You are signed in to Stirling, so billing and usage will load. Only the server link is incomplete."
|
||||
working = "Finishing the connection."
|
||||
|
||||
[portal.accountLink.connect.callback.expired]
|
||||
body = "Connection requests are short lived. Start another one."
|
||||
title = "Request expired"
|
||||
|
||||
[portal.accountLink.connect.callback.linked]
|
||||
body = "This server is connected to your Stirling account."
|
||||
title = "Server connected"
|
||||
|
||||
[portal.accountLink.connect.callback.malformed]
|
||||
body = "This page was opened without a valid connection response. Start the connection from settings."
|
||||
title = "Could not read the response"
|
||||
|
||||
[portal.accountLink.connect.callback.rejected]
|
||||
body = "This request was declined or has already been used. Start another one if that was not intended."
|
||||
title = "Connection not completed"
|
||||
|
||||
[portal.accountLink.connect.callback.unfinished]
|
||||
body = "Stirling did not confirm the connection. This is usually temporary."
|
||||
title = "Not finished yet"
|
||||
|
||||
[portal.accountLink.gate]
|
||||
action = "Link account"
|
||||
description = "Link this org's Stirling account to use billable features."
|
||||
@@ -6520,17 +6868,24 @@ minutesAgo_other = "{{count}}m ago"
|
||||
never = "never"
|
||||
|
||||
[portal.accountLink.modal]
|
||||
linkSubtitle = "Sign in to the account this server should bill against."
|
||||
linkTitle = "Link your Stirling account"
|
||||
reauthSubtitle = "Your session expired — sign back in to your Stirling account. Your instance stays linked."
|
||||
cancel = "Cancel"
|
||||
continueLink = "Continue to Stirling"
|
||||
continueReauth = "Sign in again"
|
||||
linkSubtitle = "Connect this server to the Stirling account it should bill against."
|
||||
linkTitle = "Connect your Stirling account"
|
||||
noAuthorizeUrl = "Stirling did not return somewhere to continue. Try again in a moment."
|
||||
reauthSubtitle = "Your Stirling session expired. Sign in again to keep seeing usage and billing. This server stays connected either way."
|
||||
reauthTitle = "Sign in again"
|
||||
simulateSignIn = "Simulate sign-in (dev)"
|
||||
startFailed = "Could not reach Stirling to start the connection. Check this server's outbound network access, then try again."
|
||||
step1 = "We send you to stirling.com to sign in. Any sign-in method works there, including Google and single sign-on."
|
||||
step2 = "You check this server's address and approve it. A team owner has to do this the first time."
|
||||
step3 = "Stirling brings you straight back here and finishes up."
|
||||
|
||||
[portal.accountLink.modal.loginNotConfigured]
|
||||
after = "to enable in-app linking against the hosted Stirling account."
|
||||
after = "so this server can finish the connection when you come back."
|
||||
and = "and"
|
||||
before = "Set"
|
||||
title = "SaaS login not configured"
|
||||
title = "Stirling connection not configured"
|
||||
|
||||
[portal.accountLink.panel]
|
||||
instancesSub = "Every self-hosted instance registered to this org. Revoke a credential to immediately cut off its unattended access."
|
||||
@@ -8833,7 +9188,9 @@ title = "Sources"
|
||||
connectSource = "Connect source"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Advanced"
|
||||
back = "Back to sources"
|
||||
backToSource = "Back to source setup"
|
||||
backToTypes = "All source types"
|
||||
cancel = "Cancel"
|
||||
chooseHint = "Choose where documents come from. Greyed-out connectors are on the way."
|
||||
@@ -8843,7 +9200,6 @@ create = "Create source"
|
||||
createTitle = "Connect a source"
|
||||
delete = "Delete"
|
||||
editTitle = "Edit source"
|
||||
enabled = "Enabled"
|
||||
save = "Save changes"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8863,23 +9219,24 @@ total = "Connections"
|
||||
unused = "Unused"
|
||||
|
||||
[portal.sources.networkFields.connection]
|
||||
helperText = "The stored connection with the server address and credentials. Reused by every source that references it."
|
||||
helperText = "The saved connection with the server address and credentials. Shared by every source that uses it."
|
||||
label = "Connection"
|
||||
|
||||
[portal.sources.networkFields.directory]
|
||||
helperText = "Folder on the server to poll, relative to the login home or share root. Leave blank for the root."
|
||||
helperText = "The folder on the server to watch, relative to the login home or share root."
|
||||
label = "Folder"
|
||||
placeholder = "incoming/"
|
||||
placeholder = "Login home or share root"
|
||||
|
||||
[portal.sources.networkFields.mode]
|
||||
helperText = "Consume removes each file from the server once every policy has processed it."
|
||||
label = "Read mode"
|
||||
helperText = "Whether to leave the original file in place after it has been processed. If the original file is left in place, it will be re-processed next time the pipeline scans the source."
|
||||
label = "After processing"
|
||||
|
||||
[portal.sources.networkFields.mode.options]
|
||||
consume = "Consume: process each file once"
|
||||
snapshot = "Snapshot: re-read the folder every run"
|
||||
consume = "Delete the file"
|
||||
snapshot = "Leave it in place"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Include subfolders if your files are organised into nested folders, or watch only the top level."
|
||||
label = "Folder depth"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8920,26 +9277,28 @@ description = "Watch a directory on the server for new documents."
|
||||
label = "Folder"
|
||||
|
||||
[portal.sources.types.folder.fields.directory]
|
||||
helperText = "Absolute path Stirling watches for files to process."
|
||||
label = "Directory path"
|
||||
helperText = "The absolute path to the folder to watch for new files."
|
||||
label = "Folder"
|
||||
placeholder = "/data/incoming"
|
||||
|
||||
[portal.sources.types.folder.fields.identity]
|
||||
helperText = "Content check reads each changed file, so renames and touches that don't alter content are not reprocessed."
|
||||
helperText = "Using the file contents for change detection is slower but more accurate."
|
||||
label = "Change detection"
|
||||
|
||||
[portal.sources.types.folder.fields.identity.options]
|
||||
hash = "Size, date and content check"
|
||||
stat = "Size and date modified"
|
||||
hash = "File Metadata & Content"
|
||||
stat = "File Metadata"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
label = "Read mode"
|
||||
helperText = "Whether to leave the original file in place after it has been processed. If the original file is left in place, it will be re-processed next time the pipeline scans the source."
|
||||
label = "After processing"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
consume = "Consume: process each file once"
|
||||
snapshot = "Snapshot: re-read the folder every run"
|
||||
consume = "Delete the file"
|
||||
snapshot = "Leave it in place"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Include subfolders if your files are organised into nested folders, or watch only the top level."
|
||||
label = "Folder depth"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -8967,21 +9326,21 @@ description = "Pull documents from an Amazon S3 or S3-compatible bucket."
|
||||
label = "Amazon S3"
|
||||
|
||||
[portal.sources.types.s3.fields.connection]
|
||||
helperText = "The stored connection holding the bucket and credentials. Reused by every source and pipeline output that references it."
|
||||
helperText = "The saved connection with the bucket and credentials. Shared by every source and pipeline output that uses it."
|
||||
label = "Connection"
|
||||
|
||||
[portal.sources.types.s3.fields.mode]
|
||||
helperText = "Consume removes each object from the bucket once every policy has processed it."
|
||||
label = "Read mode"
|
||||
helperText = "Whether to leave the original object in place after it has been processed. If the original object is left in place, it will be re-processed next time the pipeline scans the source."
|
||||
label = "After processing"
|
||||
|
||||
[portal.sources.types.s3.fields.mode.options]
|
||||
consume = "Consume: process each object once"
|
||||
snapshot = "Snapshot: re-read the bucket every run"
|
||||
consume = "Delete the object"
|
||||
snapshot = "Leave it in place"
|
||||
|
||||
[portal.sources.types.s3.fields.prefix]
|
||||
helperText = "Only objects whose keys start with this prefix are processed."
|
||||
label = "Key prefix"
|
||||
placeholder = "incoming/"
|
||||
helperText = "The folder (key prefix) within the bucket to watch."
|
||||
label = "Folder"
|
||||
placeholder = "Whole bucket"
|
||||
|
||||
[portal.sources.types.sftp]
|
||||
description = "Poll an SFTP drop folder for new documents."
|
||||
@@ -11357,8 +11716,12 @@ disableColorFilter = "Disable Color Filter"
|
||||
dualPageView = "Dual Page View"
|
||||
enableDarkFilter = "Enable Dark Filter"
|
||||
enableSepiaFilter = "Enable Sepia Filter"
|
||||
engineLoadError = "Failed to initialize PDF viewer engine"
|
||||
engineSlowWarning = "PDF engine initialization is taking longer than expected. Please check your browser WebAssembly and Worker settings, or reload the page."
|
||||
firstPage = "First Page"
|
||||
lastPage = "Last Page"
|
||||
loadingDocument = "Loading document..."
|
||||
loadingEngine = "Loading PDF Engine..."
|
||||
moreOptions = "More"
|
||||
nextPage = "Next Page"
|
||||
onlyPdfSupported = "This file format is not supported for preview."
|
||||
@@ -11793,6 +12156,10 @@ title = "Watermark Text"
|
||||
image = "Image"
|
||||
text = "Text"
|
||||
|
||||
[workbench.sessionRestore]
|
||||
none = "Your previous files are no longer stored on this device."
|
||||
partial = "Restored {{restored}} of {{total}} files. The rest are no longer stored on this device."
|
||||
|
||||
[workbenchBar]
|
||||
activeFiles = "Active Files"
|
||||
annotations = "Annotations"
|
||||
|
||||
@@ -3969,6 +3969,7 @@ back = "Atrás"
|
||||
backToFolder = "Volver a {{folder}}"
|
||||
backToMyFiles = "Volver a Mis archivos"
|
||||
breadcrumbs = "Ruta de carpeta"
|
||||
bulkActions = "Acciones"
|
||||
cancel = "Cancelar"
|
||||
classification = "Clasificación"
|
||||
clearSelection = "Borrar selección"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Tamaño total"
|
||||
type = "Tipo"
|
||||
versionHistory = "Evolución de versiones"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} filtros activos"
|
||||
clearAll = "Borrar filtros"
|
||||
label = "Filtros"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Cancelar"
|
||||
error = "No se pudo guardar la carpeta. Vuelve a intentarlo."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Ordenar archivos"
|
||||
modifiedAsc = "Más antiguos primero"
|
||||
modifiedDesc = "Más recientes primero"
|
||||
nameAsc = "Nombre A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Todo guardado"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "No se pudieron aplicar los cambios"
|
||||
extractCsvError = "No se pudo extraer CSV"
|
||||
extractXlsxError = "No se pudo extraer XLSX"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Aplanar después de rellenar"
|
||||
goToPage = "Ir a esta página"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Página"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "oblig."
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Volver a escanear campos"
|
||||
rescanFormFields = "Volver a escanear campos del formulario"
|
||||
save = "Guardar"
|
||||
saveShortcut = "Ctrl+S para guardar"
|
||||
skippedEdits_one = "No se pudo aplicar 1 cambio:"
|
||||
skippedEdits_other = "No se pudieron aplicar {{count}} cambios:"
|
||||
skippedEditsTruncated = "{{count}} más no mostrados."
|
||||
unsavedChanges = "Cambios sin guardar"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Añadir {{count}} campo(s) al PDF"
|
||||
empty = "Aún no se han dibujado campos."
|
||||
failed = "No se pudieron añadir los campos"
|
||||
goToField = "Ir a este campo"
|
||||
hint = "Elige un tipo de campo y luego dibújalo en la página."
|
||||
placing = "Dibuja un campo {{type}} en la página. Pulsa Esc para detener."
|
||||
preview = "Mantener para previsualizar"
|
||||
previewHelp = "Mantén pulsado para ver cómo quedarán los campos una vez añadidos, sin los contornos de edición."
|
||||
removeField = "Eliminar campo"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Acción del botón"
|
||||
actionHelp = "Lo que hace el botón al hacer clic."
|
||||
actionNone = "Ninguna"
|
||||
actionPrint = "Imprimir"
|
||||
actionReset = "Restablecer formulario"
|
||||
actionSubmit = "Enviar a URL"
|
||||
actionUri = "Abrir URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "La dirección que abre el botón o a la que envía."
|
||||
addOption = "Añadir opción"
|
||||
caption = "Texto del botón"
|
||||
captionHelp = "El texto impreso en la cara del botón."
|
||||
defaultValue = "Valor predeterminado"
|
||||
defaultValueHelp = "Lo que contiene el campo antes de que alguien lo rellene. Déjalo en blanco para un campo vacío."
|
||||
fontSize = "Tamaño de fuente"
|
||||
fontSizeHelp = "Tamaño del texto dentro del campo. Déjalo en blanco para que el lector lo ajuste al espacio disponible."
|
||||
label = "Etiqueta"
|
||||
labelHelp = "El texto que se muestra a quien rellena el formulario. Déjalo en blanco para usar el nombre del campo."
|
||||
maxLength = "Longitud máxima (comb)"
|
||||
maxLengthHelp = "Limita cuántos caracteres caben, dibujados como cajas espaciadas uniformemente."
|
||||
multiline = "Multilínea"
|
||||
multilineHelp = "Permite más de una línea de texto y ajusta el texto al borde del campo."
|
||||
multiSelect = "Permitir selección múltiple"
|
||||
multiSelectHelp = "Permite elegir más de una opción a la vez."
|
||||
name = "Nombre del campo"
|
||||
nameHelp = "El nombre interno del campo. Se utiliza al exportar datos o rellenar el formulario desde otro sistema, así que mantenlo único y sin espacios."
|
||||
optionGap = "Espaciado entre opciones"
|
||||
optionGapHelp = "Separación entre botones, en puntos. Déjalo en blanco para distribuirlos uniformemente por la caja."
|
||||
optionPlaceholder = "Opción {{n}}"
|
||||
options = "Opciones"
|
||||
optionsEmpty = "Añade al menos una opción."
|
||||
optionsHelp = "Las opciones ofrecidas en la lista. Cada una se almacena tal como se escribe, así que mantenlas cortas y distintas."
|
||||
optionSize = "Tamaño de opción"
|
||||
optionSizeHelp = "Anchura y altura de cada botón, en puntos. Déjalo en blanco para ajustarlos a la caja que dibujaste."
|
||||
readOnly = "Solo lectura"
|
||||
readOnlyHelp = "Muestra un valor pero impide que nadie lo edite."
|
||||
removeOption = "Eliminar opción"
|
||||
required = "Obligatorio"
|
||||
requiredHelp = "El formulario no se puede enviar hasta que este campo esté rellenado."
|
||||
signatureNote = "Solo marcador de posición: aquí no se firma. Marca dónde va una firma para que un firmante de PDF (Adobe Acrobat, un servicio de firma, etc.) coloque la firma en este lugar cuando se firme el documento."
|
||||
tooltip = "Información sobre herramientas"
|
||||
tooltipHelp = "La pista que se muestra cuando alguien pasa el cursor sobre el campo en un lector de PDF."
|
||||
type = "Tipo"
|
||||
typeHelp = "Qué tipo de campo es este. Cambiarlo reconstruye el campo, por lo que su valor actual no se conserva."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Crear"
|
||||
fill = "Rellenar"
|
||||
label = "Modo del editor de formularios"
|
||||
modify = "Modificar"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Guardar {{count}} cambio(s)"
|
||||
delete = "Eliminar"
|
||||
empty = "Este PDF aún no tiene campos de formulario."
|
||||
failed = "No se pudieron guardar los cambios"
|
||||
groupSizeHint = "Usar tamaño de opción"
|
||||
hint = "Selecciona un campo para editar sus propiedades, arrástralo en la página o elimínalo."
|
||||
restore = "Restaurar"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Cerrar barra lateral"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Modificar restricciones y permisos del documento"
|
||||
tags = "permisos,restricciones,derechos,control de acceso,permitir,denegar,impresión,copia,edición,modificar permisos,ajustes de seguridad,derechos de usuario"
|
||||
title = "Cambiar permisos"
|
||||
|
||||
[home.classify]
|
||||
desc = "Identifica qué tipo de documento es y etiquétalo."
|
||||
title = "Clasificar"
|
||||
|
||||
[home.compare]
|
||||
desc = "Comparar y mostrar las diferencias entre 2 documentos PDF"
|
||||
tags = "diferencia"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Nada que notificar."
|
||||
handoffUnavailable = "Este navegador no permitirá que el procesador pase el documento al editor. Ábrelo desde el editor en su lugar."
|
||||
noDocumentLinked = "Este fallo no está vinculado a un documento específico, así que no hay nada que abrir aquí."
|
||||
notOnThisDevice = "Este documento no está en este dispositivo, así que no se puede abrir aquí."
|
||||
occurrences = "{{count}} veces"
|
||||
open = "Notificaciones"
|
||||
title = "Notificaciones"
|
||||
unread = "No leídas"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Eso no ha funcionado. Inténtalo de nuevo en un momento."
|
||||
unavailable = "No disponible para esta notificación."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Copiado"
|
||||
copy = "Copiar error"
|
||||
less = "Mostrar menos"
|
||||
more = "Mostrar mensaje completo"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Anteriores"
|
||||
new = "Nuevas"
|
||||
|
||||
[oauth.error]
|
||||
message = "La autenticación no se realizó correctamente. Puede cerrar esta ventana e intentarlo de nuevo."
|
||||
title = "Autenticación fallida"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "de {{allowance}} PDFs gratuitos usados"
|
||||
capSuffix_other = "de {{allowance}} PDFs gratuitos usados"
|
||||
eyebrow = "Prueba de Processor"
|
||||
statusLabel_one = "queda {{remaining}}"
|
||||
statusLabel_other = "quedan {{remaining}}"
|
||||
statusLabel_one = "{{used}} usado"
|
||||
statusLabel_other = "{{used}} usados"
|
||||
sub = "Usa el Editor PDF gratis. Paga para procesar PDFs automáticamente."
|
||||
title_one = "Procesa {{allowance}} PDFs gratis"
|
||||
title_other = "Procesa {{allowance}} PDFs gratis"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Ver archivo"
|
||||
viewInProcessor = "Ver en el procesador"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Este fallo no se registró en un documento específico, así que no hay nada que abrir aquí."
|
||||
unattended = "Este archivo fue enviado por una carpeta, bucket o webhook, así que ningún navegador lo conserva para abrirlo."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Fuentes"
|
||||
connectSource = "Conectar fuente"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Avanzado"
|
||||
back = "Volver a orígenes"
|
||||
backToSource = "Volver a la configuración de la fuente"
|
||||
backToTypes = "Todos los tipos de origen"
|
||||
cancel = "Cancelar"
|
||||
chooseHint = "Elige de dónde proceden los documentos. Los conectores atenuados están en camino."
|
||||
@@ -8726,7 +8845,6 @@ create = "Crear origen"
|
||||
createTitle = "Conectar un origen"
|
||||
delete = "Eliminar"
|
||||
editTitle = "Editar origen"
|
||||
enabled = "Habilitado"
|
||||
save = "Guardar cambios"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consumir: procesar cada archivo una vez"
|
||||
snapshot = "Instantánea: volver a leer la carpeta en cada ejecución"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Incluye subcarpetas si tus archivos están organizados en carpetas anidadas, o vigila solo el nivel superior."
|
||||
label = "Profundidad de carpeta"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Tamaño, fecha y comprobación de contenido"
|
||||
stat = "Tamaño y fecha de modificación"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Indica si se debe dejar el archivo original en su lugar después de procesarlo. Si el archivo original se deja en su lugar, se volverá a procesar la próxima vez que la canalización analice la fuente."
|
||||
label = "Modo de lectura"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Consumir: procesar cada archivo una vez"
|
||||
snapshot = "Instantánea: volver a leer la carpeta en cada ejecución"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Incluye subcarpetas si tus archivos están organizados en carpetas anidadas, o vigila solo el nivel superior."
|
||||
label = "Profundidad de carpeta"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Tu servidor Stirling-PDF está sin conexión y \"{{en
|
||||
expired = "Su sesión ha caducado. Actualice la página e inténtelo de nuevo."
|
||||
|
||||
[settings]
|
||||
backToSections = "Todos los ajustes"
|
||||
close = "Cerrar"
|
||||
title = "Ajustes"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Documentos y RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Buscar"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Buscar herramientas..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Formato Avanzado"
|
||||
ai = "IA"
|
||||
automation = "Automatización"
|
||||
developerTools = "Herramientas de Desarrollo"
|
||||
documentReview = "Revisión de Documentos"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Columna {{index}}"
|
||||
convertToPdf = "Convertir a PDF"
|
||||
csvStats = "{{rows}} filas · {{columns}} columnas · {{size}}"
|
||||
emptyFile = "Archivo vacío"
|
||||
htmlHidePreview = "Ocultar vista previa"
|
||||
htmlPreview = "Vista previa de HTML"
|
||||
htmlPreviewMobileHidden = "Las páginas HTML están diseñadas para anchos de escritorio, así que la vista previa está desactivada aquí de forma predeterminada."
|
||||
htmlPreviewWarning = "Vista previa de HTML — los recursos externos pueden no cargarse · {{size}}"
|
||||
htmlShowPreview = "Mostrar vista previa de todos modos"
|
||||
invalidJson = "JSON no válido — mostrando contenido sin procesar"
|
||||
lineNumbers = "Números de línea"
|
||||
loading = "Cargando..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Exportar PDF"
|
||||
exportSelected = "Exportar páginas seleccionadas"
|
||||
formFill = "Rellenar formulario"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Más acciones"
|
||||
multiTool = "Multiherramienta"
|
||||
panMode = "Modo desplazar"
|
||||
print = "Imprimir PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Seleccionar todo"
|
||||
selectByNumber = "Seleccionar por números de página"
|
||||
selectLanguage = "Seleccionar idioma"
|
||||
share = "Compartir"
|
||||
showAllTools = "Mostrar todas las herramientas"
|
||||
showFewerTools = "Contraer barra de herramientas"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Alternar visibilidad de las anotaciones"
|
||||
toggleAttachments = "Alternar adjuntos"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Faltsua"
|
||||
fileNotSavedToDisk = "Ez da diskoan gorde"
|
||||
fileSavedToDisk = "Fitxategia diskoan gorde da"
|
||||
fileSelected = "Hautatuta: {{filename}}"
|
||||
filesSelected = "Hautatutako fitxategiak"
|
||||
filesSelected = "{{count}} fitxategi"
|
||||
font = "Letra-tipoa"
|
||||
fontSizeTooltip = "Orri-zenbakiaren testuaren tamaina puntutan. Zenbaki handiagoek testu handiagoa sortzen dute."
|
||||
fontTypeTooltip = "Orri-zenbakientzako letra-familia. Aukeratu zure dokumentuaren estiloaren arabera."
|
||||
@@ -3969,6 +3969,7 @@ back = "Atzera"
|
||||
backToFolder = "Itzuli hona: {{folder}}"
|
||||
backToMyFiles = "Itzuli Nire Fitxategietara"
|
||||
breadcrumbs = "Karpetaren bidea"
|
||||
bulkActions = "Ekintzak"
|
||||
cancel = "Utzi"
|
||||
classification = "Sailkapena"
|
||||
clearSelection = "Garbitu hautapena"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Tamaina osoa"
|
||||
type = "Mota"
|
||||
versionHistory = "Bertsioen ibilbidea"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} iragazki aktibo"
|
||||
clearAll = "Garbitu iragazkiak"
|
||||
label = "Iragazkiak"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Utzi"
|
||||
error = "Ezin izan da karpeta gorde. Saiatu berriro."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Ordenatu fitxategiak"
|
||||
modifiedAsc = "Zaharrenak lehenik"
|
||||
modifiedDesc = "Berrienak lehenik"
|
||||
nameAsc = "Izena A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Dena gordeta"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Ezin izan dira aldaketak aplikatu"
|
||||
extractCsvError = "Ezin izan da CSV erauzi"
|
||||
extractXlsxError = "Ezin izan da XLSX erauzi"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Lautu betetze ondoren"
|
||||
goToPage = "Joan orri honetara"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Orria"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "behar."
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Eremuak berriro eskaneatu"
|
||||
rescanFormFields = "Inprimaki-eremuak berriro eskaneatu"
|
||||
save = "Gorde"
|
||||
saveShortcut = "Ctrl+S gordetzeko"
|
||||
skippedEdits_one = "Ezin izan da aldaketa bat aplikatu:"
|
||||
skippedEdits_other = "Ezin izan dira {{count}} aldaketa aplikatu:"
|
||||
skippedEditsTruncated = "Beste {{count}} ez dira zerrendatu."
|
||||
unsavedChanges = "Gorde gabeko aldaketak"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Gehitu {{count}} eremu PDF fitxategira"
|
||||
empty = "Oraindik ez da eremurik marraztu."
|
||||
failed = "Ezin izan dira eremuak gehitu"
|
||||
goToField = "Joan eremu honetara"
|
||||
hint = "Hautatu eremu mota bat, eta marraztu orrian."
|
||||
placing = "Marraztu {{type}} eremu bat orrian. Sakatu Esc gelditzeko."
|
||||
preview = "Eduki sakatuta aurrebista ikusteko"
|
||||
previewHelp = "Eduki sakatuta eremuak gehitu ondoren nolakoak izango diren ikusteko, edizio-ingeradarik gabe."
|
||||
removeField = "Kendu eremua"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Botoiaren ekintza"
|
||||
actionHelp = "Botoiak klik egitean zer egiten duen."
|
||||
actionNone = "Bat ere ez"
|
||||
actionPrint = "Inprimatu"
|
||||
actionReset = "Berrezarri formularioa"
|
||||
actionSubmit = "Bidali URL helbidera"
|
||||
actionUri = "Ireki URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "Botoiak irekitzen duen edo bidaltzeko erabiltzen duen URL helbidea."
|
||||
addOption = "Gehitu aukera"
|
||||
caption = "Botoiaren epigrafea"
|
||||
captionHelp = "Botoiaren aurpegian inprimatzen den testua."
|
||||
defaultValue = "Balio lehenetsia"
|
||||
defaultValueHelp = "Norbaitek bete aurretik eremuak duen edukia. Utzi hutsik eremu huts baterako."
|
||||
fontSize = "Letra-tamaina"
|
||||
fontSizeHelp = "Eremu barruko testuaren tamaina. Utzi hutsik irakurgailuak egokitzeko tamaina ezar dezan."
|
||||
label = "Etiketa"
|
||||
labelHelp = "Formularioa betetzen duenari erakutsiko zaion testua. Utzi hutsik eremuaren izena erabiltzeko."
|
||||
maxLength = "Gehieneko luzera (comb)"
|
||||
maxLengthHelp = "Zenbat karaktere sar daitezkeen mugatzen du, tarte berdineko laukitxo gisa marraztuta."
|
||||
multiline = "Lerro anitzekoa"
|
||||
multilineHelp = "Testu-lerro bat baino gehiago onartzen ditu eta eremuaren ertzean doitzen du."
|
||||
multiSelect = "Baimendu hautapen anitza"
|
||||
multiSelectHelp = "Aldi berean aukera bat baino gehiago hautatzeko aukera ematen du."
|
||||
name = "Eremuaren izena"
|
||||
nameHelp = "Eremuaren barne-izena. Datuak esportatzean edo formularioa beste sistema batetik betetzean erabiltzen da; beraz, mantendu bakarra eta zuriunerik gabea."
|
||||
optionGap = "Aukeren arteko tartea"
|
||||
optionGapHelp = "Botoien arteko tartea, puntutan. Utzi hutsik kutxan behera modu uniformean banatzeko."
|
||||
optionPlaceholder = "{{n}}. aukera"
|
||||
options = "Aukerak"
|
||||
optionsEmpty = "Gehitu gutxienez aukera bat."
|
||||
optionsHelp = "Zerrendan eskaintzen diren aukerak. Bakoitza idatzi bezala gordetzen da; beraz, mantendu laburrak eta bereiziak."
|
||||
optionSize = "Aukeraren tamaina"
|
||||
optionSizeHelp = "Botoi bakoitzaren zabalera eta altuera, puntutan. Utzi hutsik marraztu duzun kutxara egokitzeko."
|
||||
readOnly = "Irakurtzeko soilik"
|
||||
readOnlyHelp = "Balio bat erakusten du, baina editatzea eragozten du."
|
||||
removeOption = "Kendu aukera"
|
||||
required = "Beharrezkoa"
|
||||
requiredHelp = "Formularioa ezin da bidali eremu hau bete arte."
|
||||
signatureNote = "Leku-marka soilik - hemen ez duzu sinatzen. Sinadura non egon behar den markatzen du, PDF sinatzaile batek (Adobe Acrobat, sinadura-zerbitzu bat, etab.) dokumentua sinatzen denean sinadura leku honetan jar dezan."
|
||||
tooltip = "Argibide-burbuila"
|
||||
tooltipHelp = "PDF irakurgailu batean norbaitek eremuaren gainean sagua jartzean erakusten den argibidea."
|
||||
type = "Mota"
|
||||
typeHelp = "Eremu mota. Aldatzean eremua berreraikitzen da; beraz, uneko balioa ez da mantenduko."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Sortu"
|
||||
fill = "Bete"
|
||||
label = "Formulario-editorearen modua"
|
||||
modify = "Aldatu"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Gorde {{count}} aldaketa"
|
||||
delete = "Ezabatu"
|
||||
empty = "PDF honek ez du oraindik formulario-eremurik."
|
||||
failed = "Ezin izan dira aldaketak gorde"
|
||||
groupSizeHint = "Erabili aukeraren tamaina"
|
||||
hint = "Hautatu eremu bat bere propietateak editatzeko, arrastatu orrian edo ezabatu."
|
||||
restore = "Lehengoratu"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Itxi alboko barra"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Aldatu dokumentuaren murrizketak eta baimenak"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Aldatu baimenak"
|
||||
|
||||
[home.classify]
|
||||
desc = "Identifikatu zer dokumentu mota den eta etiketatu."
|
||||
title = "Sailkatu"
|
||||
|
||||
[home.compare]
|
||||
desc = "Konparatu eta erakutsi 2 PDF dokumenturen aldeak"
|
||||
tags = "aldea"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Ez dago jakinaraztekorik."
|
||||
handoffUnavailable = "Arakatzaile honek ez dio prozesadoreari dokumentua editoreari pasatzen utziko. Ireki editoretik horren ordez."
|
||||
noDocumentLinked = "Hutsegite hau ez dago dokumentu zehatz bati lotuta; beraz, hemen ez dago irekitzeko ezer."
|
||||
notOnThisDevice = "Dokumentu hau ez dago gailu honetan; beraz, ezin da hemen ireki."
|
||||
occurrences = "{{count}} aldiz"
|
||||
open = "Jakinarazpenak"
|
||||
title = "Jakinarazpenak"
|
||||
unread = "Irakurri gabeak"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Horrek ez du funtzionatu. Saiatu berriro une bat barru."
|
||||
unavailable = "Ez dago erabilgarri jakinarazpen honetarako."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Kopiatuta"
|
||||
copy = "Kopiatu errorea"
|
||||
less = "Erakutsi gutxiago"
|
||||
more = "Erakutsi mezu osoa"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Lehenago"
|
||||
new = "Berria"
|
||||
|
||||
[oauth.error]
|
||||
message = "Autentifikazioa ez da ongi burutu. Leiho hau itxi eta saiatu berriro."
|
||||
title = "Autentifikazioak huts egin du"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "{{allowance}} doako PDFetatik erabilita"
|
||||
capSuffix_other = "{{allowance}} doako PDFetatik erabilita"
|
||||
eyebrow = "Processor proba"
|
||||
statusLabel_one = "{{remaining}} geratzen da"
|
||||
statusLabel_other = "{{remaining}} geratzen dira"
|
||||
statusLabel_one = "{{used}} erabilita"
|
||||
statusLabel_other = "{{used}} erabilita"
|
||||
sub = "Erabili PDF Editor doan. Ordaindu PDFak automatikoki prozesatzeko."
|
||||
title_one = "Prozesatu {{allowance}} PDF doan"
|
||||
title_other = "Prozesatu {{allowance}} PDF doan"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Ikusi fitxategia"
|
||||
viewInProcessor = "Ikusi prozesadorean"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Hutsegite hau ez zen dokumentu zehatz baten aurka erregistratu; beraz, hemen ez dago irekitzeko ezer."
|
||||
unattended = "Fitxategi hau karpeta, bucket edo webhook batek eman du; beraz, inoren arakatzaileak ez dauka irekitzeko."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Iturburuak"
|
||||
connectSource = "Konektatu iturburua"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Aurreratua"
|
||||
back = "Itzuli iturburuetara"
|
||||
backToSource = "Itzuli iturburuaren konfiguraziora"
|
||||
backToTypes = "Iturburu mota guztiak"
|
||||
cancel = "Utzi"
|
||||
chooseHint = "Aukeratu dokumentuak nondik datozen. Grisez dauden konektoreak bidean daude."
|
||||
@@ -8726,7 +8845,6 @@ create = "Sortu iturburua"
|
||||
createTitle = "Konektatu iturburu bat"
|
||||
delete = "Ezabatu"
|
||||
editTitle = "Editatu iturburua"
|
||||
enabled = "Gaituta"
|
||||
save = "Gorde aldaketak"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Kontsumitu: prozesatu fitxategi bakoitza behin"
|
||||
snapshot = "Snapshot: berrirakurri karpeta exekuzio bakoitzean"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Sartu azpikarpetak zure fitxategiak habiaratutako karpetetan antolatuta badaude, edo kontrolatu goiko maila bakarrik."
|
||||
label = "Karpetaren sakonera"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Tamaina, data eta eduki-egiaztapena"
|
||||
stat = "Tamaina eta aldatze-data"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Jatorrizko fitxategia prozesatu ondoren bere lekuan utzi behar den ala ez. Jatorrizko fitxategia bere lekuan uzten bada, pipeline-ak iturburua eskaneatzen duen hurrengo aldian berriro prozesatuko da."
|
||||
label = "Irakurketa-modua"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Kontsumitu: prozesatu fitxategi bakoitza behin"
|
||||
snapshot = "Snapshot: berrirakurri karpeta exekuzio bakoitzean"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Sartu azpikarpetak zure fitxategiak habiaratutako karpetetan antolatuta badaude, edo kontrolatu goiko maila bakarrik."
|
||||
label = "Karpetaren sakonera"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Zure Stirling-PDF zerbitzaria lineaz kanpo dago eta \
|
||||
expired = "Zure saioa iraungi da. Freskatu orria eta saiatu berriro."
|
||||
|
||||
[settings]
|
||||
backToSections = "Ezarpen guztiak"
|
||||
close = "Itxi"
|
||||
title = "Ezarpenak"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Dokumentuak eta RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Bilatu"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Bilatu tresnak..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Formateatze aurreratua"
|
||||
ai = "AI"
|
||||
automation = "Automatizazioa"
|
||||
developerTools = "Garatzaileentzako tresnak"
|
||||
documentReview = "Dokumentu-berrikuspena"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "{{index}}. zutabea"
|
||||
convertToPdf = "Bihurtu PDFra"
|
||||
csvStats = "{{rows}} errenkada · {{columns}} zutabe · {{size}}"
|
||||
emptyFile = "Fitxategi hutsa"
|
||||
htmlHidePreview = "Ezkutatu aurrebista"
|
||||
htmlPreview = "HTML aurrebista"
|
||||
htmlPreviewMobileHidden = "HTML orriak mahaigaineko zabaleretarako diseinatuta daude; beraz, aurrebista desaktibatuta dago hemen lehenespenez."
|
||||
htmlPreviewWarning = "HTML aurrebista — kanpoko baliabideak agian ez dira kargatuko · {{size}}"
|
||||
htmlShowPreview = "Erakutsi aurrebista hala ere"
|
||||
invalidJson = "JSON baliogabea — edukia gordinean erakusten"
|
||||
lineNumbers = "Lerro-zenbakiak"
|
||||
loading = "Kargatzen..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Esportatu PDFa"
|
||||
exportSelected = "Esportatu hautatutako orriak"
|
||||
formFill = "Bete inprimakia"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Ekintza gehiago"
|
||||
multiTool = "Multi-tresna"
|
||||
panMode = "Pan modua"
|
||||
print = "Inprimatu PDFa"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Hautatu dena"
|
||||
selectByNumber = "Hautatu orri-zenbakien arabera"
|
||||
selectLanguage = "Hautatu hizkuntza"
|
||||
share = "Partekatu"
|
||||
showAllTools = "Erakutsi tresna guztiak"
|
||||
showFewerTools = "Tolestu tresna-barra"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Txandakatu anotazioen ikusgarritasuna"
|
||||
toggleAttachments = "Txandakatu eranskinak"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "غلط"
|
||||
fileNotSavedToDisk = "روی دیسک ذخیره نشد"
|
||||
fileSavedToDisk = "فایل روی دیسک ذخیره شد"
|
||||
fileSelected = "انتخابشده: {{filename}}"
|
||||
filesSelected = "فایلها انتخاب شدند"
|
||||
filesSelected = "{{count}} فایل"
|
||||
font = "فونت"
|
||||
fontSizeTooltip = "اندازه متن شماره صفحه بر حسب پوینت. اعداد بزرگتر متن بزرگتری ایجاد میکنند."
|
||||
fontTypeTooltip = "خانواده فونت برای شماره صفحات. بر اساس سبک سند خود انتخاب کنید."
|
||||
@@ -3969,6 +3969,7 @@ back = "بازگشت"
|
||||
backToFolder = "بازگشت به {{folder}}"
|
||||
backToMyFiles = "بازگشت به فایلهای من"
|
||||
breadcrumbs = "مسیر پوشه"
|
||||
bulkActions = "اقدامات"
|
||||
cancel = "لغو"
|
||||
classification = "طبقهبندی"
|
||||
clearSelection = "پاککردن انتخاب"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "حجم کل"
|
||||
type = "نوع"
|
||||
versionHistory = "سفر نسخه"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} فیلتر فعال"
|
||||
clearAll = "پاک کردن فیلترها"
|
||||
label = "فیلترها"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "لغو"
|
||||
error = "امکان ذخیرهٔ پوشه نبود. دوباره امتحان کنید."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "مرتبسازی فایلها"
|
||||
modifiedAsc = "قدیمیترین ابتدا"
|
||||
modifiedDesc = "جدیدترین ابتدا"
|
||||
nameAsc = "نام A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "همه ذخیره شد"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "امکان اعمال تغییرات نبود"
|
||||
extractCsvError = "استخراج CSV ناموفق بود"
|
||||
extractXlsxError = "استخراج XLSX ناموفق بود"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "مسطحسازی پس از پرکردن"
|
||||
goToPage = "رفتن به این صفحه"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "صفحه"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "الزامی"
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "اسکن دوبارهٔ فیلدها"
|
||||
rescanFormFields = "اسکن دوبارهٔ فیلدهای فرم"
|
||||
save = "ذخیره"
|
||||
saveShortcut = "Ctrl+S برای ذخیره"
|
||||
skippedEdits_one = "۱ تغییر قابل اعمال نبود:"
|
||||
skippedEdits_other = "{{count}} تغییر قابل اعمال نبود:"
|
||||
skippedEditsTruncated = "{{count}} مورد دیگر فهرست نشده است."
|
||||
unsavedChanges = "تغییرات ذخیرهنشده"
|
||||
|
||||
[formFill.create]
|
||||
commit = "افزودن {{count}} فیلد به PDF"
|
||||
empty = "هنوز هیچ فیلدی رسم نشده است."
|
||||
failed = "افزودن فیلدها ناموفق بود"
|
||||
goToField = "رفتن به این فیلد"
|
||||
hint = "نوع فیلد را انتخاب کنید، سپس آن را روی صفحه رسم کنید."
|
||||
placing = "یک فیلد {{type}} روی صفحه رسم کنید. برای توقف Esc را فشار دهید."
|
||||
preview = "برای پیشنمایش نگه دارید"
|
||||
previewHelp = "نگه دارید تا فیلدها را همانطور که پس از افزودن دیده میشوند، بدون کادرهای ویرایش، ببینید."
|
||||
removeField = "حذف فیلد"
|
||||
|
||||
[formFill.editor]
|
||||
action = "عملکرد دکمه"
|
||||
actionHelp = "کاری که دکمه هنگام کلیک انجام میدهد."
|
||||
actionNone = "هیچکدام"
|
||||
actionPrint = "چاپ"
|
||||
actionReset = "بازنشانی فرم"
|
||||
actionSubmit = "ارسال به URL"
|
||||
actionUri = "باز کردن URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "آدرسی که دکمه باز میکند یا به آن ارسال میکند."
|
||||
addOption = "افزودن گزینه"
|
||||
caption = "عنوان دکمه"
|
||||
captionHelp = "متنی که روی دکمه چاپ میشود."
|
||||
defaultValue = "مقدار پیشفرض"
|
||||
defaultValueHelp = "مقداری که فیلد پیش از پر شدن دارد. برای فیلد خالی، خالی بگذارید."
|
||||
fontSize = "اندازه قلم"
|
||||
fontSizeHelp = "اندازه متن داخل فیلد. خالی بگذارید تا نمایشگر PDF آن را متناسب کند."
|
||||
label = "برچسب"
|
||||
labelHelp = "متنی که به کسی که فرم را پر میکند نمایش داده میشود. خالی بگذارید تا از نام فیلد استفاده شود."
|
||||
maxLength = "حداکثر طول (comb)"
|
||||
maxLengthHelp = "تعداد نویسههایی را که جا میشوند محدود میکند و آنها را بهصورت خانههای با فاصله برابر رسم میکند."
|
||||
multiline = "چندخطی"
|
||||
multilineHelp = "اجازه میدهد بیش از یک خط متن وارد شود و متن در لبه فیلد به خط بعد برود."
|
||||
multiSelect = "اجازه انتخاب چندگانه"
|
||||
multiSelectHelp = "امکان انتخاب بیش از یک گزینه را همزمان فراهم میکند."
|
||||
name = "نام فیلد"
|
||||
nameHelp = "نام داخلی فیلد. هنگام خروجی گرفتن از دادهها یا پر کردن فرم از سامانهای دیگر استفاده میشود، بنابراین آن را یکتا و بدون فاصله نگه دارید."
|
||||
optionGap = "فاصله گزینهها"
|
||||
optionGapHelp = "فاصله بین دکمهها، بر حسب پوینت. خالی بگذارید تا بهطور یکنواخت در طول کادر پخش شوند."
|
||||
optionPlaceholder = "گزینه {{n}}"
|
||||
options = "گزینهها"
|
||||
optionsEmpty = "حداقل یک گزینه اضافه کنید."
|
||||
optionsHelp = "گزینههای ارائهشده در فهرست. هرکدام دقیقاً همانطور که تایپ شده ذخیره میشود، پس آنها را کوتاه و متمایز نگه دارید."
|
||||
optionSize = "اندازه گزینه"
|
||||
optionSizeHelp = "عرض و ارتفاع هر دکمه، بر حسب پوینت. خالی بگذارید تا با کادری که رسم کردهاید متناسب شوند."
|
||||
readOnly = "فقط خواندنی"
|
||||
readOnlyHelp = "مقداری را نمایش میدهد اما از ویرایش آن جلوگیری میکند."
|
||||
removeOption = "حذف گزینه"
|
||||
required = "الزامی"
|
||||
requiredHelp = "فرم تا زمانی که این فیلد پر نشود نمیتواند ارسال شود."
|
||||
signatureNote = "فقط جاینگهدارنده است - شما اینجا امضا نمیکنید. این مشخص میکند امضا کجا قرار میگیرد تا یک امضاکننده PDF (Adobe Acrobat، یک سرویس امضا و غیره) هنگام امضای سند، امضا را در این محل قرار دهد."
|
||||
tooltip = "راهنمای شناور"
|
||||
tooltipHelp = "راهنمایی که وقتی کسی در نمایشگر PDF نشانگر را روی فیلد میبرد نمایش داده میشود."
|
||||
type = "نوع"
|
||||
typeHelp = "این فیلد از چه نوعی است. تغییر آن فیلد را دوباره میسازد، بنابراین مقدار فعلی منتقل نمیشود."
|
||||
|
||||
[formFill.mode]
|
||||
create = "ایجاد"
|
||||
fill = "پر کردن"
|
||||
label = "حالت ویرایشگر فرم"
|
||||
modify = "ویرایش"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "ذخیره {{count}} تغییر"
|
||||
delete = "حذف"
|
||||
empty = "این PDF هنوز هیچ فیلد فرمی ندارد."
|
||||
failed = "ذخیره تغییرات ناموفق بود"
|
||||
groupSizeHint = "استفاده از اندازه گزینه"
|
||||
hint = "فیلدی را برای ویرایش ویژگیهایش انتخاب کنید، آن را روی صفحه بکشید، یا حذفش کنید."
|
||||
restore = "بازیابی"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "بستن نوار کناری"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "تغییر محدودیتها و مجوزهای سند"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "تغییر مجوزها"
|
||||
|
||||
[home.classify]
|
||||
desc = "نوع این سند را شناسایی و آن را برچسبگذاری کنید."
|
||||
title = "طبقهبندی"
|
||||
|
||||
[home.compare]
|
||||
desc = "مقایسه و نمایش تفاوتها بین 2 سند PDF"
|
||||
tags = "تفاوت"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "چیزی برای گزارش وجود ندارد."
|
||||
handoffUnavailable = "این مرورگر اجازه نمیدهد پردازشگر سند را به ویرایشگر منتقل کند. بهجای آن، آن را از ویرایشگر باز کنید."
|
||||
noDocumentLinked = "این خطا به سند خاصی مرتبط نیست، بنابراین چیزی برای باز کردن در اینجا وجود ندارد."
|
||||
notOnThisDevice = "این سند روی این دستگاه نیست، بنابراین نمیتوان آن را اینجا باز کرد."
|
||||
occurrences = "{{count}} بار"
|
||||
open = "اعلانها"
|
||||
title = "اعلانها"
|
||||
unread = "خواندهنشده"
|
||||
|
||||
[notifications.action]
|
||||
failed = "انجام نشد. کمی بعد دوباره تلاش کنید."
|
||||
unavailable = "برای این اعلان در دسترس نیست."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "کپی شد"
|
||||
copy = "کپی خطا"
|
||||
less = "نمایش کمتر"
|
||||
more = "نمایش پیام کامل"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "پیشتر"
|
||||
new = "جدید"
|
||||
|
||||
[oauth.error]
|
||||
message = "احراز هویت موفق نبود. میتوانید این پنجره را ببندید و دوباره تلاش کنید."
|
||||
title = "احراز هویت ناموفق بود"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "از {{allowance}} PDF رایگان استفاده شده"
|
||||
capSuffix_other = "از {{allowance}} PDF رایگان استفاده شده"
|
||||
eyebrow = "آزمایشی Processor"
|
||||
statusLabel_one = "{{remaining}} باقیمانده"
|
||||
statusLabel_other = "{{remaining}} باقیمانده"
|
||||
statusLabel_one = "{{used}} استفادهشده"
|
||||
statusLabel_other = "{{used}} استفادهشده"
|
||||
sub = "از PDF Editor رایگان استفاده کنید. برای پردازش خودکار PDFها پرداخت کنید."
|
||||
title_one = "{{allowance}} PDF را رایگان پردازش کنید"
|
||||
title_other = "{{allowance}} PDF را رایگان پردازش کنید"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "مشاهده فایل"
|
||||
viewInProcessor = "مشاهده در پردازشگر"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "این خطا برای سند خاصی ثبت نشده است، بنابراین چیزی برای باز کردن در اینجا وجود ندارد."
|
||||
unattended = "این فایل از طریق یک پوشه، bucket یا webhook وارد شده است، بنابراین مرورگر هیچکس آن را برای باز کردن در اختیار ندارد."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "منابع"
|
||||
connectSource = "اتصال منبع"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "پیشرفته"
|
||||
back = "بازگشت به منابع"
|
||||
backToSource = "بازگشت به تنظیمات منبع"
|
||||
backToTypes = "همه انواع منبع"
|
||||
cancel = "لغو"
|
||||
chooseHint = "انتخاب کنید اسناد از کجا میآیند. اتصالدهندههای خاکستریشده در راه هستند."
|
||||
@@ -8726,7 +8845,6 @@ create = "ایجاد منبع"
|
||||
createTitle = "اتصال یک منبع"
|
||||
delete = "حذف"
|
||||
editTitle = "ویرایش منبع"
|
||||
enabled = "فعال"
|
||||
save = "ذخیره تغییرات"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: پردازش هر فایل فقط یکبار"
|
||||
snapshot = "Snapshot: خواندن دوباره پوشه در هر اجرا"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "اگر فایلهای شما در پوشههای تودرتو سازماندهی شدهاند، زیرپوشهها را هم شامل کنید، یا فقط سطح بالایی را پایش کنید."
|
||||
label = "عمق پوشه"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "اندازه، تاریخ و بررسی محتوا"
|
||||
stat = "اندازه و تاریخ تغییر"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "اینکه پس از پردازش، فایل اصلی در جای خود باقی بماند یا نه. اگر فایل اصلی در جای خود باقی بماند، دفعه بعد که خط لوله منبع را اسکن کند دوباره پردازش میشود."
|
||||
label = "حالت خواندن"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "مصرف: هر فایل را یکبار پردازش کن"
|
||||
snapshot = "Snapshot: در هر اجرا پوشه را دوباره بخوان"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "اگر فایلهای شما در پوشههای تودرتو سازماندهی شدهاند، زیرپوشهها را هم شامل کنید، یا فقط سطح بالایی را پایش کنید."
|
||||
label = "عمق پوشه"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "سرور Stirling-PDF شما آفلاین است و \
|
||||
expired = "نشست شما به پایان رسیده است. لطفاً صفحه را تازهسازی کرده و دوباره تلاش کنید."
|
||||
|
||||
[settings]
|
||||
backToSections = "همه تنظیمات"
|
||||
close = "بستن"
|
||||
title = "تنظیمات"
|
||||
|
||||
[settings.ai]
|
||||
documents = "اسناد و RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "جستجو"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "جستجوی ابزارها..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "قالببندی پیشرفته"
|
||||
ai = "AI"
|
||||
automation = "اتوماسیون"
|
||||
developerTools = "ابزارهای توسعهدهنده"
|
||||
documentReview = "بازبینی سند"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "ستون {{index}}"
|
||||
convertToPdf = "تبدیل به PDF"
|
||||
csvStats = "{{rows}} ردیف · {{columns}} ستون · {{size}}"
|
||||
emptyFile = "فایل خالی"
|
||||
htmlHidePreview = "پنهان کردن پیشنمایش"
|
||||
htmlPreview = "پیشنمایش HTML"
|
||||
htmlPreviewMobileHidden = "صفحات HTML برای عرضهای دسکتاپ چیدمان شدهاند، بنابراین پیشنمایش اینجا بهطور پیشفرض خاموش است."
|
||||
htmlPreviewWarning = "پیشنمایش HTML — ممکن است منابع خارجی بارگذاری نشوند · {{size}}"
|
||||
htmlShowPreview = "با این حال پیشنمایش را نشان بده"
|
||||
invalidJson = "JSON نامعتبر — محتوای خام نمایش داده میشود"
|
||||
lineNumbers = "شماره خطوط"
|
||||
loading = "در حال بارگذاری..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "خروجی گرفتن از PDF"
|
||||
exportSelected = "خروجی گرفتن از صفحات منتخب"
|
||||
formFill = "پر کردن فرم"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "اقدامات بیشتر"
|
||||
multiTool = "ابزار چندگانه"
|
||||
panMode = "حالت جابهجایی"
|
||||
print = "چاپ PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "انتخاب همه"
|
||||
selectByNumber = "انتخاب بر اساس شمارههای صفحه"
|
||||
selectLanguage = "انتخاب زبان"
|
||||
share = "اشتراکگذاری"
|
||||
showAllTools = "نمایش همه ابزارها"
|
||||
showFewerTools = "جمع کردن نوار ابزار"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "نمایش/مخفی کردن حاشیهنویسیها"
|
||||
toggleAttachments = "نمایش/مخفی کردن پیوستها"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Faux"
|
||||
fileNotSavedToDisk = "Non enregistré sur le disque"
|
||||
fileSavedToDisk = "Fichier enregistré sur le disque"
|
||||
fileSelected = "Sélectionné : {{filename}}"
|
||||
filesSelected = "fichiers sélectionnés"
|
||||
filesSelected = "{{count}} fichiers"
|
||||
font = "Police"
|
||||
fontSizeTooltip = "Taille du texte du numéro de page en points. Une valeur plus élevée crée un texte plus grand."
|
||||
fontTypeTooltip = "Famille de police pour les numéros de page. Choisissez selon le style de votre document."
|
||||
@@ -3969,6 +3969,7 @@ back = "Retour"
|
||||
backToFolder = "Retour à {{folder}}"
|
||||
backToMyFiles = "Retour à Mes fichiers"
|
||||
breadcrumbs = "Chemin du dossier"
|
||||
bulkActions = "Actions"
|
||||
cancel = "Annuler"
|
||||
classification = "Classification"
|
||||
clearSelection = "Effacer la sélection"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Taille totale"
|
||||
type = "Type"
|
||||
versionHistory = "Parcours de version"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} filtres actifs"
|
||||
clearAll = "Effacer les filtres"
|
||||
label = "Filtres"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Annuler"
|
||||
error = "Impossible d’enregistrer le dossier. Réessayez."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Trier les fichiers"
|
||||
modifiedAsc = "Plus ancien d’abord"
|
||||
modifiedDesc = "Plus récent d’abord"
|
||||
nameAsc = "Nom A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Tout est enregistré"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Impossible d’appliquer les modifications"
|
||||
extractCsvError = "Échec de l’extraction du CSV"
|
||||
extractXlsxError = "Échec de l’extraction du XLSX"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Aplatir après remplissage"
|
||||
goToPage = "Accéder à cette page"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Page"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "obl."
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Réanalyser les champs"
|
||||
rescanFormFields = "Réanalyser les champs du formulaire"
|
||||
save = "Enregistrer"
|
||||
saveShortcut = "Ctrl+S pour enregistrer"
|
||||
skippedEdits_one = "1 modification n’a pas pu être appliquée :"
|
||||
skippedEdits_other = "{{count}} modifications n’ont pas pu être appliquées :"
|
||||
skippedEditsTruncated = "{{count}} autres non répertoriées."
|
||||
unsavedChanges = "Modifications non enregistrées"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Ajouter {{count}} champ(s) au PDF"
|
||||
empty = "Aucun champ n’a encore été dessiné."
|
||||
failed = "Échec de l’ajout des champs"
|
||||
goToField = "Accéder à ce champ"
|
||||
hint = "Choisissez un type de champ, puis dessinez-le sur la page."
|
||||
placing = "Dessinez un champ {{type}} sur la page. Appuyez sur Échap pour arrêter."
|
||||
preview = "Maintenir pour prévisualiser"
|
||||
previewHelp = "Maintenez pour voir les champs tels qu’ils apparaîtront une fois ajoutés, sans les contours de modification."
|
||||
removeField = "Supprimer le champ"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Action du bouton"
|
||||
actionHelp = "Ce que fait le bouton lorsqu’on clique dessus."
|
||||
actionNone = "Aucune"
|
||||
actionPrint = "Imprimer"
|
||||
actionReset = "Réinitialiser le formulaire"
|
||||
actionSubmit = "Envoyer à l’URL"
|
||||
actionUri = "Ouvrir l’URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "L’adresse que le bouton ouvre ou à laquelle il envoie les données."
|
||||
addOption = "Ajouter une option"
|
||||
caption = "Libellé du bouton"
|
||||
captionHelp = "Le texte affiché sur le bouton."
|
||||
defaultValue = "Valeur par défaut"
|
||||
defaultValueHelp = "Ce que le champ contient avant d’être rempli. Laissez vide pour un champ vide."
|
||||
fontSize = "Taille de police"
|
||||
fontSizeHelp = "Taille du texte dans le champ. Laissez vide pour permettre au lecteur PDF de l’ajuster."
|
||||
label = "Libellé"
|
||||
labelHelp = "Le texte affiché à la personne qui remplit le formulaire. Laissez-le vide pour utiliser le nom du champ par défaut."
|
||||
maxLength = "Longueur max. (peigne)"
|
||||
maxLengthHelp = "Limite le nombre de caractères qui tiennent, affichés sous forme de cases régulièrement espacées."
|
||||
multiline = "Multiligne"
|
||||
multilineHelp = "Autorise plusieurs lignes de texte et le renvoi à la ligne au bord du champ."
|
||||
multiSelect = "Autoriser la sélection multiple"
|
||||
multiSelectHelp = "Permet de choisir plusieurs options à la fois."
|
||||
name = "Nom du champ"
|
||||
nameHelp = "Le nom interne du champ. Utilisé lors de l’exportation de données ou du remplissage du formulaire depuis un autre système ; gardez-le donc unique et sans espaces."
|
||||
optionGap = "Espacement des options"
|
||||
optionGapHelp = "Écart entre les boutons, en points. Laissez vide pour les répartir uniformément dans la zone."
|
||||
optionPlaceholder = "Option {{n}}"
|
||||
options = "Options"
|
||||
optionsEmpty = "Ajoutez au moins une option."
|
||||
optionsHelp = "Les choix proposés dans la liste. Chacun est enregistré tel qu’il est saisi ; veillez donc à ce qu’ils soient courts et distincts."
|
||||
optionSize = "Taille des options"
|
||||
optionSizeHelp = "Largeur et hauteur de chaque bouton, en points. Laissez vide pour les adapter à la zone que vous avez dessinée."
|
||||
readOnly = "Lecture seule"
|
||||
readOnlyHelp = "Affiche une valeur, mais empêche toute modification."
|
||||
removeOption = "Supprimer l’option"
|
||||
required = "Obligatoire"
|
||||
requiredHelp = "Le formulaire ne peut pas être envoyé tant que ce champ n’est pas rempli."
|
||||
signatureNote = "Espace réservé uniquement - vous ne signez pas ici. Il indique l’emplacement d’une signature afin qu’un outil de signature PDF (Adobe Acrobat, un service de signature, etc.) place la signature à cet endroit lorsque le document est signé."
|
||||
tooltip = "Infobulle"
|
||||
tooltipHelp = "L’indication affichée lorsqu’une personne survole le champ dans un lecteur PDF."
|
||||
type = "Type"
|
||||
typeHelp = "Le type de champ. Le modifier reconstruit le champ, sa valeur actuelle n’est donc pas conservée."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Créer"
|
||||
fill = "Remplir"
|
||||
label = "Mode de l’éditeur de formulaire"
|
||||
modify = "Modifier"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Enregistrer {{count}} modification(s)"
|
||||
delete = "Supprimer"
|
||||
empty = "Ce PDF ne contient pas encore de champs de formulaire."
|
||||
failed = "Échec de l’enregistrement des modifications"
|
||||
groupSizeHint = "Utiliser la taille des options"
|
||||
hint = "Sélectionnez un champ pour modifier ses propriétés, faites-le glisser sur la page ou supprimez-le."
|
||||
restore = "Restaurer"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Fermer la barre latérale"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Modifier les restrictions et permissions du document"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Modifier les permissions"
|
||||
|
||||
[home.classify]
|
||||
desc = "Identifiez le type de document dont il s’agit et étiquetez-le."
|
||||
title = "Classer"
|
||||
|
||||
[home.compare]
|
||||
desc = "Comparez et visualisez les différences entre deux PDF."
|
||||
tags = "différence"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Rien à signaler."
|
||||
handoffUnavailable = "Ce navigateur ne permettra pas au processeur de transmettre le document à l’éditeur. Ouvrez-le plutôt depuis l’éditeur."
|
||||
noDocumentLinked = "Cet échec n’est pas lié à un document spécifique, il n’y a donc rien à ouvrir ici."
|
||||
notOnThisDevice = "Ce document n’est pas sur cet appareil, il ne peut donc pas être ouvert ici."
|
||||
occurrences = "{{count}} fois"
|
||||
open = "Notifications"
|
||||
title = "Notifications"
|
||||
unread = "Non lues"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Cela n’a pas fonctionné. Réessayez dans un instant."
|
||||
unavailable = "Non disponible pour cette notification."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Copié"
|
||||
copy = "Copier l’erreur"
|
||||
less = "Afficher moins"
|
||||
more = "Afficher le message complet"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Plus tôt"
|
||||
new = "Nouvelles"
|
||||
|
||||
[oauth.error]
|
||||
message = "L'authentification n'a pas abouti. Vous pouvez fermer cette fenêtre et réessayer."
|
||||
title = "Échec de l'authentification"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "sur {{allowance}} PDF gratuits utilisés"
|
||||
capSuffix_other = "sur {{allowance}} PDF gratuits utilisés"
|
||||
eyebrow = "Essai Processor"
|
||||
statusLabel_one = "{{remaining}} restant"
|
||||
statusLabel_other = "{{remaining}} restants"
|
||||
statusLabel_one = "{{used}} utilisé"
|
||||
statusLabel_other = "{{used}} utilisés"
|
||||
sub = "Utilisez l’éditeur PDF gratuitement. Payez pour traiter les PDF automatiquement."
|
||||
title_one = "Traitez {{allowance}} PDF gratuitement"
|
||||
title_other = "Traitez {{allowance}} PDF gratuitement"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Afficher le fichier"
|
||||
viewInProcessor = "Afficher dans le processeur"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Cet échec n’a pas été enregistré pour un document spécifique, il n’y a donc rien à ouvrir ici."
|
||||
unattended = "Ce fichier a été fourni par un dossier, un bucket ou un webhook, aucun navigateur d’utilisateur ne le conserve donc pour l’ouvrir."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Sources"
|
||||
connectSource = "Connecter une source"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Avancé"
|
||||
back = "Retour aux sources"
|
||||
backToSource = "Retour à la configuration de la source"
|
||||
backToTypes = "Tous les types de sources"
|
||||
cancel = "Annuler"
|
||||
chooseHint = "Choisissez d’où viennent les documents. Les connecteurs grisés arrivent bientôt."
|
||||
@@ -8726,7 +8845,6 @@ create = "Créer une source"
|
||||
createTitle = "Connecter une source"
|
||||
delete = "Supprimer"
|
||||
editTitle = "Modifier la source"
|
||||
enabled = "Activé"
|
||||
save = "Enregistrer les modifications"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consommer : traiter chaque fichier une seule fois"
|
||||
snapshot = "Instantané : relire le dossier à chaque exécution"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Incluez les sous-dossiers si vos fichiers sont organisés dans des dossiers imbriqués, ou surveillez uniquement le niveau supérieur."
|
||||
label = "Profondeur du dossier"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Vérification de la taille, de la date et du contenu"
|
||||
stat = "Taille et date de modification"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Indique s’il faut laisser le fichier original en place après son traitement. Si le fichier original est laissé en place, il sera retraité la prochaine fois que le pipeline analysera la source."
|
||||
label = "Mode de lecture"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Consommer : traiter chaque fichier une fois"
|
||||
snapshot = "Instantané : relire le dossier à chaque exécution"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Incluez les sous-dossiers si vos fichiers sont organisés dans des dossiers imbriqués, ou surveillez uniquement le niveau supérieur."
|
||||
label = "Profondeur du dossier"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Your Stirling-PDF server is offline and \"{{endpoint}
|
||||
expired = "Votre session a expiré. Veuillez recharger la page et réessayer."
|
||||
|
||||
[settings]
|
||||
backToSections = "Tous les paramètres"
|
||||
close = "Fermer"
|
||||
title = "Paramètres"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Documents et RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Rechercher"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Rechercher des outils…"
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Mise en forme avancée"
|
||||
ai = "IA"
|
||||
automation = "Automatisation"
|
||||
developerTools = "Outils développeur"
|
||||
documentReview = "Révision de document"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Colonne {{index}}"
|
||||
convertToPdf = "Convertir en PDF"
|
||||
csvStats = "{{rows}} lignes · {{columns}} colonnes · {{size}}"
|
||||
emptyFile = "Fichier vide"
|
||||
htmlHidePreview = "Masquer l’aperçu"
|
||||
htmlPreview = "Aperçu HTML"
|
||||
htmlPreviewMobileHidden = "Les pages HTML sont conçues pour des largeurs d’écran de bureau, l’aperçu est donc désactivé par défaut ici."
|
||||
htmlPreviewWarning = "Aperçu HTML — les ressources externes peuvent ne pas se charger · {{size}}"
|
||||
htmlShowPreview = "Afficher quand même l’aperçu"
|
||||
invalidJson = "JSON invalide — affichage du contenu brut"
|
||||
lineNumbers = "Numéros de ligne"
|
||||
loading = "Chargement…"
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Exporter le PDF"
|
||||
exportSelected = "Exporter les pages sélectionnées"
|
||||
formFill = "Remplir le formulaire"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Plus d’actions"
|
||||
multiTool = "Outil multifonction"
|
||||
panMode = "Mode panoramique"
|
||||
print = "Imprimer le PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Tout sélectionner"
|
||||
selectByNumber = "Sélectionner par numéros de pages"
|
||||
selectLanguage = "Sélectionner la langue"
|
||||
share = "Partager"
|
||||
showAllTools = "Afficher tous les outils"
|
||||
showFewerTools = "Réduire la barre d’outils"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Basculer la visibilité des annotations"
|
||||
toggleAttachments = "Basculer les pièces jointes"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Bréagach"
|
||||
fileNotSavedToDisk = "Gan sábháil ar an diosca"
|
||||
fileSavedToDisk = "Comhad sábháilte ar an diosca"
|
||||
fileSelected = "Roghnaithe: {{filename}}"
|
||||
filesSelected = "comhaid roghnaithe"
|
||||
filesSelected = "{{count}} comhad"
|
||||
font = "Cló"
|
||||
fontSizeTooltip = "Méid téacs na huimhreach leathanaigh i bpointí. Cruthaíonn méideanna níos mó téacs níos mó."
|
||||
fontTypeTooltip = "Clannchló don uimhriú leathanaigh. Roghnaigh bunaithe ar stíl do cháipéis."
|
||||
@@ -3969,6 +3969,7 @@ back = "Ar ais"
|
||||
backToFolder = "Ar ais go {{folder}}"
|
||||
backToMyFiles = "Ar ais go Mo Chomhaid"
|
||||
breadcrumbs = "Conair an fhillteáin"
|
||||
bulkActions = "Gníomhartha"
|
||||
cancel = "Cealaigh"
|
||||
classification = "Aicmiú"
|
||||
clearSelection = "Glan an roghnú"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Méid iomlán"
|
||||
type = "Cineál"
|
||||
versionHistory = "Turas an leagain"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "Tá {{count}} scagaire gníomhach"
|
||||
clearAll = "Glan scagairí"
|
||||
label = "Scagairí"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Cealaigh"
|
||||
error = "Níorbh fhéidir an fillteán a shábháil. Bain triail eile as."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Sórtáil comhaid"
|
||||
modifiedAsc = "Is sine ar dtús"
|
||||
modifiedDesc = "Is nua ar dtús"
|
||||
nameAsc = "Ainm A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Sábháilte go léir"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Níorbh fhéidir na hathruithe a chur i bhfeidhm"
|
||||
extractCsvError = "Theip ar eastóscadh CSV"
|
||||
extractXlsxError = "Theip ar eastóscadh XLSX"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Cothromaigh tar éis líonadh"
|
||||
goToPage = "Téigh go dtí an leathanach seo"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Leathanach"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "riach"
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Athscan na réimsí"
|
||||
rescanFormFields = "Athscan réimsí an fhoirme"
|
||||
save = "Sábháil"
|
||||
saveShortcut = "Ctrl+S chun sábháil"
|
||||
skippedEdits_one = "Níorbh fhéidir 1 athrú a chur i bhfeidhm:"
|
||||
skippedEdits_other = "Níorbh fhéidir {{count}} athrú a chur i bhfeidhm:"
|
||||
skippedEditsTruncated = "{{count}} eile nach bhfuil liostaithe."
|
||||
unsavedChanges = "Athruithe gan sábháil"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Cuir {{count}} réimse le PDF"
|
||||
empty = "Níor tarraingíodh aon réimse fós."
|
||||
failed = "Theip ar réimsí a chur leis"
|
||||
goToField = "Téigh go dtí an réimse seo"
|
||||
hint = "Roghnaigh cineál réimse, ansin tarraing ar an leathanach é."
|
||||
placing = "Tarraing réimse {{type}} ar an leathanach. Brúigh Esc chun stopadh."
|
||||
preview = "Coinnigh brúite chun réamhamharc a fheiceáil"
|
||||
previewHelp = "Coinnigh brúite chun na réimsí a fheiceáil mar a bheidh siad tar éis iad a chur leis, gan imlínte eagarthóireachta."
|
||||
removeField = "Bain réimse"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Gníomh an chnaipe"
|
||||
actionHelp = "An rud a dhéanann an cnaipe nuair a chliceáiltear air."
|
||||
actionNone = "Níl aon cheann"
|
||||
actionPrint = "Priontáil"
|
||||
actionReset = "Athshocraigh an fhoirm"
|
||||
actionSubmit = "Seol chuig URL"
|
||||
actionUri = "Oscail URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "An seoladh a osclaíonn an cnaipe nó a seolann sé chuige."
|
||||
addOption = "Cuir rogha leis"
|
||||
caption = "Foscríbhinn an chnaipe"
|
||||
captionHelp = "An téacs a phriontáiltear ar aghaidh an chnaipe."
|
||||
defaultValue = "Luach réamhshocraithe"
|
||||
defaultValueHelp = "An méid atá sa réimse sula líonann duine ar bith isteach é. Fág bán é le haghaidh réimse folamh."
|
||||
fontSize = "Clómhéid"
|
||||
fontSizeHelp = "Méid an téacs laistigh den réimse. Fág bán é chun ligean don léitheoir PDF é a mhéidú le bheith oiriúnach."
|
||||
label = "Lipéad"
|
||||
labelHelp = "An téacs a thaispeántar don té a líonann an fhoirm. Fág bán é chun ainm an réimse a úsáid mar réamhshocrú."
|
||||
maxLength = "Fad uasta (comb)"
|
||||
maxLengthHelp = "Cuireann sé teorainn leis an líon carachtar a oireann, arna tharraingt mar bhoscaí spásáilte go cothrom."
|
||||
multiline = "Illíneach"
|
||||
multilineHelp = "Ceadaíonn sé níos mó ná líne amháin téacs agus fillteann sé ag imeall an réimse."
|
||||
multiSelect = "Ceadaigh ilroghnú"
|
||||
multiSelectHelp = "Ligeann sé níos mó ná rogha amháin a roghnú ag an am céanna."
|
||||
name = "Ainm an réimse"
|
||||
nameHelp = "Ainm inmheánach an réimse. Úsáidtear é agus sonraí á n-easpórtáil nó agus an fhoirm á líonadh ó chóras eile, mar sin coinnigh uathúil é agus gan spásanna."
|
||||
optionGap = "Spásáil roghanna"
|
||||
optionGapHelp = "Bearna idir cnaipí, i bpointí. Fág bán é chun iad a leathadh go cothrom síos an bosca."
|
||||
optionPlaceholder = "Rogha {{n}}"
|
||||
options = "Roghanna"
|
||||
optionsEmpty = "Cuir rogha amháin ar a laghad leis."
|
||||
optionsHelp = "Na roghanna a thairgtear sa liosta. Stóráiltear gach ceann mar a chlóscríobhtar é, mar sin coinnigh gearr agus sainiúil iad."
|
||||
optionSize = "Méid rogha"
|
||||
optionSizeHelp = "Leithead agus airde gach cnaipe, i bpointí. Fág bán é chun iad a chur in oiriúint don bhosca a tharraing tú."
|
||||
readOnly = "Léamh amháin"
|
||||
readOnlyHelp = "Taispeánann sé luach ach cuireann sé cosc ar aon duine é a chur in eagar."
|
||||
removeOption = "Bain rogha"
|
||||
required = "Riachtanach"
|
||||
requiredHelp = "Ní féidir an fhoirm a sheoladh go dtí go mbeidh an réimse seo líonta isteach."
|
||||
signatureNote = "Áitshealbhóir amháin - ní shíníonn tú anseo. Marcálann sé an áit a mbaineann síniú léi ionas go gcuirfidh sínitheoir PDF (Adobe Acrobat, seirbhís sínithe, srl.) an síniú san áit seo nuair a shínítear an doiciméad."
|
||||
tooltip = "Leid uirlisí"
|
||||
tooltipHelp = "An leid a thaispeántar nuair a fhanann duine os cionn an réimse i léitheoir PDF."
|
||||
type = "Cineál"
|
||||
typeHelp = "Cén cineál réimse é seo. Má athraítear é, atógtar an réimse, mar sin ní thugtar a luach reatha ar aghaidh."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Cruthaigh"
|
||||
fill = "Líon"
|
||||
label = "Mód eagarthóra foirme"
|
||||
modify = "Mionathraigh"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Sábháil {{count}} athrú"
|
||||
delete = "Scrios"
|
||||
empty = "Níl aon réimsí foirme sa PDF seo fós."
|
||||
failed = "Theip ar athruithe a shábháil"
|
||||
groupSizeHint = "Úsáid Méid rogha"
|
||||
hint = "Roghnaigh réimse chun a airíonna a chur in eagar, tarraing ar an leathanach é, nó scrios é."
|
||||
restore = "Athchóirigh"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Dún an barra taoibh"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Athraigh srianta agus ceadanna cáipéise"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Athrú Ceadanna"
|
||||
|
||||
[home.classify]
|
||||
desc = "Aithin cén cineál doiciméid é seo agus clibeáil é."
|
||||
title = "Aicmigh"
|
||||
|
||||
[home.compare]
|
||||
desc = "Déanann sé na difríochtaí idir 2 Dhoiciméad PDF a chur i gcomparáid agus a thaispeáint"
|
||||
tags = "difríocht"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Níl aon rud le tuairisciú."
|
||||
handoffUnavailable = "Ní ligfidh an brabhsálaí seo don phróiseálaí an doiciméad a chur ar aghaidh chuig an eagarthóir. Oscail ón eagarthóir ina ionad sin é."
|
||||
noDocumentLinked = "Níl an teip seo nasctha le doiciméad ar leith, mar sin níl aon rud le hoscailt anseo."
|
||||
notOnThisDevice = "Níl an doiciméad seo ar an ngléas seo, mar sin ní féidir é a oscailt anseo."
|
||||
occurrences = "{{count}} uair"
|
||||
open = "Fógraí"
|
||||
title = "Fógraí"
|
||||
unread = "Gan léamh"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Níor oibrigh sé sin. Bain triail eile as ar ball."
|
||||
unavailable = "Níl sé ar fáil don fhógra seo."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Cóipeáilte"
|
||||
copy = "Cóipeáil an earráid"
|
||||
less = "Taispeáin níos lú"
|
||||
more = "Taispeáin an teachtaireacht iomlán"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Níos luaithe"
|
||||
new = "Nua"
|
||||
|
||||
[oauth.error]
|
||||
message = "Níor éirigh leis an bhfíordheimhniú. Is féidir leat an fhuinneog seo a dhúnadh agus triail eile a bhaint as."
|
||||
title = "Theip ar an bhfíordheimhniú"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "as {{allowance}} PDF saor in aisce úsáidte"
|
||||
capSuffix_other = "as {{allowance}} PDFs saor in aisce úsáidte"
|
||||
eyebrow = "Triail Processor"
|
||||
statusLabel_one = "{{remaining}} fágtha"
|
||||
statusLabel_other = "{{remaining}} fágtha"
|
||||
statusLabel_one = "{{used}} úsáidte"
|
||||
statusLabel_other = "{{used}} úsáidte"
|
||||
sub = "Úsáid an PDF Editor saor in aisce. Íoc chun PDFs a phróiseáil go huathoibríoch."
|
||||
title_one = "Próiseáil {{allowance}} PDF saor in aisce"
|
||||
title_other = "Próiseáil {{allowance}} PDFs saor in aisce"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Féach ar an gcomhad"
|
||||
viewInProcessor = "Féach sa phróiseálaí"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Níor taifeadadh an teip seo i gcoinne doiciméid ar leith, mar sin níl aon rud anseo le hoscailt."
|
||||
unattended = "Tháinig an comhad seo ó fhillteán, bucket nó webhook, mar sin níl sé á choinneáil ag brabhsálaí aon duine lena oscailt."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Foinsí"
|
||||
connectSource = "Ceangail foinse"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Casta"
|
||||
back = "Ar ais chuig foinsí"
|
||||
backToSource = "Ar ais go socrú na foinse"
|
||||
backToTypes = "Gach cineál foinse"
|
||||
cancel = "Cealaigh"
|
||||
chooseHint = "Roghnaigh cá as a dtagann doiciméid. Tá nascóirí liathaithe ar an mbealach."
|
||||
@@ -8726,7 +8845,6 @@ create = "Cruthaigh foinse"
|
||||
createTitle = "Ceangail foinse"
|
||||
delete = "Scrios"
|
||||
editTitle = "Cuir foinse in eagar"
|
||||
enabled = "Cumasaithe"
|
||||
save = "Sábháil athruithe"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: próiseáil gach comhad uair amháin"
|
||||
snapshot = "Snapshot: athléigh an fillteán gach rith"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Cuir fofhillteáin san áireamh má tá do chuid comhad eagraithe i bhfillteáin neadaithe, nó déan monatóireacht ar an leibhéal barr amháin."
|
||||
label = "Doimhneacht fillteáin"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Méid, dáta agus seiceáil ábhair"
|
||||
stat = "Méid agus dáta modhnaithe"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "An bhfágfar an bunchomhad ina áit tar éis é a phróiseáil. Má fhágtar an bunchomhad ina áit, próiseálfar arís é an chéad uair eile a scanann an phíblíne an fhoinse."
|
||||
label = "Mód léite"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Ithe: próiseáil gach comhad uair amháin"
|
||||
snapshot = "Snapshot: athléigh an fillteán gach rith"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Cuir fofhillteáin san áireamh má tá do chuid comhad eagraithe i bhfillteáin neadaithe, nó déan monatóireacht ar an leibhéal barr amháin."
|
||||
label = "Doimhneacht fillteáin"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Tá do fhreastalaí Stirling-PDF as líne agus níl \
|
||||
expired = "Tá do sheisiún imithe in éag. Athnuaigh an leathanach agus bain triail eile as."
|
||||
|
||||
[settings]
|
||||
backToSections = "Gach socrú"
|
||||
close = "Dún"
|
||||
title = "Socruithe"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Doiciméid & RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Cuardach"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Cuardaigh uirlisí..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Formáidiú Casta"
|
||||
ai = "AI"
|
||||
automation = "Uathoibriú"
|
||||
developerTools = "Uirlisí Forbróra"
|
||||
documentReview = "Athbhreithniú Doiciméid"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Colún {{index}}"
|
||||
convertToPdf = "Tiontaigh go PDF"
|
||||
csvStats = "{{rows}} sraith · {{columns}} colún · {{size}}"
|
||||
emptyFile = "Comhad folamh"
|
||||
htmlHidePreview = "Folaigh réamhamharc"
|
||||
htmlPreview = "Réamhamharc HTML"
|
||||
htmlPreviewMobileHidden = "Leagtar amach leathanaigh HTML do leitheadanna deisce, mar sin tá an réamhamharc múchta de réir réamhshocraithe anseo."
|
||||
htmlPreviewWarning = "Réamhamharc HTML — seans nach lódálfar acmhainní seachtracha · {{size}}"
|
||||
htmlShowPreview = "Taispeáin réamhamharc mar sin féin"
|
||||
invalidJson = "JSON neamhbhailí — á thaispeáint amh"
|
||||
lineNumbers = "Uimhreacha línte"
|
||||
loading = "Á lódáil..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Easpórtáil PDF"
|
||||
exportSelected = "Easpórtáil na leathanaigh roghnaithe"
|
||||
formFill = "Líon foirm"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Tuilleadh gníomhartha"
|
||||
multiTool = "Il-uirlis"
|
||||
panMode = "Mód pánála"
|
||||
print = "Priontáil PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Roghnaigh uile"
|
||||
selectByNumber = "Roghnaigh de réir uimhreacha leathanaigh"
|
||||
selectLanguage = "Roghnaigh teanga"
|
||||
share = "Roinn"
|
||||
showAllTools = "Taispeáin gach uirlis"
|
||||
showFewerTools = "Laghdaigh an barra uirlisí"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Scoránaigh infheictheacht na n-anótálacha"
|
||||
toggleAttachments = "Scoránaigh iatáin"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "नहीं"
|
||||
fileNotSavedToDisk = "डिस्क पर सहेजा नहीं गया"
|
||||
fileSavedToDisk = "फ़ाइल डिस्क पर सहेजी गई"
|
||||
fileSelected = "चयनित: {{filename}}"
|
||||
filesSelected = "फ़ाइलें चयनित"
|
||||
filesSelected = "{{count}} फ़ाइलें"
|
||||
font = "फ़ॉन्ट"
|
||||
fontSizeTooltip = "पॉइंट्स में पृष्ठ संख्या के टेक्स्ट का आकार। बड़ी संख्या बड़ा टेक्स्ट बनाती है।"
|
||||
fontTypeTooltip = "पृष्ठ संख्याओं के लिए फ़ॉन्ट परिवार। अपने दस्तावेज़ शैली के आधार पर चुनें।"
|
||||
@@ -3969,6 +3969,7 @@ back = "वापस"
|
||||
backToFolder = "{{folder}} पर वापस"
|
||||
backToMyFiles = "मेरी फ़ाइलों पर वापस"
|
||||
breadcrumbs = "फ़ोल्डर पथ"
|
||||
bulkActions = "कार्रवाइयाँ"
|
||||
cancel = "रद्द करें"
|
||||
classification = "वर्गीकरण"
|
||||
clearSelection = "चयन साफ़ करें"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "कुल आकार"
|
||||
type = "प्रकार"
|
||||
versionHistory = "संस्करण यात्रा"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} फ़िल्टर सक्रिय"
|
||||
clearAll = "फ़िल्टर साफ़ करें"
|
||||
label = "फ़िल्टर"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "रद्द करें"
|
||||
error = "फ़ोल्डर सहेजा नहीं जा सका। पुनः प्रयास करें।"
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "फ़ाइलें क्रमबद्ध करें"
|
||||
modifiedAsc = "सबसे पुराने पहले"
|
||||
modifiedDesc = "हाल के पहले"
|
||||
nameAsc = "नाम A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "सब सहेजा गया"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "बदलाव लागू नहीं किए जा सके"
|
||||
extractCsvError = "CSV extract करने में विफल"
|
||||
extractXlsxError = "XLSX extract करने में विफल"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "भरने के बाद समतल करें"
|
||||
goToPage = "इस पेज पर जाएँ"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "पेज"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "आव."
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "फ़ील्ड्स दोबारा स्कैन क
|
||||
rescanFormFields = "फॉर्म फ़ील्ड्स दोबारा स्कैन करें"
|
||||
save = "सहेजें"
|
||||
saveShortcut = "सहेजने के लिए Ctrl+S"
|
||||
skippedEdits_one = "1 बदलाव लागू नहीं किया जा सका:"
|
||||
skippedEdits_other = "{{count}} बदलाव लागू नहीं किए जा सके:"
|
||||
skippedEditsTruncated = "{{count}} और सूचीबद्ध नहीं हैं।"
|
||||
unsavedChanges = "बिना सहेजे बदलाव"
|
||||
|
||||
[formFill.create]
|
||||
commit = "PDF में {{count}} फ़ील्ड जोड़ें"
|
||||
empty = "अभी तक कोई फ़ील्ड नहीं बनाया गया है।"
|
||||
failed = "फ़ील्ड जोड़ने में विफल"
|
||||
goToField = "इस फ़ील्ड पर जाएँ"
|
||||
hint = "फ़ील्ड प्रकार चुनें, फिर उसे पेज पर बनाएँ।"
|
||||
placing = "पेज पर {{type}} फ़ील्ड बनाएँ। रोकने के लिए Esc दबाएँ।"
|
||||
preview = "पूर्वावलोकन के लिए दबाए रखें"
|
||||
previewHelp = "फ़ील्ड जोड़े जाने के बाद वे कैसे दिखेंगे, यह देखने के लिए दबाए रखें, संपादन रूपरेखाओं के बिना।"
|
||||
removeField = "फ़ील्ड हटाएँ"
|
||||
|
||||
[formFill.editor]
|
||||
action = "बटन कार्रवाई"
|
||||
actionHelp = "क्लिक करने पर बटन क्या करता है।"
|
||||
actionNone = "कोई नहीं"
|
||||
actionPrint = "प्रिंट"
|
||||
actionReset = "फ़ॉर्म रीसेट करें"
|
||||
actionSubmit = "URL पर सबमिट करें"
|
||||
actionUri = "URL खोलें"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "वह पता जिसे बटन खोलता है या जिस पर सबमिट करता है।"
|
||||
addOption = "विकल्प जोड़ें"
|
||||
caption = "बटन कैप्शन"
|
||||
captionHelp = "बटन के सामने छपा पाठ।"
|
||||
defaultValue = "डिफ़ॉल्ट मान"
|
||||
defaultValueHelp = "किसी के भरने से पहले फ़ील्ड में क्या होता है। खाली फ़ील्ड के लिए खाली छोड़ें।"
|
||||
fontSize = "फ़ॉन्ट आकार"
|
||||
fontSizeHelp = "फ़ील्ड के अंदर पाठ का आकार। रीडर को फ़िट करने के लिए आकार तय करने देने हेतु खाली छोड़ें।"
|
||||
label = "लेबल"
|
||||
labelHelp = "फ़ॉर्म भरने वाले व्यक्ति को दिखाया जाने वाला पाठ। फ़ील्ड नाम पर वापस जाने के लिए इसे खाली छोड़ें।"
|
||||
maxLength = "अधिकतम लंबाई (comb)"
|
||||
maxLengthHelp = "फिट होने वाले वर्णों की संख्या सीमित करता है, जिन्हें समान दूरी वाले बॉक्स के रूप में बनाया जाता है।"
|
||||
multiline = "मल्टी-लाइन"
|
||||
multilineHelp = "एक से अधिक पंक्ति के पाठ की अनुमति देता है और फ़ील्ड के किनारे पर रैप करता है।"
|
||||
multiSelect = "एकाधिक चयन की अनुमति दें"
|
||||
multiSelectHelp = "एक साथ एक से अधिक विकल्प चुने जाने देता है।"
|
||||
name = "फ़ील्ड नाम"
|
||||
nameHelp = "फ़ील्ड का आंतरिक नाम। डेटा निर्यात करते समय या किसी अन्य सिस्टम से फ़ॉर्म भरते समय उपयोग होता है, इसलिए इसे अद्वितीय और स्पेस से मुक्त रखें।"
|
||||
optionGap = "विकल्प अंतराल"
|
||||
optionGapHelp = "बटनों के बीच अंतराल, पॉइंट्स में। उन्हें बॉक्स में समान रूप से नीचे तक फैलाने के लिए खाली छोड़ें।"
|
||||
optionPlaceholder = "विकल्प {{n}}"
|
||||
options = "विकल्प"
|
||||
optionsEmpty = "कम से कम एक विकल्प जोड़ें।"
|
||||
optionsHelp = "सूची में दिए गए विकल्प। प्रत्येक वैसा ही संग्रहीत होता है जैसा टाइप किया गया है, इसलिए उन्हें छोटा और अलग रखें।"
|
||||
optionSize = "विकल्प आकार"
|
||||
optionSizeHelp = "प्रत्येक बटन की चौड़ाई और ऊँचाई, पॉइंट्स में। उन्हें आपके बनाए बॉक्स में फ़िट करने के लिए खाली छोड़ें।"
|
||||
readOnly = "केवल-पठन"
|
||||
readOnlyHelp = "मान दिखाता है लेकिन किसी को भी इसे संपादित करने से रोकता है।"
|
||||
removeOption = "विकल्प हटाएँ"
|
||||
required = "आवश्यक"
|
||||
requiredHelp = "जब तक यह फ़ील्ड भरा नहीं जाता, फ़ॉर्म सबमिट नहीं किया जा सकता।"
|
||||
signatureNote = "केवल प्लेसहोल्डर - आप यहाँ हस्ताक्षर नहीं करते। यह चिह्नित करता है कि हस्ताक्षर कहाँ होना चाहिए ताकि PDF signer (Adobe Acrobat, signing service आदि) दस्तावेज़ पर हस्ताक्षर होने पर इस स्थान पर हस्ताक्षर रखे।"
|
||||
tooltip = "टूलटिप"
|
||||
tooltipHelp = "PDF रीडर में जब कोई फ़ील्ड पर होवर करता है, तब दिखने वाला संकेत।"
|
||||
type = "प्रकार"
|
||||
typeHelp = "यह किस प्रकार का फ़ील्ड है। इसे बदलने से फ़ील्ड फिर से बनता है, इसलिए इसका मौजूदा मान आगे नहीं ले जाया जाता।"
|
||||
|
||||
[formFill.mode]
|
||||
create = "बनाएँ"
|
||||
fill = "भरें"
|
||||
label = "फ़ॉर्म संपादक मोड"
|
||||
modify = "संशोधित करें"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "{{count}} बदलाव सहेजें"
|
||||
delete = "हटाएँ"
|
||||
empty = "इस PDF में अभी कोई फ़ॉर्म फ़ील्ड नहीं है।"
|
||||
failed = "बदलाव सहेजने में विफल"
|
||||
groupSizeHint = "विकल्प आकार का उपयोग करें"
|
||||
hint = "किसी फ़ील्ड के गुण संपादित करने के लिए उसे चुनें, उसे पेज पर खींचें, या हटाएँ।"
|
||||
restore = "पुनर्स्थापित करें"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "साइडबार बंद करें"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "दस्तावेज़ प्रतिबंध और अनुम
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "अनुमतियां बदलें"
|
||||
|
||||
[home.classify]
|
||||
desc = "पहचानें कि यह किस प्रकार का दस्तावेज़ है और उसे टैग करें।"
|
||||
title = "वर्गीकृत करें"
|
||||
|
||||
[home.compare]
|
||||
desc = "2 PDF दस्तावेजों के बीच अंतर की तुलना करें और दिखाएं"
|
||||
tags = "अंतर"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "रिपोर्ट करने के लिए कुछ नहीं।"
|
||||
handoffUnavailable = "यह ब्राउज़र प्रोसेसर को दस्तावेज़ संपादक को पास करने नहीं देगा। इसके बजाय इसे संपादक से खोलें।"
|
||||
noDocumentLinked = "यह विफलता किसी विशिष्ट दस्तावेज़ से जुड़ी नहीं है, इसलिए यहाँ खोलने के लिए कुछ नहीं है।"
|
||||
notOnThisDevice = "यह दस्तावेज़ इस डिवाइस पर नहीं है, इसलिए इसे यहाँ नहीं खोला जा सकता।"
|
||||
occurrences = "{{count}} बार"
|
||||
open = "सूचनाएँ"
|
||||
title = "सूचनाएँ"
|
||||
unread = "अपठित"
|
||||
|
||||
[notifications.action]
|
||||
failed = "यह काम नहीं किया। थोड़ी देर में फिर कोशिश करें।"
|
||||
unavailable = "इस सूचना के लिए उपलब्ध नहीं है।"
|
||||
|
||||
[notifications.detail]
|
||||
copied = "कॉपी किया गया"
|
||||
copy = "त्रुटि कॉपी करें"
|
||||
less = "कम दिखाएँ"
|
||||
more = "पूरा संदेश दिखाएँ"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "पहले"
|
||||
new = "नया"
|
||||
|
||||
[oauth.error]
|
||||
message = "प्रमाणीकरण सफल नहीं हुआ। आप यह विंडो बंद कर फिर से प्रयास कर सकते हैं।"
|
||||
title = "प्रमाणीकरण विफल"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "{{allowance}} मुफ़्त PDFs में से उपयोग हुआ"
|
||||
capSuffix_other = "{{allowance}} मुफ़्त PDFs में से उपयोग हुआ"
|
||||
eyebrow = "Processor trial"
|
||||
statusLabel_one = "{{remaining}} शेष"
|
||||
statusLabel_other = "{{remaining}} शेष"
|
||||
statusLabel_one = "{{used}} उपयोग किया गया"
|
||||
statusLabel_other = "{{used}} उपयोग किए गए"
|
||||
sub = "PDF Editor का मुफ़्त उपयोग करें। PDFs को अपने-आप process करने के लिए भुगतान करें।"
|
||||
title_one = "{{allowance}} PDFs मुफ़्त process करें"
|
||||
title_other = "{{allowance}} PDFs मुफ़्त process करें"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "फ़ाइल देखें"
|
||||
viewInProcessor = "प्रोसेसर में देखें"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "यह विफलता किसी विशिष्ट दस्तावेज़ के लिए रिकॉर्ड नहीं की गई थी, इसलिए यहाँ खोलने के लिए कुछ नहीं है।"
|
||||
unattended = "यह फ़ाइल किसी फ़ोल्डर, bucket या webhook द्वारा दी गई थी, इसलिए इसे खोलने के लिए किसी का ब्राउज़र इसे होल्ड नहीं कर रहा है।"
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "स्रोत"
|
||||
connectSource = "स्रोत कनेक्ट करें"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "उन्नत"
|
||||
back = "स्रोतों पर वापस"
|
||||
backToSource = "स्रोत सेटअप पर वापस जाएँ"
|
||||
backToTypes = "सभी स्रोत प्रकार"
|
||||
cancel = "रद्द करें"
|
||||
chooseHint = "चुनें कि दस्तावेज़ कहाँ से आते हैं। greyed-out connectors आने वाले हैं।"
|
||||
@@ -8726,7 +8845,6 @@ create = "स्रोत बनाएँ"
|
||||
createTitle = "स्रोत कनेक्ट करें"
|
||||
delete = "हटाएँ"
|
||||
editTitle = "स्रोत संपादित करें"
|
||||
enabled = "सक्षम"
|
||||
save = "बदलाव सेव करें"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: प्रत्येक file को एक बार proces
|
||||
snapshot = "Snapshot: हर run में folder फिर से पढ़ें"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "यदि आपकी फ़ाइलें नेस्टेड फ़ोल्डरों में व्यवस्थित हैं तो सबफ़ोल्डर शामिल करें, या केवल शीर्ष स्तर देखें।"
|
||||
label = "Folder depth"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Size, date और content check"
|
||||
stat = "Size और date modified"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "प्रोसेस होने के बाद मूल फ़ाइल को उसी स्थान पर छोड़ना है या नहीं। यदि मूल फ़ाइल उसी स्थान पर छोड़ी जाती है, तो अगली बार pipeline स्रोत स्कैन करने पर इसे फिर से प्रोसेस किया जाएगा।"
|
||||
label = "Read mode"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Consume: प्रत्येक फ़ाइल को एक ब
|
||||
snapshot = "Snapshot: हर run में folder को फिर से पढ़ें"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "यदि आपकी फ़ाइलें नेस्टेड फ़ोल्डरों में व्यवस्थित हैं तो सबफ़ोल्डर शामिल करें, या केवल शीर्ष स्तर देखें।"
|
||||
label = "Folder depth"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "आपका Stirling-PDF सर्वर ऑफ़
|
||||
expired = "आपका सत्र समाप्त हो गया है। कृपया पृष्ठ को रिफ्रेश करें और पुन: प्रयास करें।"
|
||||
|
||||
[settings]
|
||||
backToSections = "सभी सेटिंग्स"
|
||||
close = "बंद करें"
|
||||
title = "सेटिंग्स"
|
||||
|
||||
[settings.ai]
|
||||
documents = "दस्तावेज़ और RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "खोजें"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "टूल खोजें..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "उन्नत फ़ॉर्मेटिंग"
|
||||
ai = "AI"
|
||||
automation = "स्वचालन"
|
||||
developerTools = "डेवलपर टूल"
|
||||
documentReview = "दस्तावेज़ समीक्षा"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "स्तंभ {{index}}"
|
||||
convertToPdf = "PDF में बदलें"
|
||||
csvStats = "{{rows}} पंक्तियाँ · {{columns}} स्तंभ · {{size}}"
|
||||
emptyFile = "खाली फ़ाइल"
|
||||
htmlHidePreview = "पूर्वावलोकन छिपाएँ"
|
||||
htmlPreview = "HTML पूर्वावलोकन"
|
||||
htmlPreviewMobileHidden = "HTML पेज डेस्कटॉप चौड़ाई के लिए लेआउट किए गए हैं, इसलिए यहाँ पूर्वावलोकन डिफ़ॉल्ट रूप से बंद है।"
|
||||
htmlPreviewWarning = "HTML पूर्वावलोकन — बाहरी संसाधन लोड नहीं हो सकते · {{size}}"
|
||||
htmlShowPreview = "फिर भी पूर्वावलोकन दिखाएँ"
|
||||
invalidJson = "अवैध JSON — कच्ची सामग्री दिखाई जा रही है"
|
||||
lineNumbers = "पंक्ति संख्याएँ"
|
||||
loading = "लोड हो रहा है..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "PDF निर्यात करें"
|
||||
exportSelected = "चयनित पृष्ठ निर्यात करें"
|
||||
formFill = "फ़ॉर्म भरें"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "अधिक कार्रवाइयाँ"
|
||||
multiTool = "मल्टी-टूल"
|
||||
panMode = "पैन मोड"
|
||||
print = "PDF प्रिंट करें"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "सभी चुनें"
|
||||
selectByNumber = "पृष्ठ संख्याओं से चुनें"
|
||||
selectLanguage = "भाषा चुनें"
|
||||
share = "साझा करें"
|
||||
showAllTools = "सभी टूल दिखाएँ"
|
||||
showFewerTools = "टूलबार संक्षिप्त करें"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "एनोटेशन की दृश्यता टॉगल करें"
|
||||
toggleAttachments = "संलग्नक टॉगल करें"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Netočno"
|
||||
fileNotSavedToDisk = "Nije spremljeno na disk"
|
||||
fileSavedToDisk = "Datoteka spremljena na disk"
|
||||
fileSelected = "Odabrano: {{filename}}"
|
||||
filesSelected = "odabrane datoteke"
|
||||
filesSelected = "{{count}} datoteka"
|
||||
font = "Pismo"
|
||||
fontSizeTooltip = "Veličina teksta broja stranice u točkama. Veći brojevi stvaraju veći tekst."
|
||||
fontTypeTooltip = "Obitelj fonta za brojeve stranica. Odaberite prema stilu dokumenta."
|
||||
@@ -3969,6 +3969,7 @@ back = "Natrag"
|
||||
backToFolder = "Natrag na {{folder}}"
|
||||
backToMyFiles = "Natrag na Moje datoteke"
|
||||
breadcrumbs = "Putanja mape"
|
||||
bulkActions = "Radnje"
|
||||
cancel = "Odustani"
|
||||
classification = "Klasifikacija"
|
||||
clearSelection = "Poništi odabir"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Ukupna veličina"
|
||||
type = "Vrsta"
|
||||
versionHistory = "Povijest verzija"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} aktivnih filtara"
|
||||
clearAll = "Očisti filtre"
|
||||
label = "Filtri"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Odustani"
|
||||
error = "Nije moguće spremiti mapu. Pokušajte ponovno."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Sortiraj datoteke"
|
||||
modifiedAsc = "Najstarije prvo"
|
||||
modifiedDesc = "Najnovije prvo"
|
||||
nameAsc = "Naziv A→Ž"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Sve spremljeno"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Nije bilo moguće primijeniti promjene"
|
||||
extractCsvError = "Izdvajanje CSV-a nije uspjelo"
|
||||
extractXlsxError = "Izdvajanje XLSX-a nije uspjelo"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Izravnaj nakon ispunjavanja"
|
||||
goToPage = "Idi na ovu stranicu"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Stranica"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "obv"
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Ponovno skeniraj polja"
|
||||
rescanFormFields = "Ponovno skeniraj polja obrasca"
|
||||
save = "Spremi"
|
||||
saveShortcut = "Ctrl+S za spremanje"
|
||||
skippedEdits_one = "1 promjenu nije bilo moguće primijeniti:"
|
||||
skippedEdits_other = "{{count}} promjena nije bilo moguće primijeniti:"
|
||||
skippedEditsTruncated = "Još {{count}} nije navedeno."
|
||||
unsavedChanges = "Nespremljene promjene"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Dodaj {{count}} polje(a) u PDF"
|
||||
empty = "Još nema nacrtanih polja."
|
||||
failed = "Dodavanje polja nije uspjelo"
|
||||
goToField = "Idi na ovo polje"
|
||||
hint = "Odaberite vrstu polja, zatim ga nacrtajte na stranici."
|
||||
placing = "Nacrtajte polje {{type}} na stranici. Pritisnite Esc za zaustavljanje."
|
||||
preview = "Držite za pretpregled"
|
||||
previewHelp = "Držite za prikaz polja kako će izgledati nakon dodavanja, bez obrisa za uređivanje."
|
||||
removeField = "Ukloni polje"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Radnja gumba"
|
||||
actionHelp = "Što gumb radi kada se klikne."
|
||||
actionNone = "Ništa"
|
||||
actionPrint = "Ispis"
|
||||
actionReset = "Resetiraj obrazac"
|
||||
actionSubmit = "Pošalji na URL"
|
||||
actionUri = "Otvori URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "Adresa koju gumb otvara ili na koju šalje."
|
||||
addOption = "Dodaj opciju"
|
||||
caption = "Natpis gumba"
|
||||
captionHelp = "Tekst ispisan na površini gumba."
|
||||
defaultValue = "Zadana vrijednost"
|
||||
defaultValueHelp = "Što polje sadrži prije nego što ga netko ispuni. Ostavite prazno za prazno polje."
|
||||
fontSize = "Veličina fonta"
|
||||
fontSizeHelp = "Veličina teksta unutar polja. Ostavite prazno kako bi ga čitač prilagodio da stane."
|
||||
label = "Oznaka"
|
||||
labelHelp = "Tekst prikazan osobi koja ispunjava obrazac. Ostavite prazno za korištenje naziva polja."
|
||||
maxLength = "Maks. duljina (comb)"
|
||||
maxLengthHelp = "Ograničava koliko znakova stane, prikazano kao ravnomjerno razmaknuti okviri."
|
||||
multiline = "Više redaka"
|
||||
multilineHelp = "Omogućuje više od jednog retka teksta i prelamanje na rubu polja."
|
||||
multiSelect = "Dopusti višestruki odabir"
|
||||
multiSelectHelp = "Omogućuje odabir više od jedne opcije odjednom."
|
||||
name = "Naziv polja"
|
||||
nameHelp = "Interni naziv polja. Koristi se pri izvozu podataka ili ispunjavanju obrasca iz drugog sustava, stoga neka bude jedinstven i bez razmaka."
|
||||
optionGap = "Razmak opcija"
|
||||
optionGapHelp = "Razmak između gumba, u točkama. Ostavite prazno kako bi se ravnomjerno rasporedili niz okvir."
|
||||
optionPlaceholder = "Opcija {{n}}"
|
||||
options = "Opcije"
|
||||
optionsEmpty = "Dodajte barem jednu opciju."
|
||||
optionsHelp = "Izbori ponuđeni na popisu. Svaki se pohranjuje kako je unesen, stoga neka budu kratki i različiti."
|
||||
optionSize = "Veličina opcije"
|
||||
optionSizeHelp = "Širina i visina svakog gumba, u točkama. Ostavite prazno kako bi se prilagodili okviru koji ste nacrtali."
|
||||
readOnly = "Samo za čitanje"
|
||||
readOnlyHelp = "Prikazuje vrijednost, ali sprječava uređivanje."
|
||||
removeOption = "Ukloni opciju"
|
||||
required = "Obavezno"
|
||||
requiredHelp = "Obrazac se ne može poslati dok se ovo polje ne ispuni."
|
||||
signatureNote = "Samo rezervirano mjesto - ovdje se ne potpisujete. Označava gdje potpis pripada kako bi PDF potpisnik (Adobe Acrobat, usluga potpisivanja itd.) postavio potpis na ovo mjesto kada se dokument potpiše."
|
||||
tooltip = "Opis alata"
|
||||
tooltipHelp = "Savjet prikazan kada netko zadrži pokazivač iznad polja u PDF čitaču."
|
||||
type = "Vrsta"
|
||||
typeHelp = "Koja je ovo vrsta polja. Promjenom se polje ponovno izrađuje, pa se njegova trenutna vrijednost ne prenosi."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Izradi"
|
||||
fill = "Ispuni"
|
||||
label = "Način uređivača obrazaca"
|
||||
modify = "Izmijeni"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Spremi {{count}} promjenu(a)"
|
||||
delete = "Izbriši"
|
||||
empty = "Ovaj PDF još nema polja obrasca."
|
||||
failed = "Spremanje promjena nije uspjelo"
|
||||
groupSizeHint = "Koristi veličinu opcije"
|
||||
hint = "Odaberite polje za uređivanje njegovih svojstava, povucite ga na stranici ili ga izbrišite."
|
||||
restore = "Vrati"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Zatvori bočnu traku"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Promijenite ograničenja dokumenta i dopuštenja"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Promjena dopuštenja"
|
||||
|
||||
[home.classify]
|
||||
desc = "Prepoznajte koja je vrsta dokumenta i označite ga."
|
||||
title = "Klasificiraj"
|
||||
|
||||
[home.compare]
|
||||
desc = "Uspoređuje i pokazuje razlike između 2 PDF dokumenta"
|
||||
tags = "razlika"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Nema ništa za prijaviti."
|
||||
handoffUnavailable = "Ovaj preglednik neće dopustiti obrađivaču da proslijedi dokument uređivaču. Umjesto toga otvorite ga iz uređivača."
|
||||
noDocumentLinked = "Ovaj neuspjeh nije povezan s određenim dokumentom, pa se ovdje nema što otvoriti."
|
||||
notOnThisDevice = "Ovaj dokument nije na ovom uređaju, pa ga ovdje nije moguće otvoriti."
|
||||
occurrences = "{{count}} puta"
|
||||
open = "Obavijesti"
|
||||
title = "Obavijesti"
|
||||
unread = "Nepročitano"
|
||||
|
||||
[notifications.action]
|
||||
failed = "To nije uspjelo. Pokušajte ponovno za trenutak."
|
||||
unavailable = "Nije dostupno za ovu obavijest."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Kopirano"
|
||||
copy = "Kopiraj pogrešku"
|
||||
less = "Prikaži manje"
|
||||
more = "Prikaži cijelu poruku"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Ranije"
|
||||
new = "Novo"
|
||||
|
||||
[oauth.error]
|
||||
message = "Autentikacija nije bila uspješna. Možete zatvoriti ovaj prozor i pokušati ponovno."
|
||||
title = "Autentikacija nije uspjela"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "od {{allowance}} besplatnih PDF-ova iskorišteno"
|
||||
capSuffix_other = "od {{allowance}} besplatnih PDF-ova iskorišteno"
|
||||
eyebrow = "Probno razdoblje za Processor"
|
||||
statusLabel_one = "preostalo {{remaining}}"
|
||||
statusLabel_other = "preostalo {{remaining}}"
|
||||
statusLabel_one = "{{used}} iskorišteno"
|
||||
statusLabel_other = "{{used}} iskorišteno"
|
||||
sub = "Koristite PDF uređivač besplatno. Platite za automatsku obradu PDF-ova."
|
||||
title_one = "Obradite {{allowance}} PDF-ova besplatno"
|
||||
title_other = "Obradite {{allowance}} PDF-ova besplatno"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Prikaži datoteku"
|
||||
viewInProcessor = "Prikaži u obrađivaču"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Ovaj neuspjeh nije zabilježen za određeni dokument, pa se ovdje nema što otvoriti."
|
||||
unattended = "Ova je datoteka unesena putem mape, bucketa ili webhooka, pa je ničiji preglednik ne drži za otvaranje."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Izvori"
|
||||
connectSource = "Poveži izvor"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Napredno"
|
||||
back = "Natrag na izvore"
|
||||
backToSource = "Natrag na postavljanje izvora"
|
||||
backToTypes = "Sve vrste izvora"
|
||||
cancel = "Odustani"
|
||||
chooseHint = "Odaberite odakle dokumenti dolaze. Zasivljeni konektori su u pripremi."
|
||||
@@ -8726,7 +8845,6 @@ create = "Izradi izvor"
|
||||
createTitle = "Poveži izvor"
|
||||
delete = "Izbriši"
|
||||
editTitle = "Uredi izvor"
|
||||
enabled = "Omogućeno"
|
||||
save = "Spremi izmjene"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: obradi svaku datoteku jednom"
|
||||
snapshot = "Snapshot: ponovno pročitaj mapu pri svakom pokretanju"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Uključite podmape ako su vaše datoteke organizirane u ugniježđene mape ili pratite samo najvišu razinu."
|
||||
label = "Dubina mape"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Veličina, datum i provjera sadržaja"
|
||||
stat = "Veličina i datum izmjene"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Hoće li se izvorna datoteka ostaviti na mjestu nakon obrade. Ako izvorna datoteka ostane na mjestu, ponovno će se obraditi sljedeći put kada tijek obrade skenira izvor."
|
||||
label = "Način čitanja"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Konzumiraj: obradi svaku datoteku jednom"
|
||||
snapshot = "Snimka: ponovno pročitaj mapu pri svakom pokretanju"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Uključite podmape ako su vaše datoteke organizirane u ugniježđene mape ili pratite samo najvišu razinu."
|
||||
label = "Dubina mape"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Vaš Stirling-PDF poslužitelj je izvan mreže i \"{{
|
||||
expired = "Vaš sesija je istekla. Molim vas da osvježite stranicu i pokušate ponovno."
|
||||
|
||||
[settings]
|
||||
backToSections = "Sve postavke"
|
||||
close = "Zatvori"
|
||||
title = "Postavke"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Dokumenti i RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Pretraži"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Pretraži alate..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Napredno formatiranje"
|
||||
ai = "AI"
|
||||
automation = "Automatizacija"
|
||||
developerTools = "Alati za razvojne programere"
|
||||
documentReview = "Pregled dokumenta"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Stupac {{index}}"
|
||||
convertToPdf = "Pretvori u PDF"
|
||||
csvStats = "{{rows}} redaka · {{columns}} stupaca · {{size}}"
|
||||
emptyFile = "Prazna datoteka"
|
||||
htmlHidePreview = "Sakrij pretpregled"
|
||||
htmlPreview = "HTML pregled"
|
||||
htmlPreviewMobileHidden = "HTML stranice raspoređene su za širine stolnih računala, pa je pretpregled ovdje prema zadanim postavkama isključen."
|
||||
htmlPreviewWarning = "HTML pregled — vanjski resursi se možda neće učitati · {{size}}"
|
||||
htmlShowPreview = "Svejedno prikaži pretpregled"
|
||||
invalidJson = "Nevažeći JSON — prikaz neobrađenog sadržaja"
|
||||
lineNumbers = "Brojevi redaka"
|
||||
loading = "Učitavanje..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Izvezi PDF"
|
||||
exportSelected = "Izvezi odabrane stranice"
|
||||
formFill = "Ispuni obrazac"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Više radnji"
|
||||
multiTool = "Višenamjenski alat"
|
||||
panMode = "Način pomicanja"
|
||||
print = "Ispis PDF-a"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Odaberi sve"
|
||||
selectByNumber = "Odaberi po brojevima stranica"
|
||||
selectLanguage = "Odaberi jezik"
|
||||
share = "Dijeli"
|
||||
showAllTools = "Prikaži sve alate"
|
||||
showFewerTools = "Sažmi alatnu traku"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Uključi/isključi vidljivost anotacija"
|
||||
toggleAttachments = "Uključi/isključi privitke"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Nem"
|
||||
fileNotSavedToDisk = "Nincs lemezre mentve"
|
||||
fileSavedToDisk = "Fájl mentve a lemezre"
|
||||
fileSelected = "Kiválasztva: {{filename}}"
|
||||
filesSelected = "fájl kiválasztva"
|
||||
filesSelected = "{{count}} fájl"
|
||||
font = "Betűtípus"
|
||||
fontSizeTooltip = "A lapszám szövegének mérete pontban. A nagyobb szám nagyobb szöveget eredményez."
|
||||
fontTypeTooltip = "A lapszámok betűcsaládja. Válassza a dokumentum stílusához illően."
|
||||
@@ -3969,6 +3969,7 @@ back = "Vissza"
|
||||
backToFolder = "Vissza ide: {{folder}}"
|
||||
backToMyFiles = "Vissza a Saját fájlokhoz"
|
||||
breadcrumbs = "Mappaútvonal"
|
||||
bulkActions = "Műveletek"
|
||||
cancel = "Mégse"
|
||||
classification = "Besorolás"
|
||||
clearSelection = "Kijelölés törlése"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Teljes méret"
|
||||
type = "Típus"
|
||||
versionHistory = "Verziótörténet"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} aktív szűrő"
|
||||
clearAll = "Szűrők törlése"
|
||||
label = "Szűrők"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Mégse"
|
||||
error = "A mappát nem sikerült menteni. Próbálja újra."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Fájlok rendezése"
|
||||
modifiedAsc = "Legrégebbi elöl"
|
||||
modifiedDesc = "Legújabb elöl"
|
||||
nameAsc = "Név A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Minden mentve"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Nem sikerült alkalmazni a módosításokat"
|
||||
extractCsvError = "CSV kinyerése sikertelen"
|
||||
extractXlsxError = "XLSX kinyerése sikertelen"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Kitöltés után lapítás"
|
||||
goToPage = "Ugrás erre az oldalra"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Oldal"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "kötelező"
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Mezők újraolvasása"
|
||||
rescanFormFields = "Űrlapmezők újraolvasása"
|
||||
save = "Mentés"
|
||||
saveShortcut = "Ctrl+S a mentéshez"
|
||||
skippedEdits_one = "1 módosítást nem sikerült alkalmazni:"
|
||||
skippedEdits_other = "{{count}} módosítást nem sikerült alkalmazni:"
|
||||
skippedEditsTruncated = "További {{count}} nincs felsorolva."
|
||||
unsavedChanges = "Nem mentett módosítások"
|
||||
|
||||
[formFill.create]
|
||||
commit = "{{count}} mező hozzáadása a PDF-hez"
|
||||
empty = "Még nincs megrajzolt mező."
|
||||
failed = "Nem sikerült hozzáadni a mezőket"
|
||||
goToField = "Ugrás erre a mezőre"
|
||||
hint = "Válasszon mezőtípust, majd rajzolja meg az oldalon."
|
||||
placing = "Rajzoljon egy {{type}} mezőt az oldalon. A leállításhoz nyomja meg az Esc billentyűt."
|
||||
preview = "Tartsa lenyomva az előnézethez"
|
||||
previewHelp = "Tartsa lenyomva, hogy szerkesztési körvonalak nélkül lássa, hogyan fognak kinézni a mezők a hozzáadás után."
|
||||
removeField = "Mező eltávolítása"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Gombművelet"
|
||||
actionHelp = "Mit tegyen a gomb kattintáskor."
|
||||
actionNone = "Nincs"
|
||||
actionPrint = "Nyomtatás"
|
||||
actionReset = "Űrlap visszaállítása"
|
||||
actionSubmit = "Küldés URL-re"
|
||||
actionUri = "URL megnyitása"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "Az a cím, amelyet a gomb megnyit, vagy amelyre elküld."
|
||||
addOption = "Opció hozzáadása"
|
||||
caption = "Gombfelirat"
|
||||
captionHelp = "A gomb felületén megjelenő szöveg."
|
||||
defaultValue = "Alapértelmezett érték"
|
||||
defaultValueHelp = "Amit a mező tartalmaz, mielőtt bárki kitölti. Üres mezőhöz hagyja üresen."
|
||||
fontSize = "Betűméret"
|
||||
fontSizeHelp = "A mezőn belüli szöveg mérete. Hagyja üresen, hogy az olvasóprogram igazítsa a méretet."
|
||||
label = "Címke"
|
||||
labelHelp = "Az űrlapot kitöltő számára megjelenő szöveg. Hagyja üresen, hogy a mező neve legyen használva."
|
||||
maxLength = "Maximális hossz (comb)"
|
||||
maxLengthHelp = "Korlátozza a beírható karakterek számát, egyenletesen elosztott mezőkként megjelenítve."
|
||||
multiline = "Többsoros"
|
||||
multilineHelp = "Több sornyi szöveget engedélyez, és a mező szélénél sort tör."
|
||||
multiSelect = "Többszörös kiválasztás engedélyezése"
|
||||
multiSelectHelp = "Egyszerre több opció kiválasztását teszi lehetővé."
|
||||
name = "Mező neve"
|
||||
nameHelp = "A mező belső neve. Adatok exportálásakor vagy az űrlap másik rendszerből történő kitöltésekor használatos, ezért legyen egyedi és szóközmentes."
|
||||
optionGap = "Opciók térköze"
|
||||
optionGapHelp = "A gombok közötti távolság pontban. Hagyja üresen, hogy egyenletesen legyenek elosztva a dobozban."
|
||||
optionPlaceholder = "{{n}}. opció"
|
||||
options = "Opciók"
|
||||
optionsEmpty = "Adjon hozzá legalább egy opciót."
|
||||
optionsHelp = "A listában felkínált választási lehetőségek. Mindegyik a beírt formában tárolódik, ezért legyenek rövidek és jól megkülönböztethetők."
|
||||
optionSize = "Opció mérete"
|
||||
optionSizeHelp = "Az egyes gombok szélessége és magassága pontban. Hagyja üresen, hogy illeszkedjenek a megrajzolt dobozhoz."
|
||||
readOnly = "Csak olvasható"
|
||||
readOnlyHelp = "Megjelenít egy értéket, de megakadályozza a szerkesztését."
|
||||
removeOption = "Opció eltávolítása"
|
||||
required = "Kötelező"
|
||||
requiredHelp = "Az űrlap nem küldhető el, amíg ez a mező nincs kitöltve."
|
||||
signatureNote = "Csak helykitöltő - itt nem ír alá. Azt jelöli, hová tartozik az aláírás, hogy egy PDF aláíró (Adobe Acrobat, aláírási szolgáltatás stb.) ide helyezze az aláírást a dokumentum aláírásakor."
|
||||
tooltip = "Tooltip"
|
||||
tooltipHelp = "A PDF olvasóban akkor megjelenő tipp, amikor valaki a mező fölé viszi az egérmutatót."
|
||||
type = "Típus"
|
||||
typeHelp = "Milyen típusú ez a mező. A módosítása újraépíti a mezőt, ezért az aktuális értéke nem kerül átvitelre."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Létrehozás"
|
||||
fill = "Kitöltés"
|
||||
label = "Űrlapszerkesztő mód"
|
||||
modify = "Módosítás"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "{{count}} módosítás mentése"
|
||||
delete = "Törlés"
|
||||
empty = "Ebben a PDF-ben még nincsenek űrlapmezők."
|
||||
failed = "Nem sikerült menteni a módosításokat"
|
||||
groupSizeHint = "Opció méretének használata"
|
||||
hint = "Válasszon ki egy mezőt a tulajdonságainak szerkesztéséhez, húzza az oldalon, vagy törölje."
|
||||
restore = "Visszaállítás"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Oldalsáv bezárása"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Dokumentumkorlátozások és jogosultságok módosítása"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Jogosultságok módosítása"
|
||||
|
||||
[home.classify]
|
||||
desc = "Azonosítsa, milyen típusú dokumentum ez, és címkézze fel."
|
||||
title = "Osztályozás"
|
||||
|
||||
[home.compare]
|
||||
desc = "Két PDF dokumentum összehasonlítása és különbségek megjelenítése"
|
||||
tags = "különbség"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Nincs jelenteni való."
|
||||
handoffUnavailable = "Ez a böngésző nem engedi, hogy a feldolgozó átadja a dokumentumot a szerkesztőnek. Nyissa meg inkább a szerkesztőből."
|
||||
noDocumentLinked = "Ez a hiba nincs konkrét dokumentumhoz kapcsolva, ezért itt nincs mit megnyitni."
|
||||
notOnThisDevice = "Ez a dokumentum nincs ezen az eszközön, ezért itt nem nyitható meg."
|
||||
occurrences = "{{count}} alkalommal"
|
||||
open = "Értesítések"
|
||||
title = "Értesítések"
|
||||
unread = "Olvasatlan"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Ez nem sikerült. Próbálja újra egy pillanat múlva."
|
||||
unavailable = "Ehhez az értesítéshez nem érhető el."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Másolva"
|
||||
copy = "Hiba másolása"
|
||||
less = "Kevesebb megjelenítése"
|
||||
more = "Teljes üzenet megjelenítése"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Korábbi"
|
||||
new = "Új"
|
||||
|
||||
[oauth.error]
|
||||
message = "A hitelesítés nem volt sikeres. Bezárhatja ezt az ablakot, és megpróbálhatja újra."
|
||||
title = "Sikertelen hitelesítés"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "felhasználva a(z) {{allowance}} ingyenes PDF-ből"
|
||||
capSuffix_other = "felhasználva a(z) {{allowance}} ingyenes PDF-ből"
|
||||
eyebrow = "Processor próbaidőszak"
|
||||
statusLabel_one = "{{remaining}} maradt"
|
||||
statusLabel_other = "{{remaining}} maradt"
|
||||
statusLabel_one = "{{used}} felhasználva"
|
||||
statusLabel_other = "{{used}} felhasználva"
|
||||
sub = "Használja a PDF Editor-t ingyen. Fizessen a PDF-ek automatikus feldolgozásáért."
|
||||
title_one = "Dolgozzon fel {{allowance}} PDF-et ingyen"
|
||||
title_other = "Dolgozzon fel {{allowance}} PDF-et ingyen"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Fájl megtekintése"
|
||||
viewInProcessor = "Megtekintés a feldolgozóban"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Ez a hiba nem konkrét dokumentumhoz lett rögzítve, ezért itt nincs mit megnyitni."
|
||||
unattended = "Ezt a fájlt egy mappa, bucket vagy webhook adta át, ezért senkinek a böngészője nem tartja megnyitásra készen."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Források"
|
||||
connectSource = "Forrás csatlakoztatása"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Speciális"
|
||||
back = "Vissza a forrásokhoz"
|
||||
backToSource = "Vissza a forrás beállításához"
|
||||
backToTypes = "Minden forrástípus"
|
||||
cancel = "Mégse"
|
||||
chooseHint = "Válassza ki, honnan érkeznek a dokumentumok. A kiszürkített csatlakozók úton vannak."
|
||||
@@ -8726,7 +8845,6 @@ create = "Forrás létrehozása"
|
||||
createTitle = "Forrás csatlakoztatása"
|
||||
delete = "Törlés"
|
||||
editTitle = "Forrás szerkesztése"
|
||||
enabled = "Engedélyezve"
|
||||
save = "Módosítások mentése"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: minden fájl feldolgozása egyszer"
|
||||
snapshot = "Snapshot: mappa újraolvasása minden futtatáskor"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Tartalmazza az almappákat, ha a fájljai beágyazott mappákba vannak rendezve, vagy csak a legfelső szintet figyelje."
|
||||
label = "Mappamélység"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Méret, dátum és tartalomellenőrzés"
|
||||
stat = "Méret és módosítás dátuma"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Meghagyja-e az eredeti fájlt a helyén a feldolgozás után. Ha az eredeti fájl a helyén marad, a pipeline következő forrásellenőrzésekor újra feldolgozásra kerül."
|
||||
label = "Olvasási mód"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Elfogyasztás: minden fájl feldolgozása egyszer"
|
||||
snapshot = "Pillanatkép: a mappa újraolvasása minden futtatáskor"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Tartalmazza az almappákat, ha a fájljai beágyazott mappákba vannak rendezve, vagy csak a legfelső szintet figyelje."
|
||||
label = "Mappamélység"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "A Stirling-PDF szervere offline, és a(z) \"{{endpoin
|
||||
expired = "A munkamenet lejárt. Kérjük, frissítse az oldalt és próbálja újra."
|
||||
|
||||
[settings]
|
||||
backToSections = "Összes beállítás"
|
||||
close = "Bezárás"
|
||||
title = "Beállítások"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Dokumentumok és RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Keresés"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Eszközök keresése..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Speciális formázás"
|
||||
ai = "AI"
|
||||
automation = "Automatizálás"
|
||||
developerTools = "Fejlesztői eszközök"
|
||||
documentReview = "Dokumentum-áttekintés"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "{{index}}. oszlop"
|
||||
convertToPdf = "Konvertálás PDF-be"
|
||||
csvStats = "{{rows}} sor · {{columns}} oszlop · {{size}}"
|
||||
emptyFile = "Üres fájl"
|
||||
htmlHidePreview = "Előnézet elrejtése"
|
||||
htmlPreview = "HTML előnézet"
|
||||
htmlPreviewMobileHidden = "A HTML oldalak asztali szélességre vannak elrendezve, ezért itt az előnézet alapértelmezés szerint ki van kapcsolva."
|
||||
htmlPreviewWarning = "HTML előnézet — a külső erőforrások nem biztos, hogy betöltődnek · {{size}}"
|
||||
htmlShowPreview = "Előnézet megjelenítése mégis"
|
||||
invalidJson = "Érvénytelen JSON — nyers tartalom megjelenítése"
|
||||
lineNumbers = "Sorszámok"
|
||||
loading = "Betöltés..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "PDF exportálása"
|
||||
exportSelected = "Kijelölt oldalak exportálása"
|
||||
formFill = "Űrlap kitöltése"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "További műveletek"
|
||||
multiTool = "Többfunkciós eszköz"
|
||||
panMode = "Mozgatás mód"
|
||||
print = "PDF nyomtatása"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Összes kijelölése"
|
||||
selectByNumber = "Kiválasztás oldalszámok szerint"
|
||||
selectLanguage = "Nyelv kiválasztása"
|
||||
share = "Megosztás"
|
||||
showAllTools = "Összes eszköz megjelenítése"
|
||||
showFewerTools = "Eszköztár összecsukása"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Annotációk láthatóságának be-/kikapcsolása"
|
||||
toggleAttachments = "Mellékletek be-/kikapcsolása"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Salah"
|
||||
fileNotSavedToDisk = "Tidak disimpan ke disk"
|
||||
fileSavedToDisk = "File disimpan ke disk"
|
||||
fileSelected = "Dipilih: {{filename}}"
|
||||
filesSelected = "berkas dipilih"
|
||||
filesSelected = "{{count}} berkas"
|
||||
font = "Jenis huruf"
|
||||
fontSizeTooltip = "Ukuran teks nomor halaman dalam poin. Angka yang lebih besar membuat teks lebih besar."
|
||||
fontTypeTooltip = "Keluarga font untuk nomor halaman. Pilih sesuai gaya dokumen Anda."
|
||||
@@ -3969,6 +3969,7 @@ back = "Kembali"
|
||||
backToFolder = "Kembali ke {{folder}}"
|
||||
backToMyFiles = "Kembali ke File Saya"
|
||||
breadcrumbs = "Jalur folder"
|
||||
bulkActions = "Tindakan"
|
||||
cancel = "Batal"
|
||||
classification = "Klasifikasi"
|
||||
clearSelection = "Hapus pilihan"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Total ukuran"
|
||||
type = "Jenis"
|
||||
versionHistory = "Perjalanan versi"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} filter aktif"
|
||||
clearAll = "Hapus filter"
|
||||
label = "Filter"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Batal"
|
||||
error = "Tidak dapat menyimpan folder. Coba lagi."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Urutkan file"
|
||||
modifiedAsc = "Terlama dulu"
|
||||
modifiedDesc = "Terbaru dulu"
|
||||
nameAsc = "Nama A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Semua tersimpan"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Tidak dapat menerapkan perubahan"
|
||||
extractCsvError = "Gagal mengekstrak CSV"
|
||||
extractXlsxError = "Gagal mengekstrak XLSX"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Ratakan setelah mengisi"
|
||||
goToPage = "Buka halaman ini"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Halaman"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "wajib"
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Pindai ulang bidang"
|
||||
rescanFormFields = "Pindai ulang bidang formulir"
|
||||
save = "Simpan"
|
||||
saveShortcut = "Ctrl+S untuk menyimpan"
|
||||
skippedEdits_one = "1 perubahan tidak dapat diterapkan:"
|
||||
skippedEdits_other = "{{count}} perubahan tidak dapat diterapkan:"
|
||||
skippedEditsTruncated = "{{count}} lainnya tidak tercantum."
|
||||
unsavedChanges = "Perubahan belum disimpan"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Tambahkan {{count}} bidang ke PDF"
|
||||
empty = "Belum ada bidang yang digambar."
|
||||
failed = "Gagal menambahkan bidang"
|
||||
goToField = "Buka bidang ini"
|
||||
hint = "Pilih jenis bidang, lalu gambar di halaman."
|
||||
placing = "Gambar bidang {{type}} di halaman. Tekan Esc untuk berhenti."
|
||||
preview = "Tahan untuk pratinjau"
|
||||
previewHelp = "Tahan untuk melihat bidang sebagaimana tampilannya setelah ditambahkan, tanpa garis tepi pengeditan."
|
||||
removeField = "Hapus bidang"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Tindakan tombol"
|
||||
actionHelp = "Apa yang dilakukan tombol saat diklik."
|
||||
actionNone = "Tidak ada"
|
||||
actionPrint = "Cetak"
|
||||
actionReset = "Reset formulir"
|
||||
actionSubmit = "Kirim ke URL"
|
||||
actionUri = "Buka URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "Alamat yang dibuka tombol atau menjadi tujuan pengiriman."
|
||||
addOption = "Tambahkan opsi"
|
||||
caption = "Keterangan tombol"
|
||||
captionHelp = "Teks yang dicetak pada permukaan tombol."
|
||||
defaultValue = "Nilai default"
|
||||
defaultValueHelp = "Isi bidang sebelum diisi oleh siapa pun. Biarkan kosong untuk bidang kosong."
|
||||
fontSize = "Ukuran font"
|
||||
fontSizeHelp = "Ukuran teks di dalam bidang. Biarkan kosong agar pembaca menyesuaikannya supaya pas."
|
||||
label = "Label"
|
||||
labelHelp = "Teks yang ditampilkan kepada orang yang mengisi formulir. Biarkan kosong untuk menggunakan nama bidang."
|
||||
maxLength = "Panjang maks (comb)"
|
||||
maxLengthHelp = "Membatasi jumlah karakter yang muat, digambar sebagai kotak-kotak dengan jarak merata."
|
||||
multiline = "Multi-baris"
|
||||
multilineHelp = "Memungkinkan lebih dari satu baris teks dan membungkus di tepi bidang."
|
||||
multiSelect = "Izinkan beberapa pilihan"
|
||||
multiSelectHelp = "Memungkinkan lebih dari satu opsi dipilih sekaligus."
|
||||
name = "Nama bidang"
|
||||
nameHelp = "Nama internal bidang. Digunakan saat mengekspor data atau mengisi formulir dari sistem lain, jadi buat tetap unik dan tanpa spasi."
|
||||
optionGap = "Jarak opsi"
|
||||
optionGapHelp = "Jarak antar tombol, dalam poin. Biarkan kosong untuk menyebarkannya secara merata ke bawah kotak."
|
||||
optionPlaceholder = "Opsi {{n}}"
|
||||
options = "Opsi"
|
||||
optionsEmpty = "Tambahkan setidaknya satu opsi."
|
||||
optionsHelp = "Pilihan yang ditawarkan dalam daftar. Setiap pilihan disimpan sesuai yang diketik, jadi buat singkat dan berbeda."
|
||||
optionSize = "Ukuran opsi"
|
||||
optionSizeHelp = "Lebar dan tinggi setiap tombol, dalam poin. Biarkan kosong agar disesuaikan dengan kotak yang Anda gambar."
|
||||
readOnly = "Hanya baca"
|
||||
readOnlyHelp = "Menampilkan nilai tetapi mencegah siapa pun mengeditnya."
|
||||
removeOption = "Hapus opsi"
|
||||
required = "Wajib"
|
||||
requiredHelp = "Formulir tidak dapat dikirim sampai bidang ini diisi."
|
||||
signatureNote = "Hanya placeholder - Anda tidak menandatangani di sini. Ini menandai tempat tanda tangan berada sehingga penanda tangan PDF (Adobe Acrobat, layanan penandatanganan, dll.) menempatkan tanda tangan di lokasi ini saat dokumen ditandatangani."
|
||||
tooltip = "Tooltip"
|
||||
tooltipHelp = "Petunjuk yang ditampilkan saat seseorang mengarahkan kursor ke bidang di pembaca PDF."
|
||||
type = "Jenis"
|
||||
typeHelp = "Jenis bidang ini. Mengubahnya akan membangun ulang bidang, sehingga nilai saat ini tidak ikut dibawa."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Buat"
|
||||
fill = "Isi"
|
||||
label = "Mode editor formulir"
|
||||
modify = "Ubah"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Simpan {{count}} perubahan"
|
||||
delete = "Hapus"
|
||||
empty = "PDF ini belum memiliki bidang formulir."
|
||||
failed = "Gagal menyimpan perubahan"
|
||||
groupSizeHint = "Gunakan Ukuran opsi"
|
||||
hint = "Pilih bidang untuk mengedit propertinya, seret di halaman, atau hapus."
|
||||
restore = "Pulihkan"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Tutup bilah sisi"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Ubah pembatasan dan izin dokumen"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Ganti Perizinan"
|
||||
|
||||
[home.classify]
|
||||
desc = "Identifikasi jenis dokumen ini dan beri tag."
|
||||
title = "Klasifikasikan"
|
||||
|
||||
[home.compare]
|
||||
desc = "Membandingkan dan menunjukkan perbedaan antara 2 Dokumen PDF"
|
||||
tags = "perbedaan"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Tidak ada yang perlu dilaporkan."
|
||||
handoffUnavailable = "Browser ini tidak mengizinkan pemroses meneruskan dokumen ke editor. Buka dari editor saja."
|
||||
noDocumentLinked = "Kegagalan ini tidak terkait dengan dokumen tertentu, jadi tidak ada yang dapat dibuka di sini."
|
||||
notOnThisDevice = "Dokumen ini tidak ada di perangkat ini, sehingga tidak dapat dibuka di sini."
|
||||
occurrences = "{{count}} kali"
|
||||
open = "Notifikasi"
|
||||
title = "Notifikasi"
|
||||
unread = "Belum dibaca"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Itu tidak berhasil. Coba lagi sebentar lagi."
|
||||
unavailable = "Tidak tersedia untuk notifikasi ini."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Disalin"
|
||||
copy = "Salin kesalahan"
|
||||
less = "Tampilkan lebih sedikit"
|
||||
more = "Tampilkan pesan lengkap"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Sebelumnya"
|
||||
new = "Baru"
|
||||
|
||||
[oauth.error]
|
||||
message = "Autentikasi tidak berhasil. Anda dapat menutup jendela ini dan mencoba lagi."
|
||||
title = "Autentikasi Gagal"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "dari {{allowance}} PDF gratis digunakan"
|
||||
capSuffix_other = "dari {{allowance}} PDF gratis digunakan"
|
||||
eyebrow = "Uji coba Processor"
|
||||
statusLabel_one = "{{remaining}} tersisa"
|
||||
statusLabel_other = "{{remaining}} tersisa"
|
||||
statusLabel_one = "{{used}} terpakai"
|
||||
statusLabel_other = "{{used}} terpakai"
|
||||
sub = "Gunakan PDF Editor secara gratis. Bayar untuk memproses PDF secara otomatis."
|
||||
title_one = "Proses {{allowance}} PDF gratis"
|
||||
title_other = "Proses {{allowance}} PDF gratis"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Lihat file"
|
||||
viewInProcessor = "Lihat di pemroses"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Kegagalan ini tidak dicatat pada dokumen tertentu, jadi tidak ada yang dapat dibuka di sini."
|
||||
unattended = "File ini diberikan oleh folder, bucket, atau webhook, jadi tidak ada browser siapa pun yang menyimpannya untuk dibuka."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Sumber"
|
||||
connectSource = "Hubungkan sumber"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Lanjutan"
|
||||
back = "Kembali ke sumber"
|
||||
backToSource = "Kembali ke penyiapan sumber"
|
||||
backToTypes = "Semua jenis sumber"
|
||||
cancel = "Batal"
|
||||
chooseHint = "Pilih dari mana dokumen berasal. Konektor yang diabu-abukan sedang disiapkan."
|
||||
@@ -8726,7 +8845,6 @@ create = "Buat sumber"
|
||||
createTitle = "Hubungkan sumber"
|
||||
delete = "Hapus"
|
||||
editTitle = "Edit sumber"
|
||||
enabled = "Diaktifkan"
|
||||
save = "Simpan perubahan"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: proses setiap file sekali"
|
||||
snapshot = "Snapshot: baca ulang folder setiap proses"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Sertakan subfolder jika file Anda diatur ke dalam folder bertingkat, atau pantau hanya tingkat teratas."
|
||||
label = "Kedalaman folder"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Pemeriksaan ukuran, tanggal, dan konten"
|
||||
stat = "Ukuran dan tanggal diubah"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Apakah file asli tetap dibiarkan di tempatnya setelah diproses. Jika file asli dibiarkan di tempatnya, file tersebut akan diproses ulang saat pipeline memindai sumber berikutnya."
|
||||
label = "Mode baca"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Konsumsi: proses setiap file sekali"
|
||||
snapshot = "Snapshot: baca ulang folder setiap eksekusi"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Sertakan subfolder jika file Anda diatur ke dalam folder bertingkat, atau pantau hanya tingkat teratas."
|
||||
label = "Kedalaman folder"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Server Stirling-PDF Anda sedang offline dan \"{{endpo
|
||||
expired = "Sesi Anda telah kedaluwarsa. Silakan muat ulang halaman dan coba lagi."
|
||||
|
||||
[settings]
|
||||
backToSections = "Semua pengaturan"
|
||||
close = "Tutup"
|
||||
title = "Pengaturan"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Dokumen & RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Cari"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Cari alat..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Pemformatan Lanjutan"
|
||||
ai = "AI"
|
||||
automation = "Otomatisasi"
|
||||
developerTools = "Alat Pengembang"
|
||||
documentReview = "Peninjauan Dokumen"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Kolom {{index}}"
|
||||
convertToPdf = "Konversi ke PDF"
|
||||
csvStats = "{{rows}} baris · {{columns}} kolom · {{size}}"
|
||||
emptyFile = "File kosong"
|
||||
htmlHidePreview = "Sembunyikan pratinjau"
|
||||
htmlPreview = "Pratinjau HTML"
|
||||
htmlPreviewMobileHidden = "Halaman HTML ditata untuk lebar desktop, jadi pratinjau dinonaktifkan secara default di sini."
|
||||
htmlPreviewWarning = "Pratinjau HTML — sumber eksternal mungkin tidak dimuat · {{size}}"
|
||||
htmlShowPreview = "Tetap tampilkan pratinjau"
|
||||
invalidJson = "JSON tidak valid — menampilkan konten mentah"
|
||||
lineNumbers = "Nomor baris"
|
||||
loading = "Memuat..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Ekspor PDF"
|
||||
exportSelected = "Ekspor Halaman yang Dipilih"
|
||||
formFill = "Isi Formulir"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Tindakan lainnya"
|
||||
multiTool = "Multi-Tool"
|
||||
panMode = "Mode Geser"
|
||||
print = "Cetak PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Pilih Semua"
|
||||
selectByNumber = "Pilih berdasarkan Nomor Halaman"
|
||||
selectLanguage = "Pilih bahasa"
|
||||
share = "Bagikan"
|
||||
showAllTools = "Tampilkan semua alat"
|
||||
showFewerTools = "Ciutkan bilah alat"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Tampilkan/Sembunyikan Anotasi"
|
||||
toggleAttachments = "Tampilkan/Sembunyikan Lampiran"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Falso"
|
||||
fileNotSavedToDisk = "Non salvato su disco"
|
||||
fileSavedToDisk = "File salvato su disco"
|
||||
fileSelected = "Selezionato: {{filename}}"
|
||||
filesSelected = "file selezionati"
|
||||
filesSelected = "{{count}} file"
|
||||
font = "Carattere"
|
||||
fontSizeTooltip = "Dimensione del testo del numero di pagina in punti. Numeri maggiori creano testo più grande."
|
||||
fontTypeTooltip = "Famiglia di font per i numeri di pagina. Scegli in base allo stile del documento."
|
||||
@@ -3969,6 +3969,7 @@ back = "Indietro"
|
||||
backToFolder = "Torna a {{folder}}"
|
||||
backToMyFiles = "Torna ai miei file"
|
||||
breadcrumbs = "Percorso cartella"
|
||||
bulkActions = "Azioni"
|
||||
cancel = "Annulla"
|
||||
classification = "Classificazione"
|
||||
clearSelection = "Cancella selezione"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Dimensione totale"
|
||||
type = "Tipo"
|
||||
versionHistory = "Percorso delle versioni"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} filtri attivi"
|
||||
clearAll = "Cancella filtri"
|
||||
label = "Filtri"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Annulla"
|
||||
error = "Impossibile salvare la cartella. Riprova."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Ordina file"
|
||||
modifiedAsc = "Più vecchi prima"
|
||||
modifiedDesc = "Più recenti prima"
|
||||
nameAsc = "Nome A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Tutto salvato"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Impossibile applicare le modifiche"
|
||||
extractCsvError = "Impossibile estrarre CSV"
|
||||
extractXlsxError = "Impossibile estrarre XLSX"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Appiattisci dopo la compilazione"
|
||||
goToPage = "Vai a questa pagina"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Pagina"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "obb"
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Riscansiona campi"
|
||||
rescanFormFields = "Riscansiona i campi del modulo"
|
||||
save = "Salva"
|
||||
saveShortcut = "Ctrl+S per salvare"
|
||||
skippedEdits_one = "1 modifica non ha potuto essere applicata:"
|
||||
skippedEdits_other = "{{count}} modifiche non hanno potuto essere applicate:"
|
||||
skippedEditsTruncated = "Altre {{count}} non elencate."
|
||||
unsavedChanges = "Modifiche non salvate"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Aggiungi {{count}} campo/i al PDF"
|
||||
empty = "Nessun campo disegnato ancora."
|
||||
failed = "Impossibile aggiungere i campi"
|
||||
goToField = "Vai a questo campo"
|
||||
hint = "Scegli un tipo di campo, quindi disegnalo sulla pagina."
|
||||
placing = "Disegna un campo {{type}} sulla pagina. Premi Esc per interrompere."
|
||||
preview = "Tieni premuto per l'anteprima"
|
||||
previewHelp = "Tieni premuto per vedere come appariranno i campi una volta aggiunti, senza i contorni di modifica."
|
||||
removeField = "Rimuovi campo"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Azione del pulsante"
|
||||
actionHelp = "Cosa fa il pulsante quando viene cliccato."
|
||||
actionNone = "Nessuna"
|
||||
actionPrint = "Stampa"
|
||||
actionReset = "Reimposta modulo"
|
||||
actionSubmit = "Invia a URL"
|
||||
actionUri = "Apri URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "L'indirizzo che il pulsante apre o a cui invia."
|
||||
addOption = "Aggiungi opzione"
|
||||
caption = "Didascalia del pulsante"
|
||||
captionHelp = "Il testo stampato sulla superficie del pulsante."
|
||||
defaultValue = "Valore predefinito"
|
||||
defaultValueHelp = "Cosa contiene il campo prima che qualcuno lo compili. Lascia vuoto per un campo vuoto."
|
||||
fontSize = "Dimensione carattere"
|
||||
fontSizeHelp = "Dimensione del testo all'interno del campo. Lascia vuoto per consentire al lettore di adattarlo."
|
||||
label = "Etichetta"
|
||||
labelHelp = "Il testo mostrato a chi compila il modulo. Lascialo vuoto per usare il nome del campo."
|
||||
maxLength = "Lunghezza massima (comb)"
|
||||
maxLengthHelp = "Limita il numero di caratteri inseribili, disegnati come caselle equidistanti."
|
||||
multiline = "Multiriga"
|
||||
multilineHelp = "Consente più di una riga di testo e va a capo al bordo del campo."
|
||||
multiSelect = "Consenti selezione multipla"
|
||||
multiSelectHelp = "Consente di scegliere più di un'opzione alla volta."
|
||||
name = "Nome campo"
|
||||
nameHelp = "Il nome interno del campo. Usato quando si esportano dati o si compila il modulo da un altro sistema, quindi mantienilo univoco e senza spazi."
|
||||
optionGap = "Spaziatura opzioni"
|
||||
optionGapHelp = "Spazio tra i pulsanti, in punti. Lascia vuoto per distribuirli uniformemente nel riquadro."
|
||||
optionPlaceholder = "Opzione {{n}}"
|
||||
options = "Opzioni"
|
||||
optionsEmpty = "Aggiungi almeno un'opzione."
|
||||
optionsHelp = "Le scelte offerte nell'elenco. Ognuna viene memorizzata come digitata, quindi mantienile brevi e distinte."
|
||||
optionSize = "Dimensione opzione"
|
||||
optionSizeHelp = "Larghezza e altezza di ogni pulsante, in punti. Lascia vuoto per adattarli al riquadro che hai disegnato."
|
||||
readOnly = "Sola lettura"
|
||||
readOnlyHelp = "Mostra un valore ma impedisce a chiunque di modificarlo."
|
||||
removeOption = "Rimuovi opzione"
|
||||
required = "Obbligatorio"
|
||||
requiredHelp = "Il modulo non può essere inviato finché questo campo non è compilato."
|
||||
signatureNote = "Solo segnaposto: non firmare qui. Indica dove deve andare una firma, così un firmatario PDF (Adobe Acrobat, un servizio di firma, ecc.) inserisce la firma in questo punto quando il documento viene firmato."
|
||||
tooltip = "Tooltip"
|
||||
tooltipHelp = "Il suggerimento mostrato quando qualcuno passa il mouse sul campo in un lettore PDF."
|
||||
type = "Tipo"
|
||||
typeHelp = "Che tipo di campo è. Cambiarlo ricostruisce il campo, quindi il valore corrente non viene mantenuto."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Crea"
|
||||
fill = "Compila"
|
||||
label = "Modalità editor modulo"
|
||||
modify = "Modifica"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Salva {{count}} modifica/e"
|
||||
delete = "Elimina"
|
||||
empty = "Questo PDF non ha ancora campi modulo."
|
||||
failed = "Impossibile salvare le modifiche"
|
||||
groupSizeHint = "Usa dimensione opzione"
|
||||
hint = "Seleziona un campo per modificarne le proprietà, trascinalo sulla pagina o eliminalo."
|
||||
restore = "Ripristina"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Chiudi barra laterale"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Modifica restrizioni e permessi del documento"
|
||||
tags = "permessi,restrizioni,diritti,controllo di accesso,consentire,negare,stampa,copia,modifica,modificare i permessi,impostazioni di sicurezza,diritti utente"
|
||||
title = "Cambia Permessi"
|
||||
|
||||
[home.classify]
|
||||
desc = "Identifica che tipo di documento è e assegnagli un tag."
|
||||
title = "Classifica"
|
||||
|
||||
[home.compare]
|
||||
desc = "Vedi e compara le differenze tra due PDF."
|
||||
tags = "differenza"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Nulla da segnalare."
|
||||
handoffUnavailable = "Questo browser non consente al processore di passare il documento all'editor. Aprilo invece dall'editor."
|
||||
noDocumentLinked = "Questo errore non è collegato a un documento specifico, quindi non c'è nulla da aprire qui."
|
||||
notOnThisDevice = "Questo documento non è su questo dispositivo, quindi non può essere aperto qui."
|
||||
occurrences = "{{count}} volte"
|
||||
open = "Notifiche"
|
||||
title = "Notifiche"
|
||||
unread = "Non lette"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Non ha funzionato. Riprova tra un momento."
|
||||
unavailable = "Non disponibile per questa notifica."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Copiato"
|
||||
copy = "Copia errore"
|
||||
less = "Mostra meno"
|
||||
more = "Mostra messaggio completo"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Precedenti"
|
||||
new = "Nuove"
|
||||
|
||||
[oauth.error]
|
||||
message = "L'autenticazione non è andata a buon fine. Puoi chiudere questa finestra e riprovare."
|
||||
title = "Autenticazione non riuscita"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "di {{allowance}} PDF gratuiti usati"
|
||||
capSuffix_other = "di {{allowance}} PDF gratuiti usati"
|
||||
eyebrow = "Prova Processor"
|
||||
statusLabel_one = "{{remaining}} rimanente"
|
||||
statusLabel_other = "{{remaining}} rimanenti"
|
||||
statusLabel_one = "{{used}} utilizzato"
|
||||
statusLabel_other = "{{used}} utilizzati"
|
||||
sub = "Usa l'Editor PDF gratuitamente. Paga per elaborare PDF automaticamente."
|
||||
title_one = "Elabora {{allowance}} PDF gratuitamente"
|
||||
title_other = "Elabora {{allowance}} PDF gratuitamente"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Visualizza file"
|
||||
viewInProcessor = "Visualizza nel processore"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Questo errore non è stato registrato per un documento specifico, quindi non c'è nulla da aprire qui."
|
||||
unattended = "Questo file è stato fornito da una cartella, bucket o webhook, quindi nessun browser lo sta mantenendo disponibile per l'apertura."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Origini"
|
||||
connectSource = "Collega origine"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Avanzate"
|
||||
back = "Torna alle origini"
|
||||
backToSource = "Torna alla configurazione della sorgente"
|
||||
backToTypes = "Tutti i tipi di origine"
|
||||
cancel = "Annulla"
|
||||
chooseHint = "Scegli da dove provengono i documenti. I connettori disattivati sono in arrivo."
|
||||
@@ -8726,7 +8845,6 @@ create = "Crea origine"
|
||||
createTitle = "Connetti un'origine"
|
||||
delete = "Elimina"
|
||||
editTitle = "Modifica origine"
|
||||
enabled = "Abilitata"
|
||||
save = "Salva modifiche"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: elabora ogni file una volta"
|
||||
snapshot = "Snapshot: rileggi la cartella a ogni esecuzione"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Includi le sottocartelle se i file sono organizzati in cartelle annidate, oppure monitora solo il livello superiore."
|
||||
label = "Profondità cartella"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Dimensione, data e controllo contenuto"
|
||||
stat = "Dimensione e data di modifica"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Se lasciare il file originale al suo posto dopo che è stato elaborato. Se il file originale viene lasciato al suo posto, verrà rielaborato la prossima volta che la pipeline eseguirà la scansione della sorgente."
|
||||
label = "Modalità lettura"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Consuma: elabora ogni file una volta"
|
||||
snapshot = "Snapshot: rileggi la cartella a ogni esecuzione"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Includi le sottocartelle se i file sono organizzati in cartelle annidate, oppure monitora solo il livello superiore."
|
||||
label = "Profondità cartella"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Il tuo server Stirling-PDF è offline e \"{{endpoint}
|
||||
expired = "La tua sessione è scaduta. Aggiorna la pagina e riprova."
|
||||
|
||||
[settings]
|
||||
backToSections = "Tutte le impostazioni"
|
||||
close = "Chiudi"
|
||||
title = "Impostazioni"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Documenti e RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Cerca"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Cerca strumenti..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Formattazione avanzata"
|
||||
ai = "AI"
|
||||
automation = "Automazione"
|
||||
developerTools = "Strumenti per sviluppatori"
|
||||
documentReview = "Revisione documenti"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Colonna {{index}}"
|
||||
convertToPdf = "Converti in PDF"
|
||||
csvStats = "{{rows}} righe · {{columns}} colonne · {{size}}"
|
||||
emptyFile = "File vuoto"
|
||||
htmlHidePreview = "Nascondi anteprima"
|
||||
htmlPreview = "Anteprima HTML"
|
||||
htmlPreviewMobileHidden = "Le pagine HTML sono progettate per larghezze desktop, quindi qui l'anteprima è disattivata per impostazione predefinita."
|
||||
htmlPreviewWarning = "Anteprima HTML — le risorse esterne potrebbero non essere caricate · {{size}}"
|
||||
htmlShowPreview = "Mostra comunque anteprima"
|
||||
invalidJson = "JSON non valido — mostra contenuto grezzo"
|
||||
lineNumbers = "Numeri di riga"
|
||||
loading = "Caricamento..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Esporta PDF"
|
||||
exportSelected = "Esporta pagine selezionate"
|
||||
formFill = "Compila modulo"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Altre azioni"
|
||||
multiTool = "Strumento multiplo"
|
||||
panMode = "Modalità mano"
|
||||
print = "Stampa PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Seleziona tutto"
|
||||
selectByNumber = "Seleziona per numeri di pagina"
|
||||
selectLanguage = "Seleziona lingua"
|
||||
share = "Condividi"
|
||||
showAllTools = "Mostra tutti gli strumenti"
|
||||
showFewerTools = "Comprimi barra degli strumenti"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Mostra/Nascondi annotazioni"
|
||||
toggleAttachments = "Mostra/Nascondi allegati"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "偽"
|
||||
fileNotSavedToDisk = "ディスクに保存されていません"
|
||||
fileSavedToDisk = "ファイルをディスクに保存しました"
|
||||
fileSelected = "選択中:{{filename}}"
|
||||
filesSelected = "選択されたファイル"
|
||||
filesSelected = "{{count}} 個のファイル"
|
||||
font = "フォント"
|
||||
fontSizeTooltip = "ページ番号テキストのポイントサイズ。大きい数値ほど文字が大きくなります。"
|
||||
fontTypeTooltip = "ページ番号のフォントファミリー。文書のスタイルに合わせて選択してください。"
|
||||
@@ -3969,6 +3969,7 @@ back = "戻る"
|
||||
backToFolder = "{{folder}} に戻る"
|
||||
backToMyFiles = "マイファイルに戻る"
|
||||
breadcrumbs = "フォルダパス"
|
||||
bulkActions = "アクション"
|
||||
cancel = "キャンセル"
|
||||
classification = "分類"
|
||||
clearSelection = "選択をクリア"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "合計サイズ"
|
||||
type = "種類"
|
||||
versionHistory = "バージョンの履歴"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} 件のフィルターが有効"
|
||||
clearAll = "フィルターをクリア"
|
||||
label = "フィルター"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "キャンセル"
|
||||
error = "フォルダを保存できませんでした。もう一度お試しください。"
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "ファイルを並べ替え"
|
||||
modifiedAsc = "古い順"
|
||||
modifiedDesc = "新しい順"
|
||||
nameAsc = "名前 A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "すべて保存済み"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "変更を適用できませんでした"
|
||||
extractCsvError = "CSV の抽出に失敗しました"
|
||||
extractXlsxError = "XLSX の抽出に失敗しました"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "入力後にフラット化"
|
||||
goToPage = "このページへ移動"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "ページ"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "必須"
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "フィールドを再スキャン"
|
||||
rescanFormFields = "フォームフィールドを再スキャン"
|
||||
save = "保存"
|
||||
saveShortcut = "Ctrl+S で保存"
|
||||
skippedEdits_one = "1 件の変更を適用できませんでした:"
|
||||
skippedEdits_other = "{{count}} 件の変更を適用できませんでした:"
|
||||
skippedEditsTruncated = "さらに {{count}} 件は表示されていません。"
|
||||
unsavedChanges = "未保存の変更"
|
||||
|
||||
[formFill.create]
|
||||
commit = "{{count}} 個のフィールドを PDF に追加"
|
||||
empty = "まだフィールドが描画されていません。"
|
||||
failed = "フィールドの追加に失敗しました"
|
||||
goToField = "このフィールドへ移動"
|
||||
hint = "フィールドの種類を選択し、ページ上に描画してください。"
|
||||
placing = "ページ上に {{type}} フィールドを描画します。停止するには Esc を押してください。"
|
||||
preview = "長押しでプレビュー"
|
||||
previewHelp = "長押しすると、編集用のアウトラインなしで、追加後のフィールドの見た目を確認できます。"
|
||||
removeField = "フィールドを削除"
|
||||
|
||||
[formFill.editor]
|
||||
action = "ボタンアクション"
|
||||
actionHelp = "クリックされたときにボタンが行う動作です。"
|
||||
actionNone = "なし"
|
||||
actionPrint = "印刷"
|
||||
actionReset = "フォームをリセット"
|
||||
actionSubmit = "URL に送信"
|
||||
actionUri = "URL を開く"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "ボタンが開く、または送信する先のアドレスです。"
|
||||
addOption = "オプションを追加"
|
||||
caption = "ボタンキャプション"
|
||||
captionHelp = "ボタン面に表示されるテキストです。"
|
||||
defaultValue = "デフォルト値"
|
||||
defaultValueHelp = "入力前にフィールドに入っている内容です。空のフィールドにする場合は空欄のままにしてください。"
|
||||
fontSize = "フォントサイズ"
|
||||
fontSizeHelp = "フィールド内のテキストサイズです。リーダー側で収まるように調整する場合は空欄のままにしてください。"
|
||||
label = "ラベル"
|
||||
labelHelp = "フォーム入力者に表示される文言です。空欄にするとフィールド名が使用されます。"
|
||||
maxLength = "最大長(comb)"
|
||||
maxLengthHelp = "入力できる文字数を制限し、等間隔のボックスとして描画します。"
|
||||
multiline = "複数行"
|
||||
multilineHelp = "複数行のテキストを許可し、フィールドの端で折り返します。"
|
||||
multiSelect = "複数選択を許可"
|
||||
multiSelectHelp = "一度に複数のオプションを選択できるようにします。"
|
||||
name = "フィールド名"
|
||||
nameHelp = "フィールドの内部名です。データのエクスポートや別システムからのフォーム入力時に使用されるため、一意でスペースを含まない名前にしてください。"
|
||||
optionGap = "オプション間隔"
|
||||
optionGapHelp = "ボタン間の間隔(ポイント単位)です。空欄にすると、ボックス内で均等に縦方向へ配置されます。"
|
||||
optionPlaceholder = "オプション {{n}}"
|
||||
options = "オプション"
|
||||
optionsEmpty = "少なくとも 1 つのオプションを追加してください。"
|
||||
optionsHelp = "リストに表示する選択肢です。入力したとおりに保存されるため、短く区別しやすいものにしてください。"
|
||||
optionSize = "オプションサイズ"
|
||||
optionSizeHelp = "各ボタンの幅と高さ(ポイント単位)です。空欄にすると、描画したボックスに合わせます。"
|
||||
readOnly = "読み取り専用"
|
||||
readOnlyHelp = "値は表示しますが、編集はできないようにします。"
|
||||
removeOption = "オプションを削除"
|
||||
required = "必須"
|
||||
requiredHelp = "このフィールドが入力されるまでフォームを送信できません。"
|
||||
signatureNote = "プレースホルダーのみです - ここで署名するわけではありません。署名の位置を示すもので、ドキュメントに署名する際に PDF 署名ツール(Adobe Acrobat、署名サービスなど)がこの場所に署名を配置します。"
|
||||
tooltip = "ツールチップ"
|
||||
tooltipHelp = "PDF リーダーでフィールドにカーソルを合わせたときに表示されるヒントです。"
|
||||
type = "種類"
|
||||
typeHelp = "このフィールドの種類です。変更するとフィールドが再作成されるため、現在の値は引き継がれません。"
|
||||
|
||||
[formFill.mode]
|
||||
create = "作成"
|
||||
fill = "入力"
|
||||
label = "フォームエディターモード"
|
||||
modify = "変更"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "{{count}} 件の変更を保存"
|
||||
delete = "削除"
|
||||
empty = "この PDF にはまだフォームフィールドがありません。"
|
||||
failed = "変更の保存に失敗しました"
|
||||
groupSizeHint = "オプションサイズを使用"
|
||||
hint = "フィールドを選択してプロパティを編集するか、ページ上でドラッグするか、削除してください。"
|
||||
restore = "復元"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "サイドバーを閉じる"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "文書の制限と権限を変更"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "権限の変更"
|
||||
|
||||
[home.classify]
|
||||
desc = "このドキュメントの種類を識別してタグ付けします。"
|
||||
title = "分類"
|
||||
|
||||
[home.compare]
|
||||
desc = "2つのPDFを比較して表示します。"
|
||||
tags = "差分"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "報告する内容はありません。"
|
||||
handoffUnavailable = "このブラウザーでは、プロセッサーからエディターにドキュメントを渡せません。代わりにエディターから開いてください。"
|
||||
noDocumentLinked = "この失敗は特定のドキュメントに関連付けられていないため、ここで開くものはありません。"
|
||||
notOnThisDevice = "このドキュメントはこのデバイス上にないため、ここでは開けません。"
|
||||
occurrences = "{{count}} 回"
|
||||
open = "通知"
|
||||
title = "通知"
|
||||
unread = "未読"
|
||||
|
||||
[notifications.action]
|
||||
failed = "実行できませんでした。しばらくしてからもう一度お試しください。"
|
||||
unavailable = "この通知では利用できません。"
|
||||
|
||||
[notifications.detail]
|
||||
copied = "コピーしました"
|
||||
copy = "エラーをコピー"
|
||||
less = "表示を減らす"
|
||||
more = "完全なメッセージを表示"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "以前"
|
||||
new = "新規"
|
||||
|
||||
[oauth.error]
|
||||
message = "認証に失敗しました。このウィンドウを閉じて、もう一度お試しください。"
|
||||
title = "認証に失敗しました"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "{{allowance}} 件の無料PDFのうち使用済み"
|
||||
capSuffix_other = "{{allowance}} 件の無料PDFのうち使用済み"
|
||||
eyebrow = "Processor トライアル"
|
||||
statusLabel_one = "残り {{remaining}}"
|
||||
statusLabel_other = "残り {{remaining}}"
|
||||
statusLabel_one = "{{used}} 使用済み"
|
||||
statusLabel_other = "{{used}} 使用済み"
|
||||
sub = "PDF Editor は無料で使用できます。PDF を自動処理する場合はお支払いください。"
|
||||
title_one = "{{allowance}} 件のPDFを無料で処理"
|
||||
title_other = "{{allowance}} 件のPDFを無料で処理"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "ファイルを表示"
|
||||
viewInProcessor = "プロセッサーで表示"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "この失敗は特定のドキュメントに対して記録されていないため、ここで開くものはありません。"
|
||||
unattended = "このファイルはフォルダー、バケット、または webhook から取り込まれたため、開くために保持しているブラウザーはありません。"
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "ソース"
|
||||
connectSource = "ソースを接続"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "詳細"
|
||||
back = "ソースに戻る"
|
||||
backToSource = "ソース設定に戻る"
|
||||
backToTypes = "すべてのソースタイプ"
|
||||
cancel = "キャンセル"
|
||||
chooseHint = "ドキュメントの取得元を選択してください。グレー表示のコネクターは近日対応予定です。"
|
||||
@@ -8726,7 +8845,6 @@ create = "ソースを作成"
|
||||
createTitle = "ソースを接続"
|
||||
delete = "削除"
|
||||
editTitle = "ソースを編集"
|
||||
enabled = "有効"
|
||||
save = "変更を保存"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: 各ファイルを 1 回処理"
|
||||
snapshot = "Snapshot: 実行ごとにフォルダーを再読み取り"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "ファイルが入れ子のフォルダーに整理されている場合はサブフォルダーを含めるか、最上位のみを監視します。"
|
||||
label = "フォルダー階層"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "サイズ、日付、内容チェック"
|
||||
stat = "サイズと更新日時"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "処理後に元のファイルをそのまま残すかどうかです。元のファイルを残すと、次回パイプラインがソースをスキャンしたときに再処理されます。"
|
||||
label = "読み取りモード"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "消費: 各ファイルを1回処理"
|
||||
snapshot = "スナップショット: 実行ごとにフォルダーを再読み込み"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "ファイルが入れ子のフォルダーに整理されている場合はサブフォルダーを含めるか、最上位のみを監視します。"
|
||||
label = "フォルダーの深さ"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Stirling-PDF サーバーがオフラインで、ロ
|
||||
expired = "セッションが期限切れです。ページを更新してもう一度お試しください。"
|
||||
|
||||
[settings]
|
||||
backToSections = "すべての設定"
|
||||
close = "閉じる"
|
||||
title = "設定"
|
||||
|
||||
[settings.ai]
|
||||
documents = "ドキュメント & RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "検索"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "ツールを検索..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "高度なフォーマット"
|
||||
ai = "AI"
|
||||
automation = "自動化"
|
||||
developerTools = "開発者ツール"
|
||||
documentReview = "文書レビュー"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "列 {{index}}"
|
||||
convertToPdf = "PDF に変換"
|
||||
csvStats = "{{rows}} 行 · {{columns}} 列 · {{size}}"
|
||||
emptyFile = "空のファイル"
|
||||
htmlHidePreview = "プレビューを非表示"
|
||||
htmlPreview = "HTMLプレビュー"
|
||||
htmlPreviewMobileHidden = "HTML ページはデスクトップ幅向けにレイアウトされているため、ここではプレビューはデフォルトでオフになっています。"
|
||||
htmlPreviewWarning = "HTMLプレビュー — 外部リソースは読み込まれない場合があります · {{size}}"
|
||||
htmlShowPreview = "それでもプレビューを表示"
|
||||
invalidJson = "無効な JSON — 生の内容を表示しています"
|
||||
lineNumbers = "行番号"
|
||||
loading = "読み込み中..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "PDF をエクスポート"
|
||||
exportSelected = "選択したページをエクスポート"
|
||||
formFill = "フォーム入力"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "その他のアクション"
|
||||
multiTool = "マルチツール"
|
||||
panMode = "パンモード"
|
||||
print = "PDF を印刷"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "すべて選択"
|
||||
selectByNumber = "ページ番号で選択"
|
||||
selectLanguage = "言語を選択"
|
||||
share = "共有"
|
||||
showAllTools = "すべてのツールを表示"
|
||||
showFewerTools = "ツールバーを折りたたむ"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "注釈の表示を切り替え"
|
||||
toggleAttachments = "添付ファイルを切り替え"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "거짓"
|
||||
fileNotSavedToDisk = "디스크에 저장되지 않았습니다"
|
||||
fileSavedToDisk = "파일이 디스크에 저장되었습니다"
|
||||
fileSelected = "선택됨: {{filename}}"
|
||||
filesSelected = "개의 파일이 선택됨"
|
||||
filesSelected = "{{count}}개 파일"
|
||||
font = "글꼴"
|
||||
fontSizeTooltip = "페이지 번호 텍스트의 포인트 크기입니다. 숫자가 클수록 텍스트가 커집니다."
|
||||
fontTypeTooltip = "페이지 번호에 사용할 글꼴 계열입니다. 문서 스타일에 맞게 선택하세요."
|
||||
@@ -3969,6 +3969,7 @@ back = "뒤로"
|
||||
backToFolder = "{{folder}}로 돌아가기"
|
||||
backToMyFiles = "내 파일로 돌아가기"
|
||||
breadcrumbs = "폴더 경로"
|
||||
bulkActions = "작업"
|
||||
cancel = "취소"
|
||||
classification = "분류"
|
||||
clearSelection = "선택 해제"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "총 크기"
|
||||
type = "유형"
|
||||
versionHistory = "버전 기록"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}}개 필터 활성화됨"
|
||||
clearAll = "필터 지우기"
|
||||
label = "필터"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "취소"
|
||||
error = "폴더를 저장할 수 없습니다. 다시 시도하세요."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "파일 정렬"
|
||||
modifiedAsc = "오래된 순"
|
||||
modifiedDesc = "최신 순"
|
||||
nameAsc = "이름 A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "모두 저장됨"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "변경 사항을 적용할 수 없습니다"
|
||||
extractCsvError = "CSV 추출 실패"
|
||||
extractXlsxError = "XLSX 추출 실패"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "채운 후 평면화"
|
||||
goToPage = "이 페이지로 이동"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "페이지"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "필수"
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "필드 재검색"
|
||||
rescanFormFields = "양식 필드 재검색"
|
||||
save = "저장"
|
||||
saveShortcut = "저장하려면 Ctrl+S"
|
||||
skippedEdits_one = "1개 변경 사항을 적용할 수 없습니다:"
|
||||
skippedEdits_other = "{{count}}개 변경 사항을 적용할 수 없습니다:"
|
||||
skippedEditsTruncated = "{{count}}개가 더 있으며 목록에 표시되지 않았습니다."
|
||||
unsavedChanges = "저장되지 않은 변경 사항"
|
||||
|
||||
[formFill.create]
|
||||
commit = "{{count}}개 필드를 PDF에 추가"
|
||||
empty = "아직 그린 필드가 없습니다."
|
||||
failed = "필드 추가 실패"
|
||||
goToField = "이 필드로 이동"
|
||||
hint = "필드 유형을 선택한 다음 페이지에 그리세요."
|
||||
placing = "페이지에 {{type}} 필드를 그리세요. 중지하려면 Esc를 누르세요."
|
||||
preview = "길게 눌러 미리보기"
|
||||
previewHelp = "편집 윤곽선 없이 추가된 후의 필드 모양을 보려면 길게 누르세요."
|
||||
removeField = "필드 제거"
|
||||
|
||||
[formFill.editor]
|
||||
action = "버튼 동작"
|
||||
actionHelp = "버튼을 클릭했을 때 수행할 동작입니다."
|
||||
actionNone = "없음"
|
||||
actionPrint = "인쇄"
|
||||
actionReset = "양식 재설정"
|
||||
actionSubmit = "URL로 제출"
|
||||
actionUri = "URL 열기"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "버튼이 열거나 제출할 주소입니다."
|
||||
addOption = "옵션 추가"
|
||||
caption = "버튼 캡션"
|
||||
captionHelp = "버튼 표면에 표시될 텍스트입니다."
|
||||
defaultValue = "기본값"
|
||||
defaultValueHelp = "누군가 입력하기 전에 필드에 포함될 내용입니다. 빈 필드로 두려면 비워 두세요."
|
||||
fontSize = "글꼴 크기"
|
||||
fontSizeHelp = "필드 안의 텍스트 크기입니다. 리더가 맞게 크기를 조정하도록 하려면 비워 두세요."
|
||||
label = "레이블"
|
||||
labelHelp = "양식을 작성하는 사람에게 표시될 문구입니다. 비워 두면 필드 이름으로 대체됩니다."
|
||||
maxLength = "최대 길이(콤)"
|
||||
maxLengthHelp = "들어갈 문자 수를 제한하며, 균등한 간격의 상자로 그려집니다."
|
||||
multiline = "여러 줄"
|
||||
multilineHelp = "두 줄 이상의 텍스트를 허용하고 필드 가장자리에서 줄 바꿈합니다."
|
||||
multiSelect = "다중 선택 허용"
|
||||
multiSelectHelp = "한 번에 둘 이상의 옵션을 선택할 수 있습니다."
|
||||
name = "필드 이름"
|
||||
nameHelp = "필드의 내부 이름입니다. 데이터를 내보내거나 다른 시스템에서 양식을 작성할 때 사용되므로, 고유하고 공백이 없도록 유지하세요."
|
||||
optionGap = "옵션 간격"
|
||||
optionGapHelp = "버튼 사이의 간격(포인트)입니다. 상자 아래로 균등하게 배치하려면 비워 두세요."
|
||||
optionPlaceholder = "옵션 {{n}}"
|
||||
options = "옵션"
|
||||
optionsEmpty = "하나 이상의 옵션을 추가하세요."
|
||||
optionsHelp = "목록에 제공되는 선택지입니다. 각 항목은 입력한 그대로 저장되므로, 짧고 구별되게 유지하세요."
|
||||
optionSize = "옵션 크기"
|
||||
optionSizeHelp = "각 버튼의 너비와 높이(포인트)입니다. 그린 상자에 맞추려면 비워 두세요."
|
||||
readOnly = "읽기 전용"
|
||||
readOnlyHelp = "값을 표시하지만 아무도 편집할 수 없게 합니다."
|
||||
removeOption = "옵션 제거"
|
||||
required = "필수"
|
||||
requiredHelp = "이 필드를 입력해야 양식을 제출할 수 있습니다."
|
||||
signatureNote = "자리 표시자 전용 - 여기서 서명하지 않습니다. PDF 서명자(Adobe Acrobat, 서명 서비스 등)가 문서에 서명할 때 이 위치에 서명을 배치하도록 서명이 들어갈 위치를 표시합니다."
|
||||
tooltip = "툴팁"
|
||||
tooltipHelp = "PDF 리더에서 누군가 필드 위에 마우스를 올렸을 때 표시되는 힌트입니다."
|
||||
type = "유형"
|
||||
typeHelp = "이 필드의 종류입니다. 변경하면 필드가 다시 생성되므로 현재 값은 유지되지 않습니다."
|
||||
|
||||
[formFill.mode]
|
||||
create = "생성"
|
||||
fill = "채우기"
|
||||
label = "양식 편집기 모드"
|
||||
modify = "수정"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "{{count}}개 변경 사항 저장"
|
||||
delete = "삭제"
|
||||
empty = "이 PDF에는 아직 양식 필드가 없습니다."
|
||||
failed = "변경 사항 저장 실패"
|
||||
groupSizeHint = "옵션 크기 사용"
|
||||
hint = "필드를 선택하여 속성을 편집하거나, 페이지에서 드래그하거나, 삭제하세요."
|
||||
restore = "복원"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "사이드바 닫기"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "문서 제한 및 권한 변경"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "권한 변경"
|
||||
|
||||
[home.classify]
|
||||
desc = "이 문서의 종류를 식별하고 태그를 지정합니다."
|
||||
title = "분류"
|
||||
|
||||
[home.compare]
|
||||
desc = "2개의 PDF 문서를 비교하고 차이점을 보여줍니다"
|
||||
tags = "차이"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "보고할 내용이 없습니다."
|
||||
handoffUnavailable = "이 브라우저는 처리기가 문서를 편집기로 전달하는 것을 허용하지 않습니다. 대신 편집기에서 여세요."
|
||||
noDocumentLinked = "이 실패는 특정 문서와 연결되어 있지 않아 여기서 열 항목이 없습니다."
|
||||
notOnThisDevice = "이 문서는 이 장치에 없으므로 여기서 열 수 없습니다."
|
||||
occurrences = "{{count}}회"
|
||||
open = "알림"
|
||||
title = "알림"
|
||||
unread = "읽지 않음"
|
||||
|
||||
[notifications.action]
|
||||
failed = "작동하지 않았습니다. 잠시 후 다시 시도하세요."
|
||||
unavailable = "이 알림에는 사용할 수 없습니다."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "복사됨"
|
||||
copy = "오류 복사"
|
||||
less = "간단히 보기"
|
||||
more = "전체 메시지 표시"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "이전"
|
||||
new = "새 알림"
|
||||
|
||||
[oauth.error]
|
||||
message = "인증에 성공하지 못했습니다. 이 창을 닫고 다시 시도하세요."
|
||||
title = "인증 실패"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "무료 PDFs {{allowance}}개 중 사용됨"
|
||||
capSuffix_other = "무료 PDFs {{allowance}}개 중 사용됨"
|
||||
eyebrow = "Processor 평가판"
|
||||
statusLabel_one = "{{remaining}} 남음"
|
||||
statusLabel_other = "{{remaining}} 남음"
|
||||
statusLabel_one = "{{used}} 사용됨"
|
||||
statusLabel_other = "{{used}} 사용됨"
|
||||
sub = "PDF Editor는 무료로 사용하세요. PDF 자동 처리에는 비용이 부과됩니다."
|
||||
title_one = "PDF {{allowance}}개 무료 처리"
|
||||
title_other = "PDF {{allowance}}개 무료 처리"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "파일 보기"
|
||||
viewInProcessor = "처리기에서 보기"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "이 실패는 특정 문서에 대해 기록되지 않았으므로 여기서 열 항목이 없습니다."
|
||||
unattended = "이 파일은 폴더, 버킷 또는 webhook을 통해 전달되었으므로, 열기 위해 보관 중인 브라우저가 없습니다."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "소스"
|
||||
connectSource = "소스 연결"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "고급"
|
||||
back = "소스로 돌아가기"
|
||||
backToSource = "소스 설정으로 돌아가기"
|
||||
backToTypes = "모든 소스 유형"
|
||||
cancel = "취소"
|
||||
chooseHint = "문서가 어디에서 오는지 선택하세요. 회색으로 표시된 커넥터는 곧 제공됩니다."
|
||||
@@ -8726,7 +8845,6 @@ create = "소스 생성"
|
||||
createTitle = "소스 연결"
|
||||
delete = "삭제"
|
||||
editTitle = "소스 편집"
|
||||
enabled = "활성화됨"
|
||||
save = "변경 사항 저장"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "소비: 각 파일을 한 번 처리"
|
||||
snapshot = "스냅샷: 실행할 때마다 폴더 다시 읽기"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "파일이 중첩된 폴더로 구성되어 있으면 하위 폴더를 포함하고, 아니면 최상위 수준만 감시하세요."
|
||||
label = "폴더 깊이"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "크기, 날짜 및 콘텐츠 검사"
|
||||
stat = "크기 및 수정 날짜"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "처리된 후 원본 파일을 그대로 둘지 여부입니다. 원본 파일을 그대로 두면 다음에 파이프라인이 소스를 스캔할 때 다시 처리됩니다."
|
||||
label = "읽기 모드"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "소비: 각 파일을 한 번 처리"
|
||||
snapshot = "스냅샷: 실행할 때마다 폴더 다시 읽기"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "파일이 중첩된 폴더로 구성되어 있으면 하위 폴더를 포함하고, 아니면 최상위 수준만 감시하세요."
|
||||
label = "폴더 깊이"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Stirling-PDF 서버가 오프라인이며 로컬 백
|
||||
expired = "세션이 만료되었습니다. 페이지를 새로 고침하고 다시 시도하세요."
|
||||
|
||||
[settings]
|
||||
backToSections = "모든 설정"
|
||||
close = "닫기"
|
||||
title = "설정"
|
||||
|
||||
[settings.ai]
|
||||
documents = "문서 및 RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "검색"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "도구 검색..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "고급 서식"
|
||||
ai = "AI"
|
||||
automation = "자동화"
|
||||
developerTools = "개발자 도구"
|
||||
documentReview = "문서 검토"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "열 {{index}}"
|
||||
convertToPdf = "PDF로 변환"
|
||||
csvStats = "{{rows}}행 · {{columns}}열 · {{size}}"
|
||||
emptyFile = "빈 파일"
|
||||
htmlHidePreview = "미리보기 숨기기"
|
||||
htmlPreview = "HTML 미리보기"
|
||||
htmlPreviewMobileHidden = "HTML 페이지는 데스크톱 너비에 맞게 배치되므로 여기서는 미리보기가 기본적으로 꺼져 있습니다."
|
||||
htmlPreviewWarning = "HTML 미리보기 — 외부 리소스가 로드되지 않을 수 있습니다 · {{size}}"
|
||||
htmlShowPreview = "그래도 미리보기 표시"
|
||||
invalidJson = "잘못된 JSON — 원본 내용을 표시합니다"
|
||||
lineNumbers = "줄 번호"
|
||||
loading = "로딩 중..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "PDF 내보내기"
|
||||
exportSelected = "선택한 페이지 내보내기"
|
||||
formFill = "양식 채우기"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "추가 작업"
|
||||
multiTool = "멀티 도구"
|
||||
panMode = "이동 모드"
|
||||
print = "PDF 인쇄"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "모두 선택"
|
||||
selectByNumber = "페이지 번호로 선택"
|
||||
selectLanguage = "언어 선택"
|
||||
share = "공유"
|
||||
showAllTools = "모든 도구 표시"
|
||||
showFewerTools = "도구 모음 접기"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "주석 표시 전환"
|
||||
toggleAttachments = "첨부 파일 표시 전환"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "തെറ്റ്"
|
||||
fileNotSavedToDisk = "ഡിസ്കിലേക്ക് സംരക്ഷിച്ചില്ല"
|
||||
fileSavedToDisk = "ഫയൽ ഡിസ്കിലേക്ക് സംരക്ഷിച്ചു"
|
||||
fileSelected = "തിരഞ്ഞെടുത്തത്: {{filename}}"
|
||||
filesSelected = "ഫയലുകൾ തിരഞ്ഞെടുത്തു"
|
||||
filesSelected = "{{count}} ഫയലുകൾ"
|
||||
font = "അക്ഷരം"
|
||||
fontSizeTooltip = "പേജ് നമ്പർ ടെക്സ്റ്റിന്റെ പോയിന്റ് സൈസ്. വലിയ നമ്പറുകൾ വലുതായ ടെക്സ്റ്റ് സൃഷ്ടിക്കും."
|
||||
fontTypeTooltip = "പേജ് നമ്പറുകളുടെ ഫോണ്ട് കുടുംബം. നിങ്ങളുടെ ഡോക്യുമെന്റിന്റെ ശൈലിയനുസരിച്ച് തിരഞ്ഞെടുക്കുക."
|
||||
@@ -3969,6 +3969,7 @@ back = "തിരികെ"
|
||||
backToFolder = "{{folder}} ലേക്ക് തിരികെ"
|
||||
backToMyFiles = "എന്റെ ഫയലുകളിലേക്ക് തിരികെ"
|
||||
breadcrumbs = "ഫോൾഡർ പാത"
|
||||
bulkActions = "പ്രവർത്തനങ്ങൾ"
|
||||
cancel = "റദ്ദാക്കുക"
|
||||
classification = "വർഗ്ഗീകരണം"
|
||||
clearSelection = "തിരഞ്ഞെടുപ്പ് നീക്കുക"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "ആകെ വലുപ്പം"
|
||||
type = "തരം"
|
||||
versionHistory = "വേർഷൻ യാത്ര"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} ഫിൽട്ടറുകൾ സജീവമാണ്"
|
||||
clearAll = "ഫിൽട്ടറുകൾ മായ്ക്കുക"
|
||||
label = "ഫിൽട്ടറുകൾ"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "റദ്ദാക്കുക"
|
||||
error = "ഫോൾഡർ സംരക്ഷിക്കാൻ കഴിഞ്ഞില്ല. വീണ്ടും ശ്രമിക്കുക."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "ഫയലുകൾ ക്രമീകരിക്കുക"
|
||||
modifiedAsc = "പഴയത് ആദ്യം"
|
||||
modifiedDesc = "പുതിയത് ആദ്യം"
|
||||
nameAsc = "പേര് A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "എല്ലാം സേവ് ചെയ്തു"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "മാറ്റങ്ങൾ പ്രയോഗിക്കാൻ കഴിഞ്ഞില്ല"
|
||||
extractCsvError = "CSV extract ചെയ്യാനായില്ല"
|
||||
extractXlsxError = "XLSX extract ചെയ്യാനായില്ല"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "പൂരിപ്പിച്ചതിന് ശേഷം ഫ്ലാറ്റൻ ചെയ്യുക"
|
||||
goToPage = "ഈ പേജിലേക്ക് പോകുക"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "പേജ്"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "ആവ."
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "ഫീൽഡുകൾ വീണ്ടും സ്കാൻ ച
|
||||
rescanFormFields = "ഫോം ഫീൽഡുകൾ വീണ്ടും സ്കാൻ ചെയ്യുക"
|
||||
save = "സംരക്ഷിക്കുക"
|
||||
saveShortcut = "സേവ് ചെയ്യാൻ Ctrl+S"
|
||||
skippedEdits_one = "1 മാറ്റം പ്രയോഗിക്കാൻ കഴിഞ്ഞില്ല:"
|
||||
skippedEdits_other = "{{count}} മാറ്റങ്ങൾ പ്രയോഗിക്കാൻ കഴിഞ്ഞില്ല:"
|
||||
skippedEditsTruncated = "{{count}} എണ്ണം കൂടി പട്ടികപ്പെടുത്തിയിട്ടില്ല."
|
||||
unsavedChanges = "സേവ് ചെയ്യാത്ത മാറ്റങ്ങൾ"
|
||||
|
||||
[formFill.create]
|
||||
commit = "PDF-ലേക്ക് {{count}} ഫീൽഡ്(കൾ) ചേർക്കുക"
|
||||
empty = "ഇതുവരെ ഫീൽഡുകളൊന്നും വരച്ചിട്ടില്ല."
|
||||
failed = "ഫീൽഡുകൾ ചേർക്കുന്നതിൽ പരാജയപ്പെട്ടു"
|
||||
goToField = "ഈ ഫീൽഡിലേക്ക് പോകുക"
|
||||
hint = "ഒരു ഫീൽഡ് തരം തിരഞ്ഞെടുക്കുക, തുടർന്ന് അത് പേജിൽ വരയ്ക്കുക."
|
||||
placing = "പേജിൽ ഒരു {{type}} ഫീൽഡ് വരയ്ക്കുക. നിർത്താൻ Esc അമർത്തുക."
|
||||
preview = "പ്രിവ്യൂ കാണാൻ അമർത്തിപ്പിടിക്കുക"
|
||||
previewHelp = "എഡിറ്റിംഗ് ഔട്ട്ലൈനുകൾ ഇല്ലാതെ ചേർത്തുകഴിഞ്ഞാൽ ഫീൽഡുകൾ എങ്ങനെ കാണപ്പെടും എന്ന് കാണാൻ അമർത്തിപ്പിടിക്കുക."
|
||||
removeField = "ഫീൽഡ് നീക്കംചെയ്യുക"
|
||||
|
||||
[formFill.editor]
|
||||
action = "ബട്ടൺ പ്രവർത്തനം"
|
||||
actionHelp = "ക്ലിക്ക് ചെയ്യുമ്പോൾ ബട്ടൺ ചെയ്യുന്നത്."
|
||||
actionNone = "ഒന്നുമില്ല"
|
||||
actionPrint = "പ്രിന്റ് ചെയ്യുക"
|
||||
actionReset = "ഫോം റീസെറ്റ് ചെയ്യുക"
|
||||
actionSubmit = "URL-ലേക്ക് സമർപ്പിക്കുക"
|
||||
actionUri = "URL തുറക്കുക"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "ബട്ടൺ തുറക്കുകയോ സമർപ്പിക്കുകയോ ചെയ്യുന്ന വിലാസം."
|
||||
addOption = "ഓപ്ഷൻ ചേർക്കുക"
|
||||
caption = "ബട്ടൺ ക്യാപ്ഷൻ"
|
||||
captionHelp = "ബട്ടണിന്റെ മുഖഭാഗത്ത് പ്രിന്റ് ചെയ്യുന്ന വാചകം."
|
||||
defaultValue = "സ്ഥിര മൂല്യം"
|
||||
defaultValueHelp = "ആരും പൂരിപ്പിക്കുന്നതിന് മുമ്പ് ഫീൽഡിൽ അടങ്ങിയിരിക്കുന്നതു. ശൂന്യമായ ഫീൽഡിന് ശൂന്യമായി വിടുക."
|
||||
fontSize = "ഫോണ്ട് വലുപ്പം"
|
||||
fontSizeHelp = "ഫീൽഡിനുള്ളിലെ ടെക്സ്റ്റ് വലുപ്പം. യോജിക്കുന്ന രീതിയിൽ reader വലുപ്പം നിശ്ചയിക്കാൻ ശൂന്യമായി വിടുക."
|
||||
label = "ലേബൽ"
|
||||
labelHelp = "ഫോം പൂരിപ്പിക്കുന്ന വ്യക്തിക്ക് കാണിക്കുന്ന വാചകം. ഫീൽഡ് പേരിലേക്ക് മടങ്ങാൻ ഇത് ശൂന്യമായി വിടുക."
|
||||
maxLength = "പരമാവധി നീളം (comb)"
|
||||
maxLengthHelp = "എത്ര അക്ഷരങ്ങൾ ഉൾക്കൊള്ളാമെന്ന് പരിമിതപ്പെടുത്തുന്നു, തുല്യ അകലത്തിലുള്ള ബോക്സുകളായി വരയ്ക്കുന്നു."
|
||||
multiline = "ഒന്നിലധികം വരികൾ"
|
||||
multilineHelp = "ഒന്നിൽ കൂടുതൽ ടെക്സ്റ്റ് വരികൾ അനുവദിക്കുകയും ഫീൽഡിന്റെ അരികിൽ വരി മടക്കുകയും ചെയ്യുന്നു."
|
||||
multiSelect = "ഒന്നിലധികം തിരഞ്ഞെടുക്കൽ അനുവദിക്കുക"
|
||||
multiSelectHelp = "ഒരേ സമയം ഒന്നിൽ കൂടുതൽ ഓപ്ഷനുകൾ തിരഞ്ഞെടുക്കാൻ അനുവദിക്കുന്നു."
|
||||
name = "ഫീൽഡ് പേര്"
|
||||
nameHelp = "ഫീൽഡിന്റെ ആന്തരിക പേര്. ഡാറ്റ എക്സ്പോർട്ട് ചെയ്യുമ്പോഴോ മറ്റൊരു സിസ്റ്റത്തിൽ നിന്ന് ഫോം പൂരിപ്പിക്കുമ്പോഴോ ഉപയോഗിക്കുന്നു, അതിനാൽ ഇത് സവിശേഷവും സ്പേസുകളില്ലാത്തതുമാക്കുക."
|
||||
optionGap = "ഓപ്ഷൻ ഇടവേള"
|
||||
optionGapHelp = "ബട്ടണുകൾക്കിടയിലെ ഇടവേള, പോയിന്റുകളിൽ. ബോക്സിൽ തുല്യമായി താഴേക്ക് പരത്താൻ ശൂന്യമായി വിടുക."
|
||||
optionPlaceholder = "ഓപ്ഷൻ {{n}}"
|
||||
options = "ഓപ്ഷനുകൾ"
|
||||
optionsEmpty = "കുറഞ്ഞത് ഒരു ഓപ്ഷൻ ചേർക്കുക."
|
||||
optionsHelp = "ലിസ്റ്റിൽ നൽകുന്ന തിരഞ്ഞെടുപ്പുകൾ. ഓരോന്നും ടൈപ്പ് ചെയ്തതുപോലെ സൂക്ഷിക്കും, അതിനാൽ അവ ചെറുതും വ്യത്യസ്തവുമാക്കുക."
|
||||
optionSize = "ഓപ്ഷൻ വലുപ്പം"
|
||||
optionSizeHelp = "ഓരോ ബട്ടണിന്റെയും വീതിയും ഉയരവും, പോയിന്റുകളിൽ. നിങ്ങൾ വരച്ച ബോക്സിന് യോജിപ്പിക്കാൻ ശൂന്യമായി വിടുക."
|
||||
readOnly = "വായനയ്ക്ക് മാത്രം"
|
||||
readOnlyHelp = "ഒരു മൂല്യം കാണിക്കുന്നു, പക്ഷേ ആരെയും അത് എഡിറ്റ് ചെയ്യുന്നതിൽ നിന്ന് തടയുന്നു."
|
||||
removeOption = "ഓപ്ഷൻ നീക്കംചെയ്യുക"
|
||||
required = "ആവശ്യമാണ്"
|
||||
requiredHelp = "ഈ ഫീൽഡ് പൂരിപ്പിക്കുന്നതുവരെ ഫോം സമർപ്പിക്കാൻ കഴിയില്ല."
|
||||
signatureNote = "പ്ലേസ്ഹോൾഡർ മാത്രം - നിങ്ങൾ ഇവിടെ ഒപ്പിടുന്നില്ല. ഒരു ഒപ്പ് എവിടെ വരണമെന്ന് ഇത് അടയാളപ്പെടുത്തുന്നു, അതിനാൽ PDF signer (Adobe Acrobat, ഒരു signing service, മുതലായവ) ഡോക്യുമെന്റ് ഒപ്പിടുമ്പോൾ ഈ സ്ഥാനത്ത് ഒപ്പ് സ്ഥാപിക്കുന്നു."
|
||||
tooltip = "Tooltip"
|
||||
tooltipHelp = "PDF reader-ൽ ആരെങ്കിലും ഫീൽഡിന് മുകളിൽ ഹോവർ ചെയ്യുമ്പോൾ കാണിക്കുന്ന സൂചന."
|
||||
type = "തരം"
|
||||
typeHelp = "ഇത് ഏത് തരത്തിലുള്ള ഫീൽഡാണ്. ഇത് മാറ്റുന്നത് ഫീൽഡ് പുനർനിർമ്മിക്കും, അതിനാൽ നിലവിലെ മൂല്യം കൊണ്ടുപോകില്ല."
|
||||
|
||||
[formFill.mode]
|
||||
create = "സൃഷ്ടിക്കുക"
|
||||
fill = "പൂരിപ്പിക്കുക"
|
||||
label = "ഫോം എഡിറ്റർ മോഡ്"
|
||||
modify = "പരിഷ്കരിക്കുക"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "{{count}} മാറ്റം(ങ്ങൾ) സംരക്ഷിക്കുക"
|
||||
delete = "ഇല്ലാതാക്കുക"
|
||||
empty = "ഈ PDF-ൽ ഇതുവരെ ഫോം ഫീൽഡുകളൊന്നുമില്ല."
|
||||
failed = "മാറ്റങ്ങൾ സംരക്ഷിക്കുന്നതിൽ പരാജയപ്പെട്ടു"
|
||||
groupSizeHint = "ഓപ്ഷൻ വലുപ്പം ഉപയോഗിക്കുക"
|
||||
hint = "ഗുണങ്ങൾ എഡിറ്റ് ചെയ്യാൻ ഒരു ഫീൽഡ് തിരഞ്ഞെടുക്കുക, അത് പേജിൽ വലിച്ചിടുക, അല്ലെങ്കിൽ ഇല്ലാതാക്കുക."
|
||||
restore = "പുനഃസ്ഥാപിക്കുക"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "സൈഡ്ബാർ അടയ്ക്കുക"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "ഡോക്യുമെന്റ് നിയന്ത്രണങ്
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "അനുമതികൾ മാറ്റുക"
|
||||
|
||||
[home.classify]
|
||||
desc = "ഇത് ഏത് തരത്തിലുള്ള ഡോക്യുമെന്റാണെന്ന് തിരിച്ചറിഞ്ഞ് ടാഗ് ചെയ്യുക."
|
||||
title = "വർഗീകരിക്കുക"
|
||||
|
||||
[home.compare]
|
||||
desc = "2 PDF പ്രമാണങ്ങൾ തമ്മിലുള്ള വ്യത്യാസങ്ങൾ താരതമ്യം ചെയ്യുകയും കാണിക്കുകയും ചെയ്യുന്നു"
|
||||
tags = "വ്യത്യാസം"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "റിപ്പോർട്ട് ചെയ്യാൻ ഒന്നുമില്ല."
|
||||
handoffUnavailable = "ഈ browser പ്രോസസ്സറിന് ഡോക്യുമെന്റ് എഡിറ്ററിലേക്ക് കൈമാറാൻ അനുവദിക്കില്ല. പകരം എഡിറ്ററിൽ നിന്ന് അത് തുറക്കുക."
|
||||
noDocumentLinked = "ഈ പരാജയം ഒരു നിർദ്ദിഷ്ട ഡോക്യുമെന്റുമായി ലിങ്ക് ചെയ്തിട്ടില്ല, അതിനാൽ ഇവിടെ തുറക്കാൻ ഒന്നുമില്ല."
|
||||
notOnThisDevice = "ഈ ഡോക്യുമെന്റ് ഈ ഉപകരണത്തിലില്ല, അതിനാൽ ഇവിടെ തുറക്കാൻ കഴിയില്ല."
|
||||
occurrences = "{{count}} തവണ"
|
||||
open = "അറിയിപ്പുകൾ"
|
||||
title = "അറിയിപ്പുകൾ"
|
||||
unread = "വായിക്കാത്തത്"
|
||||
|
||||
[notifications.action]
|
||||
failed = "അത് പ്രവർത്തിച്ചില്ല. കുറച്ച് സമയത്തിന് ശേഷം വീണ്ടും ശ്രമിക്കുക."
|
||||
unavailable = "ഈ അറിയിപ്പിനായി ലഭ്യമല്ല."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "പകർത്തി"
|
||||
copy = "പിശക് പകർത്തുക"
|
||||
less = "കുറച്ച് കാണിക്കുക"
|
||||
more = "മുഴുവൻ സന്ദേശവും കാണിക്കുക"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "മുമ്പത്തേത്"
|
||||
new = "പുതിയത്"
|
||||
|
||||
[oauth.error]
|
||||
message = "ഓതന്റിക്കേഷൻ വിജയിച്ചില്ല. ഈ വിൻഡോ അടച്ച് വീണ്ടും ശ്രമിക്കാം."
|
||||
title = "ഓതന്റിക്കേഷൻ പരാജയപ്പെട്ടു"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "{{allowance}} free PDFs-ൽ ഉപയോഗിച്ചത്"
|
||||
capSuffix_other = "{{allowance}} free PDFs-ൽ ഉപയോഗിച്ചത്"
|
||||
eyebrow = "Processor trial"
|
||||
statusLabel_one = "{{remaining}} ശേഷിക്കുന്നു"
|
||||
statusLabel_other = "{{remaining}} ശേഷിക്കുന്നു"
|
||||
statusLabel_one = "{{used}} ഉപയോഗിച്ചു"
|
||||
statusLabel_other = "{{used}} ഉപയോഗിച്ചു"
|
||||
sub = "PDF Editor സൗജന്യമായി ഉപയോഗിക്കുക. PDFs സ്വയമേവ പ്രോസസ് ചെയ്യാൻ പണം നൽകുക."
|
||||
title_one = "{{allowance}} PDFs സൗജന്യമായി പ്രോസസ് ചെയ്യുക"
|
||||
title_other = "{{allowance}} PDFs സൗജന്യമായി പ്രോസസ് ചെയ്യുക"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "ഫയൽ കാണുക"
|
||||
viewInProcessor = "പ്രോസസ്സറിൽ കാണുക"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "ഈ പരാജയം ഒരു നിർദ്ദിഷ്ട ഡോക്യുമെന്റിനെതിരെ രേഖപ്പെടുത്തിയിട്ടില്ല, അതിനാൽ ഇവിടെ തുറക്കാൻ ഒന്നുമില്ല."
|
||||
unattended = "ഈ ഫയൽ ഒരു ഫോൾഡർ, bucket അല്ലെങ്കിൽ webhook വഴി നൽകിയതാണ്, അതിനാൽ തുറക്കാൻ ആരുടെയും browser അത് കൈവശം വച്ചിട്ടില്ല."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "ഉറവിടങ്ങൾ"
|
||||
connectSource = "ഉറവിടം കണക്റ്റ് ചെയ്യുക"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "വിപുലമായത്"
|
||||
back = "sources-ലേക്ക് മടങ്ങുക"
|
||||
backToSource = "സോഴ്സ് സജ്ജീകരണത്തിലേക്ക് മടങ്ങുക"
|
||||
backToTypes = "എല്ലാ source types"
|
||||
cancel = "റദ്ദാക്കുക"
|
||||
chooseHint = "ഡോക്യുമെന്റുകൾ എവിടെ നിന്നാണ് വരുന്നതെന്ന് തിരഞ്ഞെടുക്കുക. Greyed-out connectors വരാനിരിക്കുകയാണ്."
|
||||
@@ -8726,7 +8845,6 @@ create = "source സൃഷ്ടിക്കുക"
|
||||
createTitle = "ഒരു source ബന്ധിപ്പിക്കുക"
|
||||
delete = "ഇല്ലാതാക്കുക"
|
||||
editTitle = "source edit ചെയ്യുക"
|
||||
enabled = "Enabled"
|
||||
save = "മാറ്റങ്ങൾ save ചെയ്യുക"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: ഓരോ file-ഉം ഒരിക്കൽ process ചെ
|
||||
snapshot = "Snapshot: ഓരോ run-ലും folder വീണ്ടും വായിക്കുക"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "നിങ്ങളുടെ ഫയലുകൾ nested ഫോൾഡറുകളായി ക്രമീകരിച്ചിട്ടുണ്ടെങ്കിൽ സബ്ഫോൾഡറുകൾ ഉൾപ്പെടുത്തുക, അല്ലെങ്കിൽ മുകളിലെ ലെവൽ മാത്രം നിരീക്ഷിക്കുക."
|
||||
label = "Folder depth"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Size, date, content check"
|
||||
stat = "Size and date modified"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "പ്രോസസ് ചെയ്ത ശേഷം യഥാർത്ഥ ഫയൽ അതേ സ്ഥലത്ത് വിടണോ എന്നത്. യഥാർത്ഥ ഫയൽ അതേ സ്ഥലത്ത് വിടുകയാണെങ്കിൽ, അടുത്ത തവണ pipeline സോഴ്സ് സ്കാൻ ചെയ്യുമ്പോൾ അത് വീണ്ടും പ്രോസസ് ചെയ്യും."
|
||||
label = "Read mode"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Consume: ഓരോ ഫയലും ഒരിക്കൽ പ്രോ
|
||||
snapshot = "Snapshot: ഓരോ run-ലും folder വീണ്ടും വായിക്കുക"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "നിങ്ങളുടെ ഫയലുകൾ nested ഫോൾഡറുകളായി ക്രമീകരിച്ചിട്ടുണ്ടെങ്കിൽ സബ്ഫോൾഡറുകൾ ഉൾപ്പെടുത്തുക, അല്ലെങ്കിൽ മുകളിലെ ലെവൽ മാത്രം നിരീക്ഷിക്കുക."
|
||||
label = "Folder depth"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "നിങ്ങളുടെ Stirling-PDF സെർവ
|
||||
expired = "നിങ്ങളുടെ സെഷൻ കാലഹരണപ്പെട്ടു. ദയവായി പേജ് പുതുക്കി വീണ്ടും ശ്രമിക്കുക."
|
||||
|
||||
[settings]
|
||||
backToSections = "എല്ലാ ക്രമീകരണങ്ങളും"
|
||||
close = "അടയ്ക്കുക"
|
||||
title = "ക്രമീകരണങ്ങൾ"
|
||||
|
||||
[settings.ai]
|
||||
documents = "പ്രമാണങ്ങൾ & RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "തിരയുക"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "ടോളുകൾ തിരയുക..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "അഡ്വാൻസ്ഡ് ഫോർമാറ്റിംഗ്"
|
||||
ai = "AI"
|
||||
automation = "ഓട്ടോമേഷൻ"
|
||||
developerTools = "ഡെവലപ്പർ ടൂളുകൾ"
|
||||
documentReview = "ഡോക്യുമെന്റ് റിവ്യൂ"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "നിര {{index}}"
|
||||
convertToPdf = "PDF ആയി മാറിക്കുക"
|
||||
csvStats = "{{rows}} വരികൾ · {{columns}} നിരകൾ · {{size}}"
|
||||
emptyFile = "ശൂന്യ ഫയൽ"
|
||||
htmlHidePreview = "പ്രിവ്യൂ മറയ്ക്കുക"
|
||||
htmlPreview = "HTML പ്രിവ്യൂ"
|
||||
htmlPreviewMobileHidden = "HTML പേജുകൾ desktop വീതികൾക്കായി ക്രമീകരിച്ചിരിക്കുന്നതിനാൽ ഇവിടെ പ്രിവ്യൂ സ്ഥിരമായി ഓഫാണ്."
|
||||
htmlPreviewWarning = "HTML പ്രിവ്യൂ — ബാഹ്യ വിഭവങ്ങൾ ലോഡ് ചെയ്യാതിരിക്കാം · {{size}}"
|
||||
htmlShowPreview = "എങ്കിലും പ്രിവ്യൂ കാണിക്കുക"
|
||||
invalidJson = "അസാധുവായ JSON — അസംസ്കൃത ഉള്ളടക്കം കാണിക്കുന്നു"
|
||||
lineNumbers = "വരി നമ്പറുകൾ"
|
||||
loading = "ലോഡ് ചെയ്യുന്നു..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "PDF എക്സ്പോർട്ട് ചെയ്യുക"
|
||||
exportSelected = "തിരഞ്ഞെടുത്ത പേജുകൾ എക്സ്പോർട്ട് ചെയ്യുക"
|
||||
formFill = "ഫോം പൂരിപ്പിക്കുക"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "കൂടുതൽ പ്രവർത്തനങ്ങൾ"
|
||||
multiTool = "മൾട്ടി-ടൂൾ"
|
||||
panMode = "പാൻ മോഡ്"
|
||||
print = "PDF അച്ചടിക്കുക"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "എല്ലാം തിരഞ്ഞെടുക്കുക"
|
||||
selectByNumber = "പേജ് നമ്പർ പ്രകാരം തിരഞ്ഞെടുക്കുക"
|
||||
selectLanguage = "ഭാഷ തിരഞ്ഞെടുക്കുക"
|
||||
share = "പങ്കിടുക"
|
||||
showAllTools = "എല്ലാ ടൂളുകളും കാണിക്കുക"
|
||||
showFewerTools = "ടൂൾബാർ ചുരുക്കുക"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "അനോട്ടേഷനുകളുടെ ദൃശ്യത ടോഗിൾ ചെയ്യുക"
|
||||
toggleAttachments = "അറ്റാച്ച്മെന്റുകൾ ടോഗിൾ ചെയ്യുക"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Onwaar"
|
||||
fileNotSavedToDisk = "Niet op schijf opgeslagen"
|
||||
fileSavedToDisk = "Bestand opgeslagen op schijf"
|
||||
fileSelected = "Geselecteerd: {{filename}}"
|
||||
filesSelected = "Bestanden geselecteerd"
|
||||
filesSelected = "{{count}} bestanden"
|
||||
font = "Lettertype"
|
||||
fontSizeTooltip = "Grootte van de paginanummertekst in punten. Grotere getallen geven grotere tekst."
|
||||
fontTypeTooltip = "Lettertypefamilie voor de paginanummers. Kies passend bij de stijl van uw document."
|
||||
@@ -3969,6 +3969,7 @@ back = "Terug"
|
||||
backToFolder = "Terug naar {{folder}}"
|
||||
backToMyFiles = "Terug naar Mijn bestanden"
|
||||
breadcrumbs = "Mappad"
|
||||
bulkActions = "Acties"
|
||||
cancel = "Annuleren"
|
||||
classification = "Classificatie"
|
||||
clearSelection = "Selectie wissen"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Totale grootte"
|
||||
type = "Type"
|
||||
versionHistory = "Versietraject"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} filters actief"
|
||||
clearAll = "Filters wissen"
|
||||
label = "Filters"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Annuleren"
|
||||
error = "Kan map niet opslaan. Probeer het opnieuw."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Bestanden sorteren"
|
||||
modifiedAsc = "Oudste eerst"
|
||||
modifiedDesc = "Recent eerst"
|
||||
nameAsc = "Naam A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Alles opgeslagen"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Kon de wijzigingen niet toepassen"
|
||||
extractCsvError = "CSV extraheren mislukt"
|
||||
extractXlsxError = "XLSX extraheren mislukt"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Afvlakken na invullen"
|
||||
goToPage = "Ga naar deze pagina"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Pagina"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "verpl."
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Velden opnieuw scannen"
|
||||
rescanFormFields = "Formuliervelden opnieuw scannen"
|
||||
save = "Opslaan"
|
||||
saveShortcut = "Ctrl+S om op te slaan"
|
||||
skippedEdits_one = "1 wijziging kon niet worden toegepast:"
|
||||
skippedEdits_other = "{{count}} wijzigingen konden niet worden toegepast:"
|
||||
skippedEditsTruncated = "{{count}} meer niet vermeld."
|
||||
unsavedChanges = "Niet-opgeslagen wijzigingen"
|
||||
|
||||
[formFill.create]
|
||||
commit = "{{count}} veld(en) aan PDF toevoegen"
|
||||
empty = "Nog geen velden getekend."
|
||||
failed = "Velden toevoegen mislukt"
|
||||
goToField = "Ga naar dit veld"
|
||||
hint = "Kies een veldtype en teken het vervolgens op de pagina."
|
||||
placing = "Teken een {{type}}-veld op de pagina. Druk op Esc om te stoppen."
|
||||
preview = "Ingedrukt houden voor voorbeeldweergave"
|
||||
previewHelp = "Houd ingedrukt om de velden te zien zoals ze eruitzien nadat ze zijn toegevoegd, zonder de bewerkingscontouren."
|
||||
removeField = "Veld verwijderen"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Knopactie"
|
||||
actionHelp = "Wat de knop doet wanneer erop wordt geklikt."
|
||||
actionNone = "Geen"
|
||||
actionPrint = "Afdrukken"
|
||||
actionReset = "Formulier opnieuw instellen"
|
||||
actionSubmit = "Naar URL verzenden"
|
||||
actionUri = "URL openen"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "Het adres dat de knop opent of waarnaar deze verzendt."
|
||||
addOption = "Optie toevoegen"
|
||||
caption = "Knoptekst"
|
||||
captionHelp = "De tekst die op de knop wordt weergegeven."
|
||||
defaultValue = "Standaardwaarde"
|
||||
defaultValueHelp = "Wat het veld bevat voordat iemand het invult. Laat leeg voor een leeg veld."
|
||||
fontSize = "Lettergrootte"
|
||||
fontSizeHelp = "Tekstgrootte binnen het veld. Laat leeg zodat de PDF-lezer de grootte passend maakt."
|
||||
label = "Label"
|
||||
labelHelp = "De tekst die wordt getoond aan degene die het formulier invult. Laat leeg om terug te vallen op de veldnaam."
|
||||
maxLength = "Max. lengte (comb)"
|
||||
maxLengthHelp = "Beperkt hoeveel tekens passen, weergegeven als gelijkmatig verdeelde vakjes."
|
||||
multiline = "Meerregelig"
|
||||
multilineHelp = "Staat meer dan één tekstregel toe en breekt af aan de rand van het veld."
|
||||
multiSelect = "Meerdere selecties toestaan"
|
||||
multiSelectHelp = "Hiermee kan meer dan één optie tegelijk worden gekozen."
|
||||
name = "Veldnaam"
|
||||
nameHelp = "De interne naam van het veld. Wordt gebruikt bij het exporteren van gegevens of het invullen van het formulier vanuit een ander systeem, dus houd deze uniek en zonder spaties."
|
||||
optionGap = "Optieafstand"
|
||||
optionGapHelp = "Ruimte tussen knoppen, in punten. Laat leeg om ze gelijkmatig over het vak te verdelen."
|
||||
optionPlaceholder = "Optie {{n}}"
|
||||
options = "Opties"
|
||||
optionsEmpty = "Voeg ten minste één optie toe."
|
||||
optionsHelp = "De keuzes die in de lijst worden aangeboden. Elke keuze wordt opgeslagen zoals getypt, dus houd ze kort en onderscheidend."
|
||||
optionSize = "Optiegrootte"
|
||||
optionSizeHelp = "Breedte en hoogte van elke knop, in punten. Laat leeg om ze passend te maken in het vak dat je hebt getekend."
|
||||
readOnly = "Alleen-lezen"
|
||||
readOnlyHelp = "Toont een waarde maar voorkomt dat iemand deze bewerkt."
|
||||
removeOption = "Optie verwijderen"
|
||||
required = "Verplicht"
|
||||
requiredHelp = "Het formulier kan niet worden verzonden totdat dit veld is ingevuld."
|
||||
signatureNote = "Alleen een tijdelijke aanduiding - je ondertekent hier niet. Het markeert waar een handtekening hoort, zodat een PDF-ondertekenaar (Adobe Acrobat, een ondertekeningsservice, enz.) de handtekening op deze plek plaatst wanneer het document wordt ondertekend."
|
||||
tooltip = "Tooltip"
|
||||
tooltipHelp = "De hint die wordt getoond wanneer iemand met de muis over het veld beweegt in een PDF-lezer."
|
||||
type = "Type"
|
||||
typeHelp = "Welk soort veld dit is. Als je dit wijzigt, wordt het veld opnieuw opgebouwd, dus de huidige waarde wordt niet overgenomen."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Maken"
|
||||
fill = "Invullen"
|
||||
label = "Modus formuliereditor"
|
||||
modify = "Wijzigen"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "{{count}} wijziging(en) opslaan"
|
||||
delete = "Verwijderen"
|
||||
empty = "Deze PDF heeft nog geen formuliervelden."
|
||||
failed = "Wijzigingen opslaan mislukt"
|
||||
groupSizeHint = "Gebruik Optiegrootte"
|
||||
hint = "Selecteer een veld om de eigenschappen te bewerken, sleep het over de pagina of verwijder het."
|
||||
restore = "Herstellen"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Zijbalk sluiten"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Documentbeperkingen en machtigingen wijzigen"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Rechten wijzigen"
|
||||
|
||||
[home.classify]
|
||||
desc = "Bepaal wat voor soort document dit is en voorzie het van een tag."
|
||||
title = "Classificeren"
|
||||
|
||||
[home.compare]
|
||||
desc = "Vergelijkt en toont de verschillen tussen twee PDF-documenten"
|
||||
tags = "verschil"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Niets te melden."
|
||||
handoffUnavailable = "Deze browser staat niet toe dat de verwerker het document aan de editor doorgeeft. Open het in plaats daarvan vanuit de editor."
|
||||
noDocumentLinked = "Deze fout is niet gekoppeld aan een specifiek document, dus er is hier niets om te openen."
|
||||
notOnThisDevice = "Dit document staat niet op dit apparaat, dus het kan hier niet worden geopend."
|
||||
occurrences = "{{count}} keer"
|
||||
open = "Meldingen"
|
||||
title = "Meldingen"
|
||||
unread = "Ongelezen"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Dat werkte niet. Probeer het over een moment opnieuw."
|
||||
unavailable = "Niet beschikbaar voor deze melding."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Gekopieerd"
|
||||
copy = "Fout kopiëren"
|
||||
less = "Minder tonen"
|
||||
more = "Volledig bericht tonen"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Eerder"
|
||||
new = "Nieuw"
|
||||
|
||||
[oauth.error]
|
||||
message = "Authenticatie is niet geslaagd. U kunt dit venster sluiten en het opnieuw proberen."
|
||||
title = "Authenticatie mislukt"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "van {{allowance}} gratis PDF's gebruikt"
|
||||
capSuffix_other = "van {{allowance}} gratis PDF's gebruikt"
|
||||
eyebrow = "Processor-proefperiode"
|
||||
statusLabel_one = "{{remaining}} over"
|
||||
statusLabel_other = "{{remaining}} over"
|
||||
statusLabel_one = "{{used}} gebruikt"
|
||||
statusLabel_other = "{{used}} gebruikt"
|
||||
sub = "Gebruik de PDF Editor gratis. Betaal om PDF's automatisch te verwerken."
|
||||
title_one = "Verwerk {{allowance}} PDF's gratis"
|
||||
title_other = "Verwerk {{allowance}} PDF's gratis"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Bestand bekijken"
|
||||
viewInProcessor = "In verwerker bekijken"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Deze fout is niet geregistreerd voor een specifiek document, dus er is hier niets om te openen."
|
||||
unattended = "Dit bestand is aangeleverd door een map, bucket of webhook, dus er is geen browser van een gebruiker die het kan openen."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Bronnen"
|
||||
connectSource = "Bron verbinden"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Geavanceerd"
|
||||
back = "Terug naar bronnen"
|
||||
backToSource = "Terug naar bronconfiguratie"
|
||||
backToTypes = "Alle brontypen"
|
||||
cancel = "Annuleren"
|
||||
chooseHint = "Kies waar documenten vandaan komen. Grijs gemaakte connectors zijn onderweg."
|
||||
@@ -8726,7 +8845,6 @@ create = "Bron maken"
|
||||
createTitle = "Een bron verbinden"
|
||||
delete = "Verwijderen"
|
||||
editTitle = "Bron bewerken"
|
||||
enabled = "Ingeschakeld"
|
||||
save = "Wijzigingen opslaan"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: elk bestand één keer verwerken"
|
||||
snapshot = "Snapshot: de map bij elke uitvoering opnieuw lezen"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Neem submappen op als je bestanden zijn georganiseerd in geneste mappen, of bewaak alleen het bovenste niveau."
|
||||
label = "Mapdiepte"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Grootte, datum en inhoudscontrole"
|
||||
stat = "Grootte en wijzigingsdatum"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Of het oorspronkelijke bestand op zijn plek moet blijven nadat het is verwerkt. Als het oorspronkelijke bestand op zijn plek blijft, wordt het opnieuw verwerkt de volgende keer dat de pipeline de bron scant."
|
||||
label = "Leesmodus"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Consumeren: verwerk elk bestand één keer"
|
||||
snapshot = "Snapshot: lees de map bij elke uitvoering opnieuw"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Neem submappen op als je bestanden zijn georganiseerd in geneste mappen, of bewaak alleen het bovenste niveau."
|
||||
label = "Mapdiepte"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Je Stirling-PDF-server is offline en \"{{endpoint}}\"
|
||||
expired = "Uw sessie is verlopen. Voer de pagina opnieuw in en probeer het opnieuw."
|
||||
|
||||
[settings]
|
||||
backToSections = "Alle instellingen"
|
||||
close = "Sluiten"
|
||||
title = "Instellingen"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Documenten & RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Zoeken"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Tools zoeken..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Geavanceerde opmaak"
|
||||
ai = "AI"
|
||||
automation = "Automatisering"
|
||||
developerTools = "Ontwikkelaarstools"
|
||||
documentReview = "Documentreview"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Kolom {{index}}"
|
||||
convertToPdf = "Converteren naar PDF"
|
||||
csvStats = "{{rows}} rijen · {{columns}} kolommen · {{size}}"
|
||||
emptyFile = "Leeg bestand"
|
||||
htmlHidePreview = "Voorbeeldweergave verbergen"
|
||||
htmlPreview = "HTML-voorbeeld"
|
||||
htmlPreviewMobileHidden = "HTML-pagina's zijn ingedeeld voor desktopbreedtes, dus de voorbeeldweergave is hier standaard uitgeschakeld."
|
||||
htmlPreviewWarning = "HTML-voorbeeld — externe bronnen worden mogelijk niet geladen · {{size}}"
|
||||
htmlShowPreview = "Voorbeeldweergave toch tonen"
|
||||
invalidJson = "Ongeldige JSON — ruwe inhoud wordt weergegeven"
|
||||
lineNumbers = "Regelnummers"
|
||||
loading = "Laden..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "PDF exporteren"
|
||||
exportSelected = "Geselecteerde pagina's exporteren"
|
||||
formFill = "Formulier invullen"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Meer acties"
|
||||
multiTool = "Multi-tool"
|
||||
panMode = "Pan-modus"
|
||||
print = "PDF afdrukken"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Alles selecteren"
|
||||
selectByNumber = "Selecteren op paginanummers"
|
||||
selectLanguage = "Taal selecteren"
|
||||
share = "Delen"
|
||||
showAllTools = "Alle tools tonen"
|
||||
showFewerTools = "Werkbalk inklappen"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Annotaties tonen/verbergen"
|
||||
toggleAttachments = "Bijlagen tonen/verbergen"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Usann"
|
||||
fileNotSavedToDisk = "Ikke lagret til disk"
|
||||
fileSavedToDisk = "Fil lagret til disk"
|
||||
fileSelected = "Valgt: {{filename}}"
|
||||
filesSelected = "filer valgt"
|
||||
filesSelected = "{{count}} filer"
|
||||
font = "Skrifttype"
|
||||
fontSizeTooltip = "Størrelse på sidetalltekst i punkter. Større tall gir større tekst."
|
||||
fontTypeTooltip = "Skrifttype for sidetall. Velg basert på dokumentstilen din."
|
||||
@@ -3969,6 +3969,7 @@ back = "Tilbake"
|
||||
backToFolder = "Tilbake til {{folder}}"
|
||||
backToMyFiles = "Tilbake til Mine filer"
|
||||
breadcrumbs = "Mappesti"
|
||||
bulkActions = "Handlinger"
|
||||
cancel = "Avbryt"
|
||||
classification = "Klassifisering"
|
||||
clearSelection = "Tøm utvalg"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Total størrelse"
|
||||
type = "Type"
|
||||
versionHistory = "Versjonsforløp"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} filtre aktive"
|
||||
clearAll = "Fjern filtre"
|
||||
label = "Filtre"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Avbryt"
|
||||
error = "Kunne ikke lagre mappe. Prøv igjen."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Sorter filer"
|
||||
modifiedAsc = "Eldste først"
|
||||
modifiedDesc = "Nyeste først"
|
||||
nameAsc = "Navn A→Å"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Alt lagret"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Kunne ikke bruke endringene"
|
||||
extractCsvError = "Kunne ikke trekke ut CSV"
|
||||
extractXlsxError = "Kunne ikke trekke ut XLSX"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Flate ut etter utfylling"
|
||||
goToPage = "Gå til denne siden"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Side"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "obl"
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Skann felter på nytt"
|
||||
rescanFormFields = "Skann skjemafelter på nytt"
|
||||
save = "Lagre"
|
||||
saveShortcut = "Ctrl+S for å lagre"
|
||||
skippedEdits_one = "1 endring kunne ikke brukes:"
|
||||
skippedEdits_other = "{{count}} endringer kunne ikke brukes:"
|
||||
skippedEditsTruncated = "{{count}} flere ikke oppført."
|
||||
unsavedChanges = "Ulagrede endringer"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Legg til {{count}} felt i PDF"
|
||||
empty = "Ingen felt tegnet ennå."
|
||||
failed = "Kunne ikke legge til felt"
|
||||
goToField = "Gå til dette feltet"
|
||||
hint = "Velg en felttype, og tegn den deretter på siden."
|
||||
placing = "Tegn et {{type}}-felt på siden. Trykk Esc for å stoppe."
|
||||
preview = "Hold inne for forhåndsvisning"
|
||||
previewHelp = "Hold inne for å se feltene slik de vil se ut når de er lagt til, uten redigeringsomrissene."
|
||||
removeField = "Fjern felt"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Knappehandling"
|
||||
actionHelp = "Hva knappen gjør når den klikkes."
|
||||
actionNone = "Ingen"
|
||||
actionPrint = "Skriv ut"
|
||||
actionReset = "Tilbakestill skjema"
|
||||
actionSubmit = "Send til URL"
|
||||
actionUri = "Åpne URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "Adressen knappen åpner eller sender til."
|
||||
addOption = "Legg til alternativ"
|
||||
caption = "Knappetekst"
|
||||
captionHelp = "Teksten som vises på knappen."
|
||||
defaultValue = "Standardverdi"
|
||||
defaultValueHelp = "Hva feltet inneholder før noen fyller det ut. La stå tomt for et tomt felt."
|
||||
fontSize = "Skriftstørrelse"
|
||||
fontSizeHelp = "Tekststørrelse inne i feltet. La stå tomt for å la leseren tilpasse størrelsen."
|
||||
label = "Etikett"
|
||||
labelHelp = "Teksten som vises til den som fyller ut skjemaet. La den stå tom for å bruke feltnavnet som reserve."
|
||||
maxLength = "Maks lengde (comb)"
|
||||
maxLengthHelp = "Begrenser hvor mange tegn som får plass, tegnet som jevnt fordelte bokser."
|
||||
multiline = "Flere linjer"
|
||||
multilineHelp = "Tillater mer enn én tekstlinje og bryter ved feltets kant."
|
||||
multiSelect = "Tillat flere valg"
|
||||
multiSelectHelp = "Lar mer enn ett alternativ velges samtidig."
|
||||
name = "Feltnavn"
|
||||
nameHelp = "Feltets interne navn. Brukes ved eksport av data eller utfylling av skjemaet fra et annet system, så hold det unikt og uten mellomrom."
|
||||
optionGap = "Avstand mellom alternativer"
|
||||
optionGapHelp = "Avstand mellom knapper, i punkter. La stå tomt for å fordele dem jevnt nedover boksen."
|
||||
optionPlaceholder = "Alternativ {{n}}"
|
||||
options = "Alternativer"
|
||||
optionsEmpty = "Legg til minst ett alternativ."
|
||||
optionsHelp = "Valgene som tilbys i listen. Hvert lagres slik det skrives, så hold dem korte og tydelige."
|
||||
optionSize = "Alternativstørrelse"
|
||||
optionSizeHelp = "Bredde og høyde for hver knapp, i punkter. La stå tomt for å tilpasse dem til boksen du tegnet."
|
||||
readOnly = "Skrivebeskyttet"
|
||||
readOnlyHelp = "Viser en verdi, men hindrer redigering."
|
||||
removeOption = "Fjern alternativ"
|
||||
required = "Obligatorisk"
|
||||
requiredHelp = "Skjemaet kan ikke sendes inn før dette feltet er fylt ut."
|
||||
signatureNote = "Kun plassholder – du signerer ikke her. Den markerer hvor en signatur hører hjemme, slik at en PDF-signerer (Adobe Acrobat, en signeringstjeneste osv.) plasserer signaturen på dette stedet når dokumentet signeres."
|
||||
tooltip = "Verktøytips"
|
||||
tooltipHelp = "Hintet som vises når noen holder pekeren over feltet i en PDF-leser."
|
||||
type = "Type"
|
||||
typeHelp = "Hvilken type felt dette er. Hvis du endrer det, bygges feltet på nytt, så den gjeldende verdien beholdes ikke."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Opprett"
|
||||
fill = "Fyll ut"
|
||||
label = "Skjemaredigeringsmodus"
|
||||
modify = "Endre"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Lagre {{count}} endring(er)"
|
||||
delete = "Slett"
|
||||
empty = "Denne PDF har ingen skjemafelt ennå."
|
||||
failed = "Kunne ikke lagre endringer"
|
||||
groupSizeHint = "Bruk alternativstørrelse"
|
||||
hint = "Velg et felt for å redigere egenskapene, dra det på siden eller slett det."
|
||||
restore = "Gjenopprett"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Lukk sidepanel"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Endre dokumentbegrensninger og tillatelser"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Endre tillatelser"
|
||||
|
||||
[home.classify]
|
||||
desc = "Identifiser hvilken type dokument dette er, og merk det."
|
||||
title = "Klassifiser"
|
||||
|
||||
[home.compare]
|
||||
desc = "Sammenligner og viser forskjellene mellom to PDF-dokumenter"
|
||||
tags = "sammenlign"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Ingenting å rapportere."
|
||||
handoffUnavailable = "Denne nettleseren lar ikke behandleren overføre dokumentet til redigeringsprogrammet. Åpne det fra redigeringsprogrammet i stedet."
|
||||
noDocumentLinked = "Denne feilen er ikke knyttet til et bestemt dokument, så det er ingenting å åpne her."
|
||||
notOnThisDevice = "Dette dokumentet er ikke på denne enheten, så det kan ikke åpnes her."
|
||||
occurrences = "{{count}} ganger"
|
||||
open = "Varsler"
|
||||
title = "Varsler"
|
||||
unread = "Ulest"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Det fungerte ikke. Prøv igjen om et øyeblikk."
|
||||
unavailable = "Ikke tilgjengelig for dette varselet."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Kopiert"
|
||||
copy = "Kopier feil"
|
||||
less = "Vis mindre"
|
||||
more = "Vis hele meldingen"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Tidligere"
|
||||
new = "Nye"
|
||||
|
||||
[oauth.error]
|
||||
message = "Autentiseringen var ikke vellykket. Du kan lukke dette vinduet og prøve igjen."
|
||||
title = "Autentisering mislyktes"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "av {{allowance}} gratis PDF-er brukt"
|
||||
capSuffix_other = "av {{allowance}} gratis PDF-er brukt"
|
||||
eyebrow = "Processor-prøveperiode"
|
||||
statusLabel_one = "{{remaining}} igjen"
|
||||
statusLabel_other = "{{remaining}} igjen"
|
||||
statusLabel_one = "{{used}} brukt"
|
||||
statusLabel_other = "{{used}} brukt"
|
||||
sub = "Bruk PDF Editor gratis. Betal for å behandle PDF-er automatisk."
|
||||
title_one = "Behandle {{allowance}} PDF-er gratis"
|
||||
title_other = "Behandle {{allowance}} PDF-er gratis"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Vis fil"
|
||||
viewInProcessor = "Vis i behandler"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Denne feilen ble ikke registrert mot et bestemt dokument, så det er ingenting å åpne her."
|
||||
unattended = "Denne filen ble matet inn av en mappe, bucket eller webhook, så ingen nettleser holder den for åpning."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Kilder"
|
||||
connectSource = "Koble til kilde"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Avansert"
|
||||
back = "Tilbake til kilder"
|
||||
backToSource = "Tilbake til kildeoppsett"
|
||||
backToTypes = "Alle kildetyper"
|
||||
cancel = "Avbryt"
|
||||
chooseHint = "Velg hvor dokumenter kommer fra. Nedtonede tilkoblinger er på vei."
|
||||
@@ -8726,7 +8845,6 @@ create = "Opprett kilde"
|
||||
createTitle = "Koble til en kilde"
|
||||
delete = "Slett"
|
||||
editTitle = "Rediger kilde"
|
||||
enabled = "Aktivert"
|
||||
save = "Lagre endringer"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: behandle hver fil én gang"
|
||||
snapshot = "Snapshot: les mappen på nytt ved hver kjøring"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Inkluder undermapper hvis filene dine er organisert i nestede mapper, eller overvåk bare toppnivået."
|
||||
label = "Mappedybde"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Størrelse, dato og innholdskontroll"
|
||||
stat = "Størrelse og endringsdato"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Om den opprinnelige filen skal bli liggende etter at den er behandlet. Hvis den opprinnelige filen blir liggende, behandles den på nytt neste gang pipeline skanner kilden."
|
||||
label = "Lesemodus"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Konsumer: behandle hver fil én gang"
|
||||
snapshot = "Snapshot: les mappen på nytt ved hver kjøring"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Inkluder undermapper hvis filene dine er organisert i nestede mapper, eller overvåk bare toppnivået."
|
||||
label = "Mappedybde"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Stirling-PDF-serveren din er frakoblet og \"{{endpoin
|
||||
expired = "Økten din har utløpt. Vennligst oppdater siden og prøv igjen."
|
||||
|
||||
[settings]
|
||||
backToSections = "Alle innstillinger"
|
||||
close = "Lukk"
|
||||
title = "Innstillinger"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Dokumenter og RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Søk"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Søk i verktøy..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Avansert formatering"
|
||||
ai = "AI"
|
||||
automation = "Automatisering"
|
||||
developerTools = "Utviklerverktøy"
|
||||
documentReview = "Dokumentgjennomgang"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Kolonne {{index}}"
|
||||
convertToPdf = "Konverter til PDF"
|
||||
csvStats = "{{rows}} rader · {{columns}} kolonner · {{size}}"
|
||||
emptyFile = "Tom fil"
|
||||
htmlHidePreview = "Skjul forhåndsvisning"
|
||||
htmlPreview = "HTML-forhåndsvisning"
|
||||
htmlPreviewMobileHidden = "HTML-sider er utformet for skrivebordsbredder, så forhåndsvisningen er av som standard her."
|
||||
htmlPreviewWarning = "HTML-forhåndsvisning — eksterne ressurser kan hende ikke lastes · {{size}}"
|
||||
htmlShowPreview = "Vis forhåndsvisning likevel"
|
||||
invalidJson = "Ugyldig JSON — viser rått innhold"
|
||||
lineNumbers = "Linjenumre"
|
||||
loading = "Laster ..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Eksporter PDF"
|
||||
exportSelected = "Eksporter valgte sider"
|
||||
formFill = "Fyll ut skjema"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Flere handlinger"
|
||||
multiTool = "Multiverktøy"
|
||||
panMode = "Panoreringsmodus"
|
||||
print = "Skriv ut PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Velg alle"
|
||||
selectByNumber = "Velg etter sidenummer"
|
||||
selectLanguage = "Velg språk"
|
||||
share = "Del"
|
||||
showAllTools = "Vis alle verktøy"
|
||||
showFewerTools = "Skjul verktøylinje"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Veksle synlighet for annoteringer"
|
||||
toggleAttachments = "Veksle vedlegg"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Nie"
|
||||
fileNotSavedToDisk = "Nie zapisano na dysku"
|
||||
fileSavedToDisk = "Plik zapisano na dysku"
|
||||
fileSelected = "Wybrano: {{filename}}"
|
||||
filesSelected = "wybrane pliki"
|
||||
filesSelected = "{{count}} plików"
|
||||
font = "Czcionka"
|
||||
fontSizeTooltip = "Rozmiar tekstu numeru strony w punktach. Większe wartości tworzą większy tekst."
|
||||
fontTypeTooltip = "Rodzina czcionek dla numerów stron. Wybierz zgodnie ze stylem dokumentu."
|
||||
@@ -3969,6 +3969,7 @@ back = "Wstecz"
|
||||
backToFolder = "Wróć do {{folder}}"
|
||||
backToMyFiles = "Wróć do Moich plików"
|
||||
breadcrumbs = "Ścieżka folderu"
|
||||
bulkActions = "Akcje"
|
||||
cancel = "Anuluj"
|
||||
classification = "Klasyfikacja"
|
||||
clearSelection = "Wyczyść zaznaczenie"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Łączny rozmiar"
|
||||
type = "Typ"
|
||||
versionHistory = "Historia wersji"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "Aktywne filtry: {{count}}"
|
||||
clearAll = "Wyczyść filtry"
|
||||
label = "Filtry"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Anuluj"
|
||||
error = "Nie można było zapisać folderu. Spróbuj ponownie."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Sortuj pliki"
|
||||
modifiedAsc = "Najpierw najstarsze"
|
||||
modifiedDesc = "Najpierw najnowsze"
|
||||
nameAsc = "Nazwa A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Wszystko zapisano"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Nie udało się zastosować zmian"
|
||||
extractCsvError = "Nie udało się wyodrębnić CSV"
|
||||
extractXlsxError = "Nie udało się wyodrębnić XLSX"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Spłaszcz po wypełnieniu"
|
||||
goToPage = "Przejdź do tej strony"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Strona"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "wym."
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Skanuj pola ponownie"
|
||||
rescanFormFields = "Skanuj ponownie pola formularza"
|
||||
save = "Zapisz"
|
||||
saveShortcut = "Ctrl+S, aby zapisać"
|
||||
skippedEdits_one = "Nie udało się zastosować 1 zmiany:"
|
||||
skippedEdits_other = "Nie udało się zastosować {{count}} zmian:"
|
||||
skippedEditsTruncated = "{{count}} kolejnych nie wymieniono."
|
||||
unsavedChanges = "Niezapisane zmiany"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Dodaj {{count}} pól do PDF"
|
||||
empty = "Nie narysowano jeszcze żadnych pól."
|
||||
failed = "Nie udało się dodać pól"
|
||||
goToField = "Przejdź do tego pola"
|
||||
hint = "Wybierz typ pola, a następnie narysuj je na stronie."
|
||||
placing = "Narysuj pole {{type}} na stronie. Naciśnij Esc, aby zakończyć."
|
||||
preview = "Przytrzymaj, aby wyświetlić podgląd"
|
||||
previewHelp = "Przytrzymaj, aby zobaczyć pola tak, jak będą wyglądać po dodaniu, bez obrysów edycji."
|
||||
removeField = "Usuń pole"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Akcja przycisku"
|
||||
actionHelp = "Co robi przycisk po kliknięciu."
|
||||
actionNone = "Brak"
|
||||
actionPrint = "Drukuj"
|
||||
actionReset = "Resetuj formularz"
|
||||
actionSubmit = "Prześlij do URL"
|
||||
actionUri = "Otwórz URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "Adres, który przycisk otwiera lub do którego przesyła dane."
|
||||
addOption = "Dodaj opcję"
|
||||
caption = "Podpis przycisku"
|
||||
captionHelp = "Tekst wyświetlany na przycisku."
|
||||
defaultValue = "Wartość domyślna"
|
||||
defaultValueHelp = "Zawartość pola, zanim ktoś je wypełni. Pozostaw puste, aby pole było puste."
|
||||
fontSize = "Rozmiar czcionki"
|
||||
fontSizeHelp = "Rozmiar tekstu w polu. Pozostaw puste, aby czytnik dopasował rozmiar."
|
||||
label = "Etykieta"
|
||||
labelHelp = "Tekst wyświetlany osobie wypełniającej formularz. Pozostaw puste, aby użyć nazwy pola."
|
||||
maxLength = "Maks. długość (comb)"
|
||||
maxLengthHelp = "Ogranicza liczbę znaków, rysując je jako równomiernie rozmieszczone pola."
|
||||
multiline = "Wielowierszowe"
|
||||
multilineHelp = "Pozwala na więcej niż jeden wiersz tekstu i zawija go przy krawędzi pola."
|
||||
multiSelect = "Zezwól na wielokrotny wybór"
|
||||
multiSelectHelp = "Pozwala wybrać więcej niż jedną opcję naraz."
|
||||
name = "Nazwa pola"
|
||||
nameHelp = "Wewnętrzna nazwa pola. Używana podczas eksportowania danych lub wypełniania formularza z innego systemu, więc powinna być unikalna i bez spacji."
|
||||
optionGap = "Odstęp opcji"
|
||||
optionGapHelp = "Odstęp między przyciskami, w punktach. Pozostaw puste, aby rozmieścić je równomiernie w dół pola."
|
||||
optionPlaceholder = "Opcja {{n}}"
|
||||
options = "Opcje"
|
||||
optionsEmpty = "Dodaj co najmniej jedną opcję."
|
||||
optionsHelp = "Wybory oferowane na liście. Każdy jest zapisywany tak, jak wpisano, więc powinny być krótkie i odrębne."
|
||||
optionSize = "Rozmiar opcji"
|
||||
optionSizeHelp = "Szerokość i wysokość każdego przycisku, w punktach. Pozostaw puste, aby dopasować je do narysowanego pola."
|
||||
readOnly = "Tylko do odczytu"
|
||||
readOnlyHelp = "Wyświetla wartość, ale uniemożliwia jej edycję."
|
||||
removeOption = "Usuń opcję"
|
||||
required = "Wymagane"
|
||||
requiredHelp = "Formularza nie można przesłać, dopóki to pole nie zostanie wypełnione."
|
||||
signatureNote = "Tylko symbol zastępczy — tutaj nie składasz podpisu. Wskazuje miejsce na podpis, aby osoba podpisująca PDF (Adobe Acrobat, usługa podpisywania itp.) umieściła podpis w tym miejscu podczas podpisywania dokumentu."
|
||||
tooltip = "Podpowiedź"
|
||||
tooltipHelp = "Wskazówka wyświetlana, gdy ktoś najedzie kursorem na pole w czytniku PDF."
|
||||
type = "Typ"
|
||||
typeHelp = "Rodzaj tego pola. Zmiana przebudowuje pole, więc jego bieżąca wartość nie zostanie zachowana."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Utwórz"
|
||||
fill = "Wypełnij"
|
||||
label = "Tryb edytora formularzy"
|
||||
modify = "Modyfikuj"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Zapisz {{count}} zmian"
|
||||
delete = "Usuń"
|
||||
empty = "Ten PDF nie ma jeszcze pól formularza."
|
||||
failed = "Nie udało się zapisać zmian"
|
||||
groupSizeHint = "Użyj rozmiaru opcji"
|
||||
hint = "Wybierz pole, aby edytować jego właściwości, przeciągnij je na stronie lub usuń."
|
||||
restore = "Przywróć"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Zamknij pasek boczny"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Zmień ograniczenia i uprawnienia dokumentu"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Zmień uprawnienia"
|
||||
|
||||
[home.classify]
|
||||
desc = "Określ, jakiego rodzaju jest ten dokument, i oznacz go tagiem."
|
||||
title = "Klasyfikuj"
|
||||
|
||||
[home.compare]
|
||||
desc = "Porównuje i pokazuje różnice między dwoma dokumentami PDF"
|
||||
tags = "różnice"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Brak zgłoszeń."
|
||||
handoffUnavailable = "Ta przeglądarka nie pozwoli procesorowi przekazać dokumentu do edytora. Otwórz go zamiast tego z poziomu edytora."
|
||||
noDocumentLinked = "Ta awaria nie jest powiązana z konkretnym dokumentem, więc nie ma tu nic do otwarcia."
|
||||
notOnThisDevice = "Tego dokumentu nie ma na tym urządzeniu, więc nie można go tutaj otworzyć."
|
||||
occurrences = "{{count}} razy"
|
||||
open = "Powiadomienia"
|
||||
title = "Powiadomienia"
|
||||
unread = "Nieprzeczytane"
|
||||
|
||||
[notifications.action]
|
||||
failed = "To nie zadziałało. Spróbuj ponownie za chwilę."
|
||||
unavailable = "Niedostępne dla tego powiadomienia."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Skopiowano"
|
||||
copy = "Kopiuj błąd"
|
||||
less = "Pokaż mniej"
|
||||
more = "Pokaż pełny komunikat"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Wcześniejsze"
|
||||
new = "Nowe"
|
||||
|
||||
[oauth.error]
|
||||
message = "Uwierzytelnienie nie zakończyło się pomyślnie. Możesz zamknąć to okno i spróbować ponownie."
|
||||
title = "Uwierzytelnienie nie powiodło się"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "z {{allowance}} bezpłatnych PDF wykorzystano"
|
||||
capSuffix_other = "z {{allowance}} bezpłatnych PDF wykorzystano"
|
||||
eyebrow = "Okres próbny Processor"
|
||||
statusLabel_one = "Pozostało {{remaining}}"
|
||||
statusLabel_other = "Pozostało {{remaining}}"
|
||||
statusLabel_one = "Wykorzystano {{used}}"
|
||||
statusLabel_other = "Wykorzystano {{used}}"
|
||||
sub = "Korzystaj z edytora PDF bezpłatnie. Płać za automatyczne przetwarzanie PDF."
|
||||
title_one = "Przetwórz {{allowance}} PDF bezpłatnie"
|
||||
title_other = "Przetwórz {{allowance}} PDF bezpłatnie"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Wyświetl plik"
|
||||
viewInProcessor = "Wyświetl w procesorze"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Ta awaria nie została zarejestrowana dla konkretnego dokumentu, więc nie ma tu nic do otwarcia."
|
||||
unattended = "Ten plik został przekazany przez folder, bucket lub webhook, więc żadna przeglądarka nie przechowuje go do otwarcia."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Źródła"
|
||||
connectSource = "Połącz źródło"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Zaawansowane"
|
||||
back = "Wróć do źródeł"
|
||||
backToSource = "Wróć do konfiguracji źródła"
|
||||
backToTypes = "Wszystkie typy źródeł"
|
||||
cancel = "Anuluj"
|
||||
chooseHint = "Wybierz, skąd pochodzą dokumenty. Wyszarzone konektory są w przygotowaniu."
|
||||
@@ -8726,7 +8845,6 @@ create = "Utwórz źródło"
|
||||
createTitle = "Połącz źródło"
|
||||
delete = "Usuń"
|
||||
editTitle = "Edytuj źródło"
|
||||
enabled = "Włączone"
|
||||
save = "Zapisz zmiany"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consume: przetwórz każdy plik raz"
|
||||
snapshot = "Snapshot: odczytuj folder ponownie przy każdym uruchomieniu"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Uwzględnij podfoldery, jeśli pliki są uporządkowane w zagnieżdżonych folderach, albo obserwuj tylko najwyższy poziom."
|
||||
label = "Głębokość folderu"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Rozmiar, data i kontrola zawartości"
|
||||
stat = "Rozmiar i data modyfikacji"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Czy pozostawić oryginalny plik na miejscu po jego przetworzeniu. Jeśli oryginalny plik zostanie pozostawiony na miejscu, zostanie ponownie przetworzony przy następnym skanowaniu źródła przez pipeline."
|
||||
label = "Tryb odczytu"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Zużyj: przetwórz każdy plik raz"
|
||||
snapshot = "Migawka: ponownie odczytaj folder przy każdym uruchomieniu"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Uwzględnij podfoldery, jeśli pliki są uporządkowane w zagnieżdżonych folderach, albo obserwuj tylko najwyższy poziom."
|
||||
label = "Głębokość folderu"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Twój serwer Stirling-PDF jest offline, a \"{{endpoin
|
||||
expired = "Twoja sesja wygasła. Odśwież stronę i spróbuj ponownie."
|
||||
|
||||
[settings]
|
||||
backToSections = "Wszystkie ustawienia"
|
||||
close = "Zamknij"
|
||||
title = "Ustawienia"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Dokumenty i RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Szukaj"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Szukaj narzędzi..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Zaawansowane formatowanie"
|
||||
ai = "AI"
|
||||
automation = "Automatyzacja"
|
||||
developerTools = "Narzędzia programistyczne"
|
||||
documentReview = "Przegląd dokumentu"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Kolumna {{index}}"
|
||||
convertToPdf = "Konwertuj do PDF"
|
||||
csvStats = "{{rows}} wierszy · {{columns}} kolumn · {{size}}"
|
||||
emptyFile = "Pusty plik"
|
||||
htmlHidePreview = "Ukryj podgląd"
|
||||
htmlPreview = "Podgląd HTML"
|
||||
htmlPreviewMobileHidden = "Strony HTML są projektowane dla szerokości komputerów stacjonarnych, więc podgląd jest tutaj domyślnie wyłączony."
|
||||
htmlPreviewWarning = "Podgląd HTML — zasoby zewnętrzne mogą się nie załadować · {{size}}"
|
||||
htmlShowPreview = "Mimo to pokaż podgląd"
|
||||
invalidJson = "Nieprawidłowy JSON — wyświetlanie surowej zawartości"
|
||||
lineNumbers = "Numery wierszy"
|
||||
loading = "Wczytywanie..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Eksportuj PDF"
|
||||
exportSelected = "Eksportuj wybrane strony"
|
||||
formFill = "Wypełnij formularz"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Więcej akcji"
|
||||
multiTool = "Wielofunkcyjne narzędzie"
|
||||
panMode = "Tryb przesuwania"
|
||||
print = "Drukuj PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Zaznacz wszystko"
|
||||
selectByNumber = "Zaznacz według numerów stron"
|
||||
selectLanguage = "Wybierz język"
|
||||
share = "Udostępnij"
|
||||
showAllTools = "Pokaż wszystkie narzędzia"
|
||||
showFewerTools = "Zwiń pasek narzędzi"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Pokaż/ukryj adnotacje"
|
||||
toggleAttachments = "Pokaż/ukryj załączniki"
|
||||
|
||||
@@ -41,7 +41,7 @@ false = "Falso"
|
||||
fileNotSavedToDisk = "Não salvo no disco"
|
||||
fileSavedToDisk = "Arquivo salvo no disco"
|
||||
fileSelected = "Selecionado: {{filename}}"
|
||||
filesSelected = "Arquivos Selecionados"
|
||||
filesSelected = "{{count}} arquivos"
|
||||
font = "Fonte"
|
||||
fontSizeTooltip = "Tamanho do texto do número da página em pontos. Números maiores criam texto maior."
|
||||
fontTypeTooltip = "Família de fontes para os números de página. Escolha conforme o estilo do seu documento."
|
||||
@@ -3969,6 +3969,7 @@ back = "Voltar"
|
||||
backToFolder = "Voltar para {{folder}}"
|
||||
backToMyFiles = "Voltar para Meus arquivos"
|
||||
breadcrumbs = "Caminho da pasta"
|
||||
bulkActions = "Ações"
|
||||
cancel = "Cancelar"
|
||||
classification = "Classificação"
|
||||
clearSelection = "Limpar seleção"
|
||||
@@ -4142,6 +4143,11 @@ totalSize = "Tamanho total"
|
||||
type = "Tipo"
|
||||
versionHistory = "Jornada da versão"
|
||||
|
||||
[filesPage.filters]
|
||||
activeCount = "{{count}} filtros ativos"
|
||||
clearAll = "Limpar filtros"
|
||||
label = "Filtros"
|
||||
|
||||
[filesPage.folderName]
|
||||
cancel = "Cancelar"
|
||||
error = "Não foi possível salvar a pasta. Tente novamente."
|
||||
@@ -4176,6 +4182,7 @@ label = "Filter files by name"
|
||||
placeholder = "Filter files…"
|
||||
|
||||
[filesPage.sort]
|
||||
label = "Ordenar arquivos"
|
||||
modifiedAsc = "Mais antigo primeiro"
|
||||
modifiedDesc = "Mais recente primeiro"
|
||||
nameAsc = "Nome A→Z"
|
||||
@@ -4305,11 +4312,14 @@ issues = "GitHub"
|
||||
[formFill]
|
||||
allSaved = "Tudo salvo"
|
||||
analyzingFields = "Analysing form fields..."
|
||||
applyFailed = "Não foi possível aplicar as alterações"
|
||||
extractCsvError = "Falha ao extrair CSV"
|
||||
extractXlsxError = "Falha ao extrair XLSX"
|
||||
filled = "filled"
|
||||
flattenAfterFilling = "Achatar após o preenchimento"
|
||||
goToPage = "Ir para esta página"
|
||||
noFields = "No fillable form fields found in this PDF."
|
||||
page = "Página"
|
||||
placeholderEnter = "Enter"
|
||||
placeholderSelect = "Select"
|
||||
requiredAbbreviation = "obr"
|
||||
@@ -4318,8 +4328,83 @@ rescanFields = "Escanear campos novamente"
|
||||
rescanFormFields = "Escanear campos do formulário novamente"
|
||||
save = "Salvar"
|
||||
saveShortcut = "Ctrl+S para salvar"
|
||||
skippedEdits_one = "1 alteração não pôde ser aplicada:"
|
||||
skippedEdits_other = "{{count}} alterações não puderam ser aplicadas:"
|
||||
skippedEditsTruncated = "Mais {{count}} não listadas."
|
||||
unsavedChanges = "Alterações não salvas"
|
||||
|
||||
[formFill.create]
|
||||
commit = "Adicionar {{count}} campo(s) ao PDF"
|
||||
empty = "Nenhum campo desenhado ainda."
|
||||
failed = "Falha ao adicionar campos"
|
||||
goToField = "Ir para este campo"
|
||||
hint = "Escolha um tipo de campo e desenhe-o na página."
|
||||
placing = "Desenhe um campo {{type}} na página. Pressione Esc para parar."
|
||||
preview = "Segure para pré-visualizar"
|
||||
previewHelp = "Segure para ver os campos como ficarão depois de adicionados, sem os contornos de edição."
|
||||
removeField = "Remover campo"
|
||||
|
||||
[formFill.editor]
|
||||
action = "Ação do botão"
|
||||
actionHelp = "O que o botão faz ao ser clicado."
|
||||
actionNone = "Nenhuma"
|
||||
actionPrint = "Imprimir"
|
||||
actionReset = "Redefinir formulário"
|
||||
actionSubmit = "Enviar para URL"
|
||||
actionUri = "Abrir URL"
|
||||
actionUrl = "URL"
|
||||
actionUrlHelp = "O endereço que o botão abre ou para o qual envia."
|
||||
addOption = "Adicionar opção"
|
||||
caption = "Legenda do botão"
|
||||
captionHelp = "O texto impresso na face do botão."
|
||||
defaultValue = "Valor padrão"
|
||||
defaultValueHelp = "O que o campo contém antes de alguém preenchê-lo. Deixe em branco para um campo vazio."
|
||||
fontSize = "Tamanho da fonte"
|
||||
fontSizeHelp = "Tamanho do texto dentro do campo. Deixe em branco para permitir que o leitor ajuste o tamanho ao espaço."
|
||||
label = "Rótulo"
|
||||
labelHelp = "O texto exibido para quem preenche o formulário. Deixe em branco para usar o nome do campo."
|
||||
maxLength = "Comprimento máximo (comb)"
|
||||
maxLengthHelp = "Limita quantos caracteres cabem, desenhados como caixas espaçadas uniformemente."
|
||||
multiline = "Várias linhas"
|
||||
multilineHelp = "Permite mais de uma linha de texto e quebra o texto na borda do campo."
|
||||
multiSelect = "Permitir seleção múltipla"
|
||||
multiSelectHelp = "Permite que mais de uma opção seja escolhida ao mesmo tempo."
|
||||
name = "Nome do campo"
|
||||
nameHelp = "O nome interno do campo. Usado ao exportar dados ou preencher o formulário a partir de outro sistema, portanto mantenha-o único e sem espaços."
|
||||
optionGap = "Espaçamento das opções"
|
||||
optionGapHelp = "Espaço entre os botões, em pontos. Deixe em branco para distribuí-los uniformemente pela caixa."
|
||||
optionPlaceholder = "Opção {{n}}"
|
||||
options = "Opções"
|
||||
optionsEmpty = "Adicione pelo menos uma opção."
|
||||
optionsHelp = "As escolhas oferecidas na lista. Cada uma é armazenada como digitada, então mantenha-as curtas e distintas."
|
||||
optionSize = "Tamanho da opção"
|
||||
optionSizeHelp = "Largura e altura de cada botão, em pontos. Deixe em branco para ajustá-los à caixa que você desenhou."
|
||||
readOnly = "Somente leitura"
|
||||
readOnlyHelp = "Mostra um valor, mas impede que alguém o edite."
|
||||
removeOption = "Remover opção"
|
||||
required = "Obrigatório"
|
||||
requiredHelp = "O formulário não pode ser enviado até que este campo seja preenchido."
|
||||
signatureNote = "Apenas espaço reservado - você não assina aqui. Ele marca onde uma assinatura deve ficar, para que um assinador de PDF (Adobe Acrobat, um serviço de assinatura, etc.) coloque a assinatura neste local quando o documento for assinado."
|
||||
tooltip = "Tooltip"
|
||||
tooltipHelp = "A dica exibida quando alguém passa o cursor sobre o campo em um leitor de PDF."
|
||||
type = "Tipo"
|
||||
typeHelp = "Que tipo de campo é este. Alterá-lo recria o campo, então o valor atual não é mantido."
|
||||
|
||||
[formFill.mode]
|
||||
create = "Criar"
|
||||
fill = "Preencher"
|
||||
label = "Modo do editor de formulários"
|
||||
modify = "Modificar"
|
||||
|
||||
[formFill.modify]
|
||||
commit = "Salvar {{count}} alteração(ões)"
|
||||
delete = "Excluir"
|
||||
empty = "Este PDF ainda não tem campos de formulário."
|
||||
failed = "Falha ao salvar alterações"
|
||||
groupSizeHint = "Usar Tamanho da opção"
|
||||
hint = "Selecione um campo para editar suas propriedades, arraste-o na página ou exclua-o."
|
||||
restore = "Restaurar"
|
||||
|
||||
[formFill.sidebar]
|
||||
close = "Fechar barra lateral"
|
||||
|
||||
@@ -4585,6 +4670,10 @@ desc = "Alterar restrições e permissões do documento"
|
||||
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
|
||||
title = "Alterar Permissões"
|
||||
|
||||
[home.classify]
|
||||
desc = "Identifique que tipo de documento é este e marque-o."
|
||||
title = "Classificar"
|
||||
|
||||
[home.compare]
|
||||
desc = "Comparar e mostrar as diferenças entre dois documentos PDF."
|
||||
tags = "diferença"
|
||||
@@ -5098,6 +5187,30 @@ openProcessor = "Open PDF Processor"
|
||||
count = "{{remaining}} of {{total}}"
|
||||
label = "Free credits"
|
||||
|
||||
[notifications]
|
||||
empty = "Nada a relatar."
|
||||
handoffUnavailable = "Este navegador não permitirá que o processador passe o documento para o editor. Abra-o pelo editor em vez disso."
|
||||
noDocumentLinked = "Esta falha não está vinculada a um documento específico, então não há nada para abrir aqui."
|
||||
notOnThisDevice = "Este documento não está neste dispositivo, então não pode ser aberto aqui."
|
||||
occurrences = "{{count}} vezes"
|
||||
open = "Notificações"
|
||||
title = "Notificações"
|
||||
unread = "Não lidas"
|
||||
|
||||
[notifications.action]
|
||||
failed = "Isso não funcionou. Tente novamente em alguns instantes."
|
||||
unavailable = "Não disponível para esta notificação."
|
||||
|
||||
[notifications.detail]
|
||||
copied = "Copiado"
|
||||
copy = "Copiar erro"
|
||||
less = "Mostrar menos"
|
||||
more = "Mostrar mensagem completa"
|
||||
|
||||
[notifications.section]
|
||||
earlier = "Anteriores"
|
||||
new = "Novas"
|
||||
|
||||
[oauth.error]
|
||||
message = "A autenticação não foi bem-sucedida. Você pode fechar esta janela e tentar novamente."
|
||||
title = "Falha na autenticação"
|
||||
@@ -6735,8 +6848,8 @@ barAria = "Free PDFs remaining"
|
||||
capSuffix_one = "de {{allowance}} PDFs gratuitos usados"
|
||||
capSuffix_other = "de {{allowance}} PDFs gratuitos usados"
|
||||
eyebrow = "Teste do Processor"
|
||||
statusLabel_one = "{{remaining}} restante"
|
||||
statusLabel_other = "{{remaining}} restantes"
|
||||
statusLabel_one = "{{used}} usado"
|
||||
statusLabel_other = "{{used}} usados"
|
||||
sub = "Use o Editor de PDF gratuitamente. Pague para processar PDFs automaticamente."
|
||||
title_one = "Processe {{allowance}} PDFs grátis"
|
||||
title_other = "Processe {{allowance}} PDFs grátis"
|
||||
@@ -7417,6 +7530,8 @@ acknowledge = "Acknowledge"
|
||||
confirm = "Are you sure?"
|
||||
dismiss = "Dismiss"
|
||||
dismissSkipFile = "Skip this file"
|
||||
viewFile = "Visualizar arquivo"
|
||||
viewInProcessor = "Visualizar no processador"
|
||||
|
||||
[portal.failures.debug]
|
||||
copyJson = "Copy JSON"
|
||||
@@ -7428,6 +7543,8 @@ showJson = "Show raw JSON ({{total}})"
|
||||
|
||||
[portal.failures.disabled]
|
||||
closed = "This failure is already closed."
|
||||
noDocument = "Esta falha não foi registrada em um documento específico, então não há nada aqui para abrir."
|
||||
unattended = "Este arquivo foi alimentado por uma pasta, bucket ou webhook, então nenhum navegador o está mantendo para abrir."
|
||||
unavailable = "Not available for this failure."
|
||||
|
||||
[portal.failures.empty]
|
||||
@@ -8716,7 +8833,9 @@ title = "Fontes"
|
||||
connectSource = "Conectar fonte"
|
||||
|
||||
[portal.sources.builder]
|
||||
advanced = "Avançado"
|
||||
back = "Voltar para fontes"
|
||||
backToSource = "Voltar para a configuração da origem"
|
||||
backToTypes = "Todos os tipos de fonte"
|
||||
cancel = "Cancelar"
|
||||
chooseHint = "Escolha de onde vêm os documentos. Conectores esmaecidos estão a caminho."
|
||||
@@ -8726,7 +8845,6 @@ create = "Criar fonte"
|
||||
createTitle = "Conectar uma fonte"
|
||||
delete = "Excluir"
|
||||
editTitle = "Editar fonte"
|
||||
enabled = "Ativado"
|
||||
save = "Salvar alterações"
|
||||
|
||||
[portal.sources.builder.folderAccess]
|
||||
@@ -8763,6 +8881,7 @@ consume = "Consumir: processar cada arquivo uma vez"
|
||||
snapshot = "Snapshot: reler a pasta a cada execução"
|
||||
|
||||
[portal.sources.networkFields.recursive]
|
||||
helperText = "Inclua subpastas se seus arquivos estiverem organizados em pastas aninhadas, ou monitore apenas o nível superior."
|
||||
label = "Profundidade da pasta"
|
||||
|
||||
[portal.sources.networkFields.recursive.options]
|
||||
@@ -8816,6 +8935,7 @@ hash = "Tamanho, data e verificação de conteúdo"
|
||||
stat = "Tamanho e data modificada"
|
||||
|
||||
[portal.sources.types.folder.fields.mode]
|
||||
helperText = "Se deve deixar o arquivo original no local depois de processado. Se o arquivo original for deixado no local, ele será reprocessado na próxima vez que o pipeline verificar a origem."
|
||||
label = "Modo de leitura"
|
||||
|
||||
[portal.sources.types.folder.fields.mode.options]
|
||||
@@ -8823,6 +8943,7 @@ consume = "Consumir: processar cada arquivo uma vez"
|
||||
snapshot = "Snapshot: reler a pasta a cada execução"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive]
|
||||
helperText = "Inclua subpastas se seus arquivos estiverem organizados em pastas aninhadas, ou monitore apenas o nível superior."
|
||||
label = "Profundidade da pasta"
|
||||
|
||||
[portal.sources.types.folder.fields.recursive.options]
|
||||
@@ -9696,7 +9817,9 @@ toolNotAvailableLocally = "Your Stirling-PDF server is offline and \"{{endpoint}
|
||||
expired = "Your session has expired. Please refresh the page and try again."
|
||||
|
||||
[settings]
|
||||
backToSections = "Todas as configurações"
|
||||
close = "Close"
|
||||
title = "Configurações"
|
||||
|
||||
[settings.ai]
|
||||
documents = "Documentos e RAG"
|
||||
@@ -10635,6 +10758,7 @@ ariaLabel = "Super search"
|
||||
filtersAriaLabel = "Search filters"
|
||||
hint = "Type to search"
|
||||
placeholder = "Search Stirling"
|
||||
placeholderShort = "Pesquisar"
|
||||
showLess = "Show less"
|
||||
showMore = "Show {{count}} more"
|
||||
|
||||
@@ -10831,6 +10955,7 @@ searchPlaceholder = "Search tools..."
|
||||
|
||||
[toolPicker.subcategories]
|
||||
advancedFormatting = "Advanced Formatting"
|
||||
ai = "AI"
|
||||
automation = "Automation"
|
||||
developerTools = "Developer Tools"
|
||||
documentReview = "Document Review"
|
||||
@@ -11335,8 +11460,11 @@ columnDefault = "Column {{index}}"
|
||||
convertToPdf = "Convert to PDF"
|
||||
csvStats = "{{rows}} rows · {{columns}} columns · {{size}}"
|
||||
emptyFile = "Empty file"
|
||||
htmlHidePreview = "Ocultar pré-visualização"
|
||||
htmlPreview = "HTML preview"
|
||||
htmlPreviewMobileHidden = "Páginas HTML são organizadas para larguras de desktop, então a pré-visualização fica desativada por padrão aqui."
|
||||
htmlPreviewWarning = "HTML preview - external resources may not load · {{size}}"
|
||||
htmlShowPreview = "Mostrar pré-visualização mesmo assim"
|
||||
invalidJson = "Invalid JSON - showing raw content"
|
||||
lineNumbers = "Line numbers"
|
||||
loading = "Loading..."
|
||||
@@ -11683,6 +11811,7 @@ exportAll = "Export PDF"
|
||||
exportSelected = "Export Selected Pages"
|
||||
formFill = "Fill Form"
|
||||
hideToolbar = "Hide toolbar"
|
||||
moreActions = "Mais ações"
|
||||
multiTool = "Multi-Tool"
|
||||
panMode = "Pan Mode"
|
||||
print = "Print PDF"
|
||||
@@ -11703,6 +11832,8 @@ selectAll = "Select All"
|
||||
selectByNumber = "Select by Page Numbers"
|
||||
selectLanguage = "Select language"
|
||||
share = "Compartilhar"
|
||||
showAllTools = "Mostrar todas as ferramentas"
|
||||
showFewerTools = "Recolher barra de ferramentas"
|
||||
showToolbar = "Show toolbar"
|
||||
toggleAnnotations = "Toggle Annotations Visibility"
|
||||
toggleAttachments = "Toggle Attachments"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user