Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6439d352d2 | |||
| a33cfb2b7d | |||
| 590adb9967 | |||
| 9e817e646f | |||
| 5594586941 | |||
| 5db66fc279 | |||
| 561389da69 | |||
| cd7d61966c | |||
| 29f6dc8896 | |||
| 6c83e8c477 | |||
| 29015903ba | |||
| 709e671168 | |||
| 473c9f225e | |||
| 90a389ac25 | |||
| 6106546a32 | |||
| ad82191ce1 | |||
| 6080d56a5c | |||
| 854985bcfa | |||
| 1bf7c0bb51 | |||
| bc409480e7 | |||
| 3cb74e152b | |||
| fbc4d700d5 | |||
| 40d9965b28 | |||
| 28e670d989 | |||
| dc12713be1 | |||
| 7bf58e7ab0 | |||
| c502b80fa9 | |||
| c23a62184b | |||
| 327119c828 | |||
| 488a3bba5f | |||
| 7f3c6db4d8 | |||
| 884569fc76 | |||
| 0960624e85 | |||
| 4063ca39c9 | |||
| f6476db767 | |||
| 142d8d7679 | |||
| 5bd5993a24 | |||
| 18cd544fab | |||
| 88f9f279e6 | |||
| dc1653ba3a | |||
| 3ea4aee4d6 | |||
| f447307359 | |||
| 4892e04e9b | |||
| 579b1efc68 | |||
| a16aa10fed | |||
| ea9adc8978 | |||
| fd7c886997 | |||
| b3a63dbd86 | |||
| 7ac8eb96c4 | |||
| c43a90716b | |||
| 0a08f4832e | |||
| bc375e3c59 | |||
| d3ebfd67dd | |||
| c1cfd184ee | |||
| e580126b8d | |||
| 86fca5d060 | |||
| c365118cf7 | |||
| 6f2d10c436 | |||
| 093004b724 | |||
| d2c783b06c | |||
| 7e2dd3afe6 | |||
| 4ace959667 | |||
| 14f31954d1 | |||
| 907daf78bf | |||
| cf6386b471 | |||
| 76df99f3a3 | |||
| 57e3eaecd9 | |||
| 21d3a5a61c |
@@ -9,7 +9,9 @@
|
||||
],
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {},
|
||||
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
|
||||
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
|
||||
"moby": false
|
||||
}
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
@@ -28,4 +30,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
image: mcr.microsoft.com/devcontainers/go:1.23
|
||||
image: mcr.microsoft.com/devcontainers/go:1-trixie # https://www.debian.org/releases/trixie/index.en.html
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
command: sleep infinity
|
||||
@@ -11,10 +10,10 @@ services:
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD=test123
|
||||
db:
|
||||
image: postgres:15
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
- postgres-data:/var/lib/postgresql
|
||||
hostname: postgres
|
||||
environment:
|
||||
POSTGRES_DB: miniflux2
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
name: Build Binaries
|
||||
permissions:
|
||||
contents: read
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
@@ -10,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up Golang
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
@@ -21,7 +23,7 @@ jobs:
|
||||
CGO_ENABLED: 0
|
||||
run: make build
|
||||
- name: Upload binaries
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: binaries
|
||||
path: miniflux-*
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Mirror to Codeberg
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
delete:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Mirror to Codeberg
|
||||
env:
|
||||
CODEBERG_USERNAME: ${{ secrets.CODEBERG_USERNAME }}
|
||||
CODEBERG_TOKEN: ${{ secrets.CODEBERG_TOKEN }}
|
||||
run: |
|
||||
git remote add codeberg https://${{ secrets.CODEBERG_USERNAME }}:${{ secrets.CODEBERG_TOKEN }}@codeberg.org/miniflux/v2.git
|
||||
git push --force --prune codeberg \
|
||||
"refs/heads/*:refs/heads/*" \
|
||||
"refs/tags/*:refs/tags/*"
|
||||
@@ -9,6 +9,7 @@ on:
|
||||
- '**.js'
|
||||
- '**.go'
|
||||
- '!**_test.go'
|
||||
- '.github/workflows/codeql-analysis.yml'
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
@@ -16,12 +17,14 @@ on:
|
||||
- '**.js'
|
||||
- '**.go'
|
||||
- '!**_test.go'
|
||||
- '.github/workflows/codeql-analysis.yml'
|
||||
schedule:
|
||||
- cron: '45 22 * * 3'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
name: Analyze (${{ matrix.language }})
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
@@ -30,20 +33,27 @@ jobs:
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go', 'javascript' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
if: matrix.language == 'go'
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
name: Test Packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up QEMU
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
name: Build Packages Manually
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up QEMU
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
- name: Build Debian Packages
|
||||
run: make debian-packages
|
||||
- name: Upload package
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: packages
|
||||
path: "*.deb"
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
name: Publish Packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up QEMU
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
name: Javascript Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install linters
|
||||
run: |
|
||||
sudo npm install -g jshint@2.13.6 eslint@8.57.0
|
||||
@@ -25,11 +25,11 @@ jobs:
|
||||
name: Golang Linters
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: stable
|
||||
- uses: golangci/golangci-lint-action@v8
|
||||
- uses: golangci/golangci-lint-action@v9
|
||||
- name: Run gofmt linter
|
||||
run: gofmt -d -e .
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
name: Commit Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
|
||||
@@ -11,17 +11,18 @@ on:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'packaging/rpm/**' # Only run on changes to the rpm packaging files
|
||||
- '.github/workflows/rpm_packages.yml'
|
||||
jobs:
|
||||
test-package:
|
||||
if: github.event_name == 'schedule' || github.event_name == 'pull_request'
|
||||
name: Test Packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build RPM Package
|
||||
run: make rpm
|
||||
run: make rpm VERSION=2.2.x_dev
|
||||
- name: List generated files
|
||||
run: ls -l *.rpm
|
||||
build-package-manually:
|
||||
@@ -29,13 +30,13 @@ jobs:
|
||||
name: Build Packages Manually
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build RPM Package
|
||||
run: make rpm
|
||||
- name: Upload package
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: packages
|
||||
path: "*.rpm"
|
||||
@@ -46,7 +47,7 @@ jobs:
|
||||
name: Publish Packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build RPM Package
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
|
||||
@@ -717,6 +717,32 @@ func (c *Client) UpdateFeedContext(ctx context.Context, feedID int64, feedChange
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// ImportFeedEntry imports a single entry into a feed.
|
||||
func (c *Client) ImportFeedEntry(feedID int64, payload any) (int64, error) {
|
||||
ctx, cancel := withDefaultTimeout()
|
||||
defer cancel()
|
||||
|
||||
body, err := c.request.Post(
|
||||
ctx,
|
||||
fmt.Sprintf("/v1/feeds/%d/entries/import", feedID),
|
||||
payload,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var response struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(body).Decode(&response); err != nil {
|
||||
return 0, fmt.Errorf("miniflux: json error (%v)", err)
|
||||
}
|
||||
|
||||
return response.ID, nil
|
||||
}
|
||||
|
||||
// MarkFeedAsRead marks all unread entries of the feed as read.
|
||||
func (c *Client) MarkFeedAsRead(feedID int64) error {
|
||||
ctx, cancel := withDefaultTimeout()
|
||||
|
||||
@@ -26,7 +26,7 @@ services:
|
||||
- POSTGRES_PASSWORD=secret
|
||||
- POSTGRES_DB=miniflux
|
||||
volumes:
|
||||
- miniflux-db:/var/lib/postgresql/data
|
||||
- miniflux-db:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "miniflux"]
|
||||
interval: 10s
|
||||
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
- POSTGRES_USER=miniflux
|
||||
- POSTGRES_PASSWORD=secret
|
||||
volumes:
|
||||
- miniflux-db:/var/lib/postgresql/data
|
||||
- miniflux-db:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "miniflux"]
|
||||
interval: 10s
|
||||
|
||||
@@ -43,7 +43,7 @@ services:
|
||||
- POSTGRES_USER=miniflux
|
||||
- POSTGRES_PASSWORD=secret
|
||||
volumes:
|
||||
- miniflux-db:/var/lib/postgresql/data
|
||||
- miniflux-db:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "miniflux"]
|
||||
interval: 10s
|
||||
|
||||
@@ -3,25 +3,25 @@ module miniflux.app/v2
|
||||
// +heroku goVersion go1.24
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.3
|
||||
github.com/PuerkitoBio/goquery v1.11.0
|
||||
github.com/andybalholm/brotli v1.2.0
|
||||
github.com/coreos/go-oidc/v3 v3.16.0
|
||||
github.com/go-webauthn/webauthn v0.14.0
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/go-webauthn/webauthn v0.15.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/tdewolff/minify/v2 v2.24.4
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/image v0.32.0
|
||||
golang.org/x/net v0.46.0
|
||||
golang.org/x/oauth2 v0.32.0
|
||||
golang.org/x/term v0.36.0
|
||||
github.com/tdewolff/minify/v2 v2.24.8
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/image v0.34.0
|
||||
golang.org/x/net v0.48.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/term v0.38.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-webauthn/x v0.1.25 // indirect
|
||||
github.com/go-webauthn/x v0.1.26 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/google/go-tpm v0.9.5 // indirect
|
||||
github.com/google/go-tpm v0.9.6 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -30,18 +30,18 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.4 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.5 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
@@ -8,8 +8,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
|
||||
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -17,17 +17,19 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-webauthn/webauthn v0.14.0 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx2QnhL0=
|
||||
github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k=
|
||||
github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88=
|
||||
github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-webauthn/webauthn v0.15.0 h1:LR1vPv62E0/6+sTenX35QrCmpMCzLeVAcnXeH4MrbJY=
|
||||
github.com/go-webauthn/webauthn v0.15.0/go.mod h1:hcAOhVChPRG7oqG7Xj6XKN1mb+8eXTGP/B7zBLzkX5A=
|
||||
github.com/go-webauthn/x v0.1.26 h1:eNzreFKnwNLDFoywGh9FA8YOMebBWTUNlNSdolQRebs=
|
||||
github.com/go-webauthn/x v0.1.26/go.mod h1:jmf/phPV6oIsF6hmdVre+ovHkxjDOmNH0t6fekWUxvg=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
|
||||
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
|
||||
github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
@@ -42,8 +44,6 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -60,10 +60,10 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tdewolff/minify/v2 v2.24.4 h1:pQyr6eWDa+RXtAoZg+6wurh0jB9ojqw/qc5LlU7/z6c=
|
||||
github.com/tdewolff/minify/v2 v2.24.4/go.mod h1:iD9Qn7/brhKY9d0KLKMkZrqS8/bqxSxRKruBi7V6m+w=
|
||||
github.com/tdewolff/parse/v2 v2.8.4 h1:A6slgBLGGDPBMGA28KQZfHpaKffuNvhOe7zSag+x/rw=
|
||||
github.com/tdewolff/parse/v2 v2.8.4/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/minify/v2 v2.24.8 h1:58/VjsbevI4d5FGV0ZSuBrHMSSkH4MCH0sIz/eKIauE=
|
||||
github.com/tdewolff/minify/v2 v2.24.8/go.mod h1:0Ukj0CRpo/sW/nd8uZ4ccXaV1rEVIWA3dj8U7+Shhfw=
|
||||
github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvwYU=
|
||||
github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
|
||||
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
@@ -83,10 +83,10 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
|
||||
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
@@ -101,10 +101,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -123,8 +123,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -134,8 +134,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@@ -145,8 +145,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
||||
@@ -62,6 +62,7 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
|
||||
sr.HandleFunc("/export", handler.exportFeeds).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/import", handler.importFeeds).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/feeds/{feedID}/entries", handler.getFeedEntries).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/feeds/{feedID}/entries/import", handler.importFeedEntry).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/feeds/{feedID}/entries/{entryID}", handler.getFeedEntry).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/entries", handler.getEntries).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/entries", handler.setEntryStatus).Methods(http.MethodPut)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"testing"
|
||||
|
||||
miniflux "miniflux.app/v2/client"
|
||||
"miniflux.app/v2/internal/model"
|
||||
)
|
||||
|
||||
const skipIntegrationTestsMessage = `Set TEST_MINIFLUX_* environment variables to run the API integration tests`
|
||||
@@ -2932,3 +2933,55 @@ func TestFlushHistoryEndpoint(t *testing.T) {
|
||||
t.Fatalf(`Invalid total, got %d`, readEntries.Total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportFeedEntryEndpoint(t *testing.T) {
|
||||
testConfig := newIntegrationTestConfig()
|
||||
if !testConfig.isConfigured() {
|
||||
t.Skip(skipIntegrationTestsMessage)
|
||||
}
|
||||
|
||||
client := miniflux.NewClient(
|
||||
testConfig.testBaseURL,
|
||||
testConfig.testAdminUsername,
|
||||
testConfig.testAdminPassword,
|
||||
)
|
||||
|
||||
// Create a feed
|
||||
feedID, err := client.CreateFeed(&miniflux.FeedCreationRequest{
|
||||
FeedURL: testConfig.testFeedURL,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.DeleteFeed(feedID)
|
||||
|
||||
payload := map[string]any{
|
||||
"title": "Imported Entry",
|
||||
"url": "https://example.org/imported-entry",
|
||||
"content": "Hello world",
|
||||
"external_id": "integration-test-entry-1",
|
||||
"status": model.EntryStatusUnread,
|
||||
"starred": false,
|
||||
"published_at": 0,
|
||||
}
|
||||
|
||||
// First import
|
||||
firstID, err := client.ImportFeedEntry(feedID, payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if firstID == 0 {
|
||||
t.Fatal("expected non-zero entry ID on first import")
|
||||
}
|
||||
|
||||
// Second import (same payload)
|
||||
secondID, err := client.ImportFeedEntry(feedID, payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if secondID != firstID {
|
||||
t.Fatalf("expected same entry ID on re-import, got %d and %d", firstID, secondID)
|
||||
}
|
||||
}
|
||||
|
||||
+112
-5
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/crypto"
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/integration"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/reader/processor"
|
||||
"miniflux.app/v2/internal/reader/readingtime"
|
||||
"miniflux.app/v2/internal/reader/sanitizer"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
"miniflux.app/v2/internal/validator"
|
||||
)
|
||||
@@ -274,6 +276,11 @@ func (h *handler) updateEntry(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if entryUpdateRequest.Content != nil {
|
||||
sanitizedContent := sanitizer.SanitizeHTML(entry.URL, *entryUpdateRequest.Content, &sanitizer.SanitizerOptions{OpenLinksInNewTab: user.OpenExternalLinksInNewTab})
|
||||
entryUpdateRequest.Content = &sanitizedContent
|
||||
}
|
||||
|
||||
entryUpdateRequest.Patch(entry)
|
||||
if user.ShowReadingTime {
|
||||
entry.ReadingTime = readingtime.EstimateReadingTime(entry.Content, user.DefaultReadingSpeed, user.CJKReadingSpeed)
|
||||
@@ -287,6 +294,110 @@ func (h *handler) updateEntry(w http.ResponseWriter, r *http.Request) {
|
||||
json.Created(w, r, entry)
|
||||
}
|
||||
|
||||
func (h *handler) importFeedEntry(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
|
||||
if feedID <= 0 {
|
||||
json.BadRequest(w, r, errors.New("invalid feed ID"))
|
||||
return
|
||||
}
|
||||
|
||||
if !h.store.FeedExists(userID, feedID) {
|
||||
json.BadRequest(w, r, errors.New("feed does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
var req EntryImportRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if req.URL == "" {
|
||||
json.BadRequest(w, r, errors.New("url is required"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Status == "" {
|
||||
req.Status = model.EntryStatusRead
|
||||
}
|
||||
|
||||
if err := validator.ValidateEntryStatus(req.Status); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
entry := model.NewEntry()
|
||||
entry.URL = req.URL
|
||||
entry.CommentsURL = req.CommentsURL
|
||||
entry.Author = req.Author
|
||||
entry.Tags = req.Tags
|
||||
|
||||
if req.PublishedAt > 0 {
|
||||
entry.Date = time.Unix(req.PublishedAt, 0).UTC()
|
||||
} else {
|
||||
entry.Date = time.Now().UTC()
|
||||
}
|
||||
|
||||
if req.Title == "" {
|
||||
entry.Title = entry.URL
|
||||
} else {
|
||||
entry.Title = req.Title
|
||||
}
|
||||
|
||||
hashInput := req.ExternalID
|
||||
if hashInput == "" {
|
||||
hashInput = req.URL
|
||||
}
|
||||
entry.Hash = crypto.HashFromBytes([]byte(hashInput))
|
||||
|
||||
user, err := h.store.UserByID(userID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Content != "" {
|
||||
entry.Content = sanitizer.SanitizeHTML(entry.URL, req.Content, &sanitizer.SanitizerOptions{OpenLinksInNewTab: user.OpenExternalLinksInNewTab})
|
||||
}
|
||||
|
||||
if user.ShowReadingTime {
|
||||
entry.ReadingTime = readingtime.EstimateReadingTime(entry.Content, user.DefaultReadingSpeed, user.CJKReadingSpeed)
|
||||
}
|
||||
|
||||
created, err := h.store.InsertEntryForFeed(userID, feedID, entry)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.SetEntriesStatus(userID, []int64{entry.ID}, req.Status); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
entry.Status = req.Status
|
||||
|
||||
if req.Starred {
|
||||
if err := h.store.SetEntriesStarredState(userID, []int64{entry.ID}, true); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
entry.Starred = true
|
||||
}
|
||||
|
||||
if created {
|
||||
json.Created(w, r, map[string]int64{"id": entry.ID})
|
||||
} else {
|
||||
json.OK(w, r, map[string]int64{"id": entry.ID})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
|
||||
loggedUserID := request.UserID(r)
|
||||
entryID := request.RouteInt64Param(r, "entryID")
|
||||
@@ -341,13 +452,9 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.OK(w, r, map[string]any{"content": mediaproxy.RewriteDocumentWithRelativeProxyURL(h.router, entry.Content), "reading_time": entry.ReadingTime})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
json.OK(w, r, map[string]string{"content": entry.Content})
|
||||
json.OK(w, r, map[string]any{"content": mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content), "reading_time": entry.ReadingTime})
|
||||
}
|
||||
|
||||
func (h *handler) flushHistory(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -18,6 +18,20 @@ type entriesResponse struct {
|
||||
Entries model.Entries `json:"entries"`
|
||||
}
|
||||
|
||||
// EntryImportRequest represents a manually imported entry for a feed.
|
||||
type EntryImportRequest struct {
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Author string `json:"author"`
|
||||
CommentsURL string `json:"comments_url"`
|
||||
PublishedAt int64 `json:"published_at"`
|
||||
Status string `json:"status"`
|
||||
Starred bool `json:"starred"`
|
||||
Tags []string `json:"tags"`
|
||||
ExternalID string `json:"external_id"`
|
||||
}
|
||||
|
||||
type feedCreationResponse struct {
|
||||
FeedID int64 `json:"feed_id"`
|
||||
}
|
||||
|
||||
+8
-2
@@ -30,9 +30,9 @@ const (
|
||||
flagDebugModeHelp = "Show debug logs"
|
||||
flagConfigFileHelp = "Load configuration file"
|
||||
flagConfigDumpHelp = "Print parsed configuration values"
|
||||
flagHealthCheckHelp = `Perform a health check on the given endpoint (the value "auto" try to guess the health check endpoint).`
|
||||
flagHealthCheckHelp = `Perform a health check on the given endpoint (the value "auto" tries to guess the health check endpoint).`
|
||||
flagRefreshFeedsHelp = "Refresh a batch of feeds and exit"
|
||||
flagRunCleanupTasksHelp = "Run cleanup tasks (delete old sessions and archives old entries)"
|
||||
flagRunCleanupTasksHelp = "Run cleanup tasks (delete old sessions and archive old entries)"
|
||||
flagExportUserFeedsHelp = "Export user feeds (provide the username as argument)"
|
||||
flagResetNextCheckAtHelp = "Reset the next check time for all feeds"
|
||||
)
|
||||
@@ -109,6 +109,12 @@ func Parse() {
|
||||
}
|
||||
}
|
||||
|
||||
if config.Opts.AuthProxyHeader() != "" {
|
||||
if len(config.Opts.TrustedReverseProxyNetworks()) == 0 {
|
||||
printErrorAndExit(errors.New("TRUSTED_REVERSE_PROXY_NETWORKS must be configured when AUTH_PROXY_HEADER is used"))
|
||||
}
|
||||
}
|
||||
|
||||
if flagConfigDump {
|
||||
fmt.Print(config.Opts)
|
||||
return
|
||||
|
||||
+442
-404
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,10 @@
|
||||
|
||||
package config // import "miniflux.app/v2/internal/config"
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBaseURLOptionParsing(t *testing.T) {
|
||||
configParser := NewConfigParser()
|
||||
@@ -1364,6 +1367,30 @@ func TestHTTPClientTimeoutOptionParsing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIconFetchAllowPrivateNetworksOptionParsing(t *testing.T) {
|
||||
configParser := NewConfigParser()
|
||||
|
||||
if configParser.options.IconFetchAllowPrivateNetworks() {
|
||||
t.Fatalf("Expected ICON_FETCH_ALLOW_PRIVATE_NETWORKS to be disabled by default")
|
||||
}
|
||||
|
||||
if err := configParser.parseLines([]string{"ICON_FETCH_ALLOW_PRIVATE_NETWORKS=1"}); err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !configParser.options.IconFetchAllowPrivateNetworks() {
|
||||
t.Fatalf("Expected ICON_FETCH_ALLOW_PRIVATE_NETWORKS to be enabled")
|
||||
}
|
||||
|
||||
if err := configParser.parseLines([]string{"ICON_FETCH_ALLOW_PRIVATE_NETWORKS=0"}); err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if configParser.options.IconFetchAllowPrivateNetworks() {
|
||||
t.Fatalf("Expected ICON_FETCH_ALLOW_PRIVATE_NETWORKS to be disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPServerTimeoutOptionParsing(t *testing.T) {
|
||||
configParser := NewConfigParser()
|
||||
|
||||
@@ -1431,6 +1458,30 @@ func TestMediaProxyHTTPClientTimeoutOptionParsing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaProxyAllowPrivateNetworksOptionParsing(t *testing.T) {
|
||||
configParser := NewConfigParser()
|
||||
|
||||
if configParser.options.MediaProxyAllowPrivateNetworks() {
|
||||
t.Fatalf("Expected MEDIA_PROXY_ALLOW_PRIVATE_NETWORKS to be disabled by default")
|
||||
}
|
||||
|
||||
if err := configParser.parseLines([]string{"MEDIA_PROXY_ALLOW_PRIVATE_NETWORKS=1"}); err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !configParser.options.MediaProxyAllowPrivateNetworks() {
|
||||
t.Fatalf("Expected MEDIA_PROXY_ALLOW_PRIVATE_NETWORKS to be enabled")
|
||||
}
|
||||
|
||||
if err := configParser.parseLines([]string{"MEDIA_PROXY_ALLOW_PRIVATE_NETWORKS=0"}); err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if configParser.options.MediaProxyAllowPrivateNetworks() {
|
||||
t.Fatalf("Expected MEDIA_PROXY_ALLOW_PRIVATE_NETWORKS to be disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaProxyPrivateKeyOptionParsing(t *testing.T) {
|
||||
configParser := NewConfigParser()
|
||||
|
||||
@@ -1584,6 +1635,32 @@ func TestSchedulerRoundRobinMinIntervalOptionParsing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustedReverseProxyNetworksOptionParsing(t *testing.T) {
|
||||
configParser := NewConfigParser()
|
||||
|
||||
// Test default value
|
||||
defaultNetworks := configParser.options.TrustedReverseProxyNetworks()
|
||||
if len(defaultNetworks) != 0 {
|
||||
t.Fatalf("Expected 0 allowed networks by default, got %d", len(defaultNetworks))
|
||||
}
|
||||
|
||||
// Test valid value
|
||||
if err := configParser.parseLines([]string{"TRUSTED_REVERSE_PROXY_NETWORKS=10.0.0.0/8,192.168.1.0/24"}); err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
allowedNetworks := configParser.options.TrustedReverseProxyNetworks()
|
||||
if len(allowedNetworks) != 2 {
|
||||
t.Fatalf("Expected 2 allowed networks, got %d", len(allowedNetworks))
|
||||
}
|
||||
if !slices.Contains(allowedNetworks, "10.0.0.0/8") {
|
||||
t.Errorf("Expected 10.0.0.0/8 in allowed networks")
|
||||
}
|
||||
if !slices.Contains(allowedNetworks, "192.168.1.0/24") {
|
||||
t.Errorf("Expected 192.168.1.0/24 in allowed networks")
|
||||
}
|
||||
}
|
||||
|
||||
func TestYouTubeEmbedDomainOptionParsing(t *testing.T) {
|
||||
configParser := NewConfigParser()
|
||||
|
||||
@@ -1614,8 +1691,8 @@ func TestSetLogLevelFunction(t *testing.T) {
|
||||
if configParser.options.LogLevel() != "debug" {
|
||||
t.Fatalf("Expected LOG_LEVEL to be 'debug' after SetLogLevel('debug'), got '%s'", configParser.options.LogLevel())
|
||||
}
|
||||
if configParser.options.options["LOG_LEVEL"].RawValue != "debug" {
|
||||
t.Fatalf("Expected LOG_LEVEL RawValue to be 'debug', got '%s'", configParser.options.options["LOG_LEVEL"].RawValue)
|
||||
if configParser.options.options["LOG_LEVEL"].rawValue != "debug" {
|
||||
t.Fatalf("Expected LOG_LEVEL RawValue to be 'debug', got '%s'", configParser.options.options["LOG_LEVEL"].rawValue)
|
||||
}
|
||||
|
||||
// Test setting log level to warning
|
||||
@@ -1623,8 +1700,8 @@ func TestSetLogLevelFunction(t *testing.T) {
|
||||
if configParser.options.LogLevel() != "warning" {
|
||||
t.Fatalf("Expected LOG_LEVEL to be 'warning' after SetLogLevel('warning'), got '%s'", configParser.options.LogLevel())
|
||||
}
|
||||
if configParser.options.options["LOG_LEVEL"].RawValue != "warning" {
|
||||
t.Fatalf("Expected LOG_LEVEL RawValue to be 'warning', got '%s'", configParser.options.options["LOG_LEVEL"].RawValue)
|
||||
if configParser.options.options["LOG_LEVEL"].rawValue != "warning" {
|
||||
t.Fatalf("Expected LOG_LEVEL RawValue to be 'warning', got '%s'", configParser.options.options["LOG_LEVEL"].rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+39
-39
@@ -51,7 +51,7 @@ func (cp *configParser) ParseFile(filename string) (*configOptions, error) {
|
||||
|
||||
func (cp *configParser) postParsing() error {
|
||||
// Parse basePath and rootURL based on BASE_URL
|
||||
baseURL := cp.options.options["BASE_URL"].ParsedStringValue
|
||||
baseURL := cp.options.options["BASE_URL"].parsedStringValue
|
||||
baseURL = strings.TrimSuffix(baseURL, "/")
|
||||
|
||||
parsedURL, err := url.Parse(baseURL)
|
||||
@@ -64,14 +64,14 @@ func (cp *configParser) postParsing() error {
|
||||
return errors.New("BASE_URL scheme must be http or https")
|
||||
}
|
||||
|
||||
cp.options.options["BASE_URL"].ParsedStringValue = baseURL
|
||||
cp.options.options["BASE_URL"].parsedStringValue = baseURL
|
||||
cp.options.basePath = parsedURL.Path
|
||||
|
||||
parsedURL.Path = ""
|
||||
cp.options.rootURL = parsedURL.String()
|
||||
|
||||
// Parse YouTube embed domain based on YOUTUBE_EMBED_URL_OVERRIDE
|
||||
youTubeEmbedURLOverride := cp.options.options["YOUTUBE_EMBED_URL_OVERRIDE"].ParsedStringValue
|
||||
youTubeEmbedURLOverride := cp.options.options["YOUTUBE_EMBED_URL_OVERRIDE"].parsedStringValue
|
||||
if youTubeEmbedURLOverride != "" {
|
||||
parsedYouTubeEmbedURL, err := url.Parse(youTubeEmbedURLOverride)
|
||||
if err != nil {
|
||||
@@ -81,16 +81,16 @@ func (cp *configParser) postParsing() error {
|
||||
}
|
||||
|
||||
// Generate a media proxy private key if not set
|
||||
if len(cp.options.options["MEDIA_PROXY_PRIVATE_KEY"].ParsedBytesValue) == 0 {
|
||||
if len(cp.options.options["MEDIA_PROXY_PRIVATE_KEY"].parsedBytesValue) == 0 {
|
||||
randomKey := make([]byte, 16)
|
||||
rand.Read(randomKey)
|
||||
cp.options.options["MEDIA_PROXY_PRIVATE_KEY"].ParsedBytesValue = randomKey
|
||||
cp.options.options["MEDIA_PROXY_PRIVATE_KEY"].parsedBytesValue = randomKey
|
||||
}
|
||||
|
||||
// Override LISTEN_ADDR with PORT if set (for compatibility reasons)
|
||||
if cp.options.Port() != "" {
|
||||
cp.options.options["LISTEN_ADDR"].ParsedStringList = []string{":" + cp.options.Port()}
|
||||
cp.options.options["LISTEN_ADDR"].RawValue = ":" + cp.options.Port()
|
||||
cp.options.options["LISTEN_ADDR"].parsedStringList = []string{":" + cp.options.Port()}
|
||||
cp.options.options["LISTEN_ADDR"].rawValue = ":" + cp.options.Port()
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -124,68 +124,68 @@ func (cp *configParser) parseLine(key, value string) error {
|
||||
}
|
||||
|
||||
// Validate the option if a validator is provided
|
||||
if field.Validator != nil {
|
||||
if err := field.Validator(value); err != nil {
|
||||
if field.validator != nil {
|
||||
if err := field.validator(value); err != nil {
|
||||
return fmt.Errorf("invalid value for key %s: %v", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the raw value based on its type
|
||||
switch field.ValueType {
|
||||
switch field.valueType {
|
||||
case stringType:
|
||||
field.ParsedStringValue = parseStringValue(value, field.ParsedStringValue)
|
||||
field.RawValue = value
|
||||
field.parsedStringValue = parseStringValue(value, field.parsedStringValue)
|
||||
field.rawValue = value
|
||||
case stringListType:
|
||||
field.ParsedStringList = parseStringListValue(value, field.ParsedStringList)
|
||||
field.RawValue = value
|
||||
field.parsedStringList = parseStringListValue(value, field.parsedStringList)
|
||||
field.rawValue = value
|
||||
case boolType:
|
||||
parsedValue, err := parseBoolValue(value, field.ParsedBoolValue)
|
||||
parsedValue, err := parseBoolValue(value, field.parsedBoolValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid boolean value for key %s: %v", key, err)
|
||||
}
|
||||
field.ParsedBoolValue = parsedValue
|
||||
field.RawValue = value
|
||||
field.parsedBoolValue = parsedValue
|
||||
field.rawValue = value
|
||||
case intType:
|
||||
field.ParsedIntValue = parseIntValue(value, field.ParsedIntValue)
|
||||
field.RawValue = value
|
||||
field.parsedIntValue = parseIntValue(value, field.parsedIntValue)
|
||||
field.rawValue = value
|
||||
case int64Type:
|
||||
field.ParsedInt64Value = ParsedInt64Value(value, field.ParsedInt64Value)
|
||||
field.RawValue = value
|
||||
field.parsedInt64Value = ParsedInt64Value(value, field.parsedInt64Value)
|
||||
field.rawValue = value
|
||||
case secondType:
|
||||
field.ParsedDuration = parseDurationValue(value, time.Second, field.ParsedDuration)
|
||||
field.RawValue = value
|
||||
field.parsedDuration = parseDurationValue(value, time.Second, field.parsedDuration)
|
||||
field.rawValue = value
|
||||
case minuteType:
|
||||
field.ParsedDuration = parseDurationValue(value, time.Minute, field.ParsedDuration)
|
||||
field.RawValue = value
|
||||
field.parsedDuration = parseDurationValue(value, time.Minute, field.parsedDuration)
|
||||
field.rawValue = value
|
||||
case hourType:
|
||||
field.ParsedDuration = parseDurationValue(value, time.Hour, field.ParsedDuration)
|
||||
field.RawValue = value
|
||||
field.parsedDuration = parseDurationValue(value, time.Hour, field.parsedDuration)
|
||||
field.rawValue = value
|
||||
case dayType:
|
||||
field.ParsedDuration = parseDurationValue(value, time.Hour*24, field.ParsedDuration)
|
||||
field.RawValue = value
|
||||
field.parsedDuration = parseDurationValue(value, time.Hour*24, field.parsedDuration)
|
||||
field.rawValue = value
|
||||
case urlType:
|
||||
parsedURL, err := parseURLValue(value, field.ParsedURLValue)
|
||||
parsedURL, err := parseURLValue(value, field.parsedURLValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid URL for key %s: %v", key, err)
|
||||
}
|
||||
field.ParsedURLValue = parsedURL
|
||||
field.RawValue = value
|
||||
field.parsedURLValue = parsedURL
|
||||
field.rawValue = value
|
||||
case secretFileType:
|
||||
secretValue, err := readSecretFileValue(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading secret file for key %s: %v", key, err)
|
||||
}
|
||||
if field.TargetKey != "" {
|
||||
if targetField, ok := cp.options.options[field.TargetKey]; ok {
|
||||
targetField.ParsedStringValue = secretValue
|
||||
targetField.RawValue = secretValue
|
||||
if field.targetKey != "" {
|
||||
if targetField, ok := cp.options.options[field.targetKey]; ok {
|
||||
targetField.parsedStringValue = secretValue
|
||||
targetField.rawValue = secretValue
|
||||
}
|
||||
}
|
||||
field.RawValue = value
|
||||
field.rawValue = value
|
||||
case bytesType:
|
||||
if value != "" {
|
||||
field.ParsedBytesValue = []byte(value)
|
||||
field.RawValue = value
|
||||
field.parsedBytesValue = []byte(value)
|
||||
field.rawValue = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ func validateChoices(rawValue string, choices []string) error {
|
||||
|
||||
func validateListChoices(inputValues, choices []string) error {
|
||||
for _, value := range inputValues {
|
||||
if !slices.Contains(choices, value) {
|
||||
return fmt.Errorf("value must be one of: %v", strings.Join(choices, ", "))
|
||||
if err := validateChoices(value, choices); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -1373,4 +1373,34 @@ var migrations = [...]func(tx *sql.Tx) error{
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN linkwarden_collection_id int;
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN readeck_push_enabled bool default 'f';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
// There is no need to keep an index on the content of deleted entries.
|
||||
_, err = tx.Exec(`DROP INDEX document_vectors_idx;`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sql := `
|
||||
CREATE INDEX document_vectors_idx
|
||||
ON entries
|
||||
USING gin(document_vectors)
|
||||
WHERE status != 'removed';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
@@ -47,9 +47,7 @@ func Serve(router *mux.Router, store *storage.Storage) {
|
||||
|
||||
middleware := newMiddleware(store)
|
||||
sr := router.PathPrefix("/reader/api/0").Subrouter()
|
||||
sr.Use(middleware.handleCORS)
|
||||
sr.Use(middleware.apiKeyAuth)
|
||||
sr.Methods(http.MethodOptions)
|
||||
sr.HandleFunc("/token", handler.tokenHandler).Methods(http.MethodGet).Name("Token")
|
||||
sr.HandleFunc("/edit-tag", handler.editTagHandler).Methods(http.MethodPost).Name("EditTag")
|
||||
sr.HandleFunc("/rename-tag", handler.renameTagHandler).Methods(http.MethodPost).Name("Rename Tag")
|
||||
@@ -244,7 +242,6 @@ func (h *handler) tokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.Int64("user_id", request.UserID(r)),
|
||||
slog.String("token", token),
|
||||
)
|
||||
|
||||
w.Header().Add("Content-Type", "text/plain; charset=UTF-8")
|
||||
@@ -1003,16 +1000,16 @@ func (h *handler) userInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
|
||||
if err := checkOutputFormat(r); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.store.UserByID(request.UserID(r))
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := userInfoResponse{UserID: strconv.FormatInt(user.ID, 10), UserName: user.Username, UserProfileID: strconv.FormatInt(user.ID, 10), UserEmail: user.Username}
|
||||
json.OK(w, r, userInfo)
|
||||
}
|
||||
|
||||
@@ -25,19 +25,6 @@ func newMiddleware(s *storage.Storage) *middleware {
|
||||
return &middleware{s}
|
||||
}
|
||||
|
||||
func (m *middleware) handleCORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization")
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
clientIP := request.ClientIP(r)
|
||||
|
||||
@@ -9,19 +9,46 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsTrustedIP checks if the given remote IP belongs to one of the trusted networks.
|
||||
func IsTrustedIP(remoteIP string, trustedNetworks []string) bool {
|
||||
if remoteIP == "@" || strings.HasPrefix(remoteIP, "/") {
|
||||
return true
|
||||
}
|
||||
|
||||
ip := net.ParseIP(remoteIP)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, cidr := range trustedNetworks {
|
||||
_, network, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if network.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// FindClientIP returns the client real IP address based on trusted Reverse-Proxy HTTP headers.
|
||||
func FindClientIP(r *http.Request) string {
|
||||
headers := [...]string{"X-Forwarded-For", "X-Real-Ip"}
|
||||
for _, header := range headers {
|
||||
value := r.Header.Get(header)
|
||||
func FindClientIP(r *http.Request, isTrustedProxyClient bool) string {
|
||||
if isTrustedProxyClient {
|
||||
headers := [...]string{"X-Forwarded-For", "X-Real-Ip"}
|
||||
for _, header := range headers {
|
||||
value := r.Header.Get(header)
|
||||
|
||||
if value != "" {
|
||||
addresses := strings.Split(value, ",")
|
||||
address := strings.TrimSpace(addresses[0])
|
||||
address = dropIPv6zone(address)
|
||||
if value != "" {
|
||||
addresses := strings.Split(value, ",")
|
||||
address := strings.TrimSpace(addresses[0])
|
||||
address = dropIPv6zone(address)
|
||||
|
||||
if net.ParseIP(address) != nil {
|
||||
return address
|
||||
if net.ParseIP(address) != nil {
|
||||
return address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,27 +10,27 @@ import (
|
||||
|
||||
func TestFindClientIPWithoutHeaders(t *testing.T) {
|
||||
r := &http.Request{RemoteAddr: "192.168.0.1:4242"}
|
||||
if ip := FindClientIP(r); ip != "192.168.0.1" {
|
||||
if ip := FindClientIP(r, false); ip != "192.168.0.1" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
|
||||
r = &http.Request{RemoteAddr: "192.168.0.1"}
|
||||
if ip := FindClientIP(r); ip != "192.168.0.1" {
|
||||
if ip := FindClientIP(r, false); ip != "192.168.0.1" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
|
||||
r = &http.Request{RemoteAddr: "fe80::14c2:f039:edc7:edc7"}
|
||||
if ip := FindClientIP(r); ip != "fe80::14c2:f039:edc7:edc7" {
|
||||
if ip := FindClientIP(r, false); ip != "fe80::14c2:f039:edc7:edc7" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
|
||||
r = &http.Request{RemoteAddr: "fe80::14c2:f039:edc7:edc7%eth0"}
|
||||
if ip := FindClientIP(r); ip != "fe80::14c2:f039:edc7:edc7" {
|
||||
if ip := FindClientIP(r, false); ip != "fe80::14c2:f039:edc7:edc7" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
|
||||
r = &http.Request{RemoteAddr: "[fe80::14c2:f039:edc7:edc7%eth0]:4242"}
|
||||
if ip := FindClientIP(r); ip != "fe80::14c2:f039:edc7:edc7" {
|
||||
if ip := FindClientIP(r, false); ip != "fe80::14c2:f039:edc7:edc7" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func TestFindClientIPWithXFFHeader(t *testing.T) {
|
||||
headers.Set("X-Forwarded-For", "203.0.113.195, 70.41.3.18, 150.172.238.178")
|
||||
r := &http.Request{RemoteAddr: "192.168.0.1:4242", Header: headers}
|
||||
|
||||
if ip := FindClientIP(r); ip != "203.0.113.195" {
|
||||
if ip := FindClientIP(r, true); ip != "203.0.113.195" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestFindClientIPWithXFFHeader(t *testing.T) {
|
||||
headers.Set("X-Forwarded-For", "2001:db8:85a3:8d3:1319:8a2e:370:7348")
|
||||
r = &http.Request{RemoteAddr: "192.168.0.1:4242", Header: headers}
|
||||
|
||||
if ip := FindClientIP(r); ip != "2001:db8:85a3:8d3:1319:8a2e:370:7348" {
|
||||
if ip := FindClientIP(r, true); ip != "2001:db8:85a3:8d3:1319:8a2e:370:7348" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestFindClientIPWithXFFHeader(t *testing.T) {
|
||||
headers.Set("X-Forwarded-For", "fe80::14c2:f039:edc7:edc7%eth0")
|
||||
r = &http.Request{RemoteAddr: "192.168.0.1:4242", Header: headers}
|
||||
|
||||
if ip := FindClientIP(r); ip != "fe80::14c2:f039:edc7:edc7" {
|
||||
if ip := FindClientIP(r, true); ip != "fe80::14c2:f039:edc7:edc7" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func TestFindClientIPWithXFFHeader(t *testing.T) {
|
||||
headers.Set("X-Forwarded-For", "70.41.3.18")
|
||||
r = &http.Request{RemoteAddr: "192.168.0.1:4242", Header: headers}
|
||||
|
||||
if ip := FindClientIP(r); ip != "70.41.3.18" {
|
||||
if ip := FindClientIP(r, true); ip != "70.41.3.18" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func TestFindClientIPWithXFFHeader(t *testing.T) {
|
||||
headers.Set("X-Forwarded-For", "fake IP")
|
||||
r = &http.Request{RemoteAddr: "192.168.0.1:4242", Header: headers}
|
||||
|
||||
if ip := FindClientIP(r); ip != "192.168.0.1" {
|
||||
if ip := FindClientIP(r, true); ip != "192.168.0.1" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func TestClientIPWithXRealIPHeader(t *testing.T) {
|
||||
headers.Set("X-Real-Ip", "192.168.122.1")
|
||||
r := &http.Request{RemoteAddr: "192.168.0.1:4242", Header: headers}
|
||||
|
||||
if ip := FindClientIP(r); ip != "192.168.122.1" {
|
||||
if ip := FindClientIP(r, true); ip != "192.168.122.1" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func TestClientIPWithBothHeaders(t *testing.T) {
|
||||
|
||||
r := &http.Request{RemoteAddr: "192.168.0.1:4242", Header: headers}
|
||||
|
||||
if ip := FindClientIP(r); ip != "203.0.113.195" {
|
||||
if ip := FindClientIP(r, true); ip != "203.0.113.195" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@ func TestClientIPWithBothHeaders(t *testing.T) {
|
||||
func TestClientIPWithUnixSocketRemoteAddress(t *testing.T) {
|
||||
r := &http.Request{RemoteAddr: "@"}
|
||||
|
||||
if ip := FindClientIP(r); ip != "@" {
|
||||
if ip := FindClientIP(r, false); ip != "@" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,39 @@ func TestClientIPWithUnixSocketRemoteAddrAndBothHeaders(t *testing.T) {
|
||||
|
||||
r := &http.Request{RemoteAddr: "@", Header: headers}
|
||||
|
||||
if ip := FindClientIP(r); ip != "203.0.113.195" {
|
||||
if ip := FindClientIP(r, true); ip != "203.0.113.195" {
|
||||
t.Fatalf(`Unexpected result, got: %q`, ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTrustedIP(t *testing.T) {
|
||||
trustedNetworks := []string{"127.0.0.1/8", "10.0.0.0/8", "::1/128", "invalid"}
|
||||
|
||||
scenarios := []struct {
|
||||
ip string
|
||||
expected bool
|
||||
}{
|
||||
{"127.0.0.1", true},
|
||||
{"10.0.0.1", true},
|
||||
{"::1", true},
|
||||
{"192.168.1.1", false},
|
||||
{"invalid", false},
|
||||
{"@", true},
|
||||
{"/tmp/miniflux.sock", true},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
result := IsTrustedIP(scenario.ip, trustedNetworks)
|
||||
if result != scenario.expected {
|
||||
t.Errorf("Expected %v for IP %s, got %v", scenario.expected, scenario.ip, result)
|
||||
}
|
||||
}
|
||||
|
||||
if IsTrustedIP("127.0.0.1", nil) {
|
||||
t.Error("Expected false when no trusted networks are defined")
|
||||
}
|
||||
|
||||
if IsTrustedIP("127.0.0.1", []string{}) {
|
||||
t.Error("Expected false when trusted networks list is empty")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,9 @@ func setupHandler(store *storage.Storage, pool *worker.Pool) *mux.Router {
|
||||
|
||||
fever.Serve(subrouter, store)
|
||||
googlereader.Serve(subrouter, store)
|
||||
api.Serve(subrouter, store, pool)
|
||||
if config.Opts.HasAPI() {
|
||||
api.Serve(subrouter, store, pool)
|
||||
}
|
||||
ui.Serve(subrouter, store, pool)
|
||||
|
||||
subrouter.HandleFunc("/healthcheck", readinessProbe).Name("healthcheck")
|
||||
@@ -314,32 +316,7 @@ func isAllowedToAccessMetricsEndpoint(r *http.Request) bool {
|
||||
}
|
||||
|
||||
remoteIP := request.FindRemoteIP(r)
|
||||
if remoteIP == "@" {
|
||||
// This indicates a request sent via a Unix socket, always consider these trusted.
|
||||
return true
|
||||
}
|
||||
|
||||
for _, cidr := range config.Opts.MetricsAllowedNetworks() {
|
||||
_, network, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
slog.Error("Metrics endpoint accessed with invalid CIDR",
|
||||
slog.Bool("authentication_failed", true),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("client_user_agent", r.UserAgent()),
|
||||
slog.String("client_remote_addr", r.RemoteAddr),
|
||||
slog.String("cidr", cidr),
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// We use r.RemoteAddr in this case because HTTP headers like X-Forwarded-For can be easily spoofed.
|
||||
// The recommendation is to use HTTP Basic authentication.
|
||||
if network.Contains(net.ParseIP(remoteIP)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return request.IsTrustedIP(remoteIP, config.Opts.MetricsAllowedNetworks())
|
||||
}
|
||||
|
||||
func printErrorAndExit(format string, a ...any) {
|
||||
|
||||
@@ -15,11 +15,13 @@ import (
|
||||
|
||||
func middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
clientIP := request.FindClientIP(r)
|
||||
remoteIP := request.FindRemoteIP(r)
|
||||
isTrustedProxyClientIP := request.IsTrustedIP(remoteIP, config.Opts.TrustedReverseProxyNetworks())
|
||||
clientIP := request.FindClientIP(r, isTrustedProxyClientIP)
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, request.ClientIPContextKey, clientIP)
|
||||
|
||||
if r.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
if isTrustedProxyClientIP && r.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
config.Opts.SetHTTPSValue(true)
|
||||
}
|
||||
|
||||
|
||||
@@ -271,23 +271,26 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
|
||||
}
|
||||
|
||||
if userIntegrations.LinkwardenEnabled {
|
||||
slog.Debug("Sending entry to linkwarden",
|
||||
attrs := []any{
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int64("entry_id", entry.ID),
|
||||
slog.String("entry_url", entry.URL),
|
||||
)
|
||||
}
|
||||
|
||||
if userIntegrations.LinkwardenCollectionID != nil {
|
||||
attrs = append(attrs, slog.Int64("collection_id", *userIntegrations.LinkwardenCollectionID))
|
||||
}
|
||||
|
||||
slog.Debug("Sending entry to linkwarden", attrs...)
|
||||
|
||||
client := linkwarden.NewClient(
|
||||
userIntegrations.LinkwardenURL,
|
||||
userIntegrations.LinkwardenAPIKey,
|
||||
userIntegrations.LinkwardenCollectionID,
|
||||
)
|
||||
if err := client.CreateBookmark(entry.URL, entry.Title); err != nil {
|
||||
slog.Error("Unable to send entry to Linkwarden",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int64("entry_id", entry.ID),
|
||||
slog.String("entry_url", entry.URL),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
attrs = append(attrs, slog.Any("error", err))
|
||||
slog.Error("Unable to send entry to Linkwarden", attrs...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,4 +685,30 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push each new entry to Readeck when push is enabled
|
||||
if userIntegrations.ReadeckPushEnabled {
|
||||
client := readeck.NewClient(
|
||||
userIntegrations.ReadeckURL,
|
||||
userIntegrations.ReadeckAPIKey,
|
||||
userIntegrations.ReadeckLabels,
|
||||
userIntegrations.ReadeckOnlyURL,
|
||||
)
|
||||
for _, entry := range entries {
|
||||
slog.Debug("Sending a new entry to Readeck",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int64("entry_id", entry.ID),
|
||||
slog.String("entry_url", entry.URL),
|
||||
)
|
||||
|
||||
if err := client.CreateBookmark(entry.URL, entry.Title, entry.Content); err != nil {
|
||||
slog.Error("Unable to send entry to Readeck",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int64("entry_id", entry.ID),
|
||||
slog.String("entry_url", entry.URL),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"miniflux.app/v2/internal/model"
|
||||
)
|
||||
|
||||
func TestSendEntryLogsLinkwardenCollectionID(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
handler := slog.NewJSONHandler(&buf, nil)
|
||||
logger := slog.New(handler)
|
||||
prev := slog.Default()
|
||||
slog.SetDefault(logger)
|
||||
defer slog.SetDefault(prev)
|
||||
|
||||
entry := &model.Entry{ID: 52, URL: "https://example.org/test.html", Title: "Test"}
|
||||
coll := int64(12345)
|
||||
userIntegrations := &model.Integration{
|
||||
UserID: 1,
|
||||
LinkwardenEnabled: true,
|
||||
LinkwardenCollectionID: &coll,
|
||||
LinkwardenURL: "",
|
||||
LinkwardenAPIKey: "",
|
||||
}
|
||||
|
||||
SendEntry(entry, userIntegrations)
|
||||
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, `"collection_id":12345`) {
|
||||
t.Fatalf("expected collection_id in logs; got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendEntryLogsLinkwardenWithoutCollectionID(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
handler := slog.NewJSONHandler(&buf, nil)
|
||||
logger := slog.New(handler)
|
||||
prev := slog.Default()
|
||||
slog.SetDefault(logger)
|
||||
defer slog.SetDefault(prev)
|
||||
|
||||
entry := &model.Entry{ID: 52, URL: "https://example.org/test.html", Title: "Test"}
|
||||
userIntegrations := &model.Integration{
|
||||
UserID: 1,
|
||||
LinkwardenEnabled: true,
|
||||
LinkwardenURL: "",
|
||||
LinkwardenAPIKey: "",
|
||||
}
|
||||
|
||||
SendEntry(entry, userIntegrations)
|
||||
|
||||
out := buf.String()
|
||||
if strings.Contains(out, "collection_id") {
|
||||
t.Fatalf("did not expect collection_id in logs; got: %s", out)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -18,12 +19,23 @@ import (
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
baseURL string
|
||||
apiKey string
|
||||
collectionID *int64
|
||||
}
|
||||
|
||||
func NewClient(baseURL, apiKey string) *Client {
|
||||
return &Client{baseURL: baseURL, apiKey: apiKey}
|
||||
type linkwardenCollection struct {
|
||||
ID *int64 `json:"id"`
|
||||
}
|
||||
|
||||
type linkwardenRequest struct {
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
Collection *linkwardenCollection `json:"collection,omitempty"`
|
||||
}
|
||||
|
||||
func NewClient(baseURL, apiKey string, collectionID *int64) *Client {
|
||||
return &Client{baseURL: baseURL, apiKey: apiKey, collectionID: collectionID}
|
||||
}
|
||||
|
||||
func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
@@ -36,10 +48,16 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
return fmt.Errorf(`linkwarden: invalid API endpoint: %v`, err)
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(map[string]string{
|
||||
"url": entryURL,
|
||||
"name": entryTitle,
|
||||
})
|
||||
payload := linkwardenRequest{
|
||||
URL: entryURL,
|
||||
Name: entryTitle,
|
||||
}
|
||||
|
||||
if c.collectionID != nil {
|
||||
payload.Collection = &linkwardenCollection{ID: c.collectionID}
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(payload)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("linkwarden: unable to encode request body: %v", err)
|
||||
@@ -61,8 +79,13 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
responseBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("linkwarden: unable to read response body: %v", err)
|
||||
}
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
return fmt.Errorf("linkwarden: unable to create link: url=%s status=%d", apiEndpoint, response.StatusCode)
|
||||
return fmt.Errorf("linkwarden: unable to create link: status=%d body=%s", response.StatusCode, string(responseBody))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package linkwarden
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"miniflux.app/v2/internal/model"
|
||||
)
|
||||
|
||||
func TestCreateBookmark(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
baseURL string
|
||||
apiKey string
|
||||
collectionID *int64
|
||||
entryURL string
|
||||
entryTitle string
|
||||
serverResponse func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionID *int64)
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "successful bookmark creation without collection",
|
||||
baseURL: "",
|
||||
apiKey: "test-api-key",
|
||||
collectionID: nil,
|
||||
entryURL: "https://example.com",
|
||||
entryTitle: "Test Article",
|
||||
serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
|
||||
// Verify authorization header
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth != "Bearer test-api-key" {
|
||||
t.Errorf("Expected Authorization header 'Bearer test-api-key', got %s", auth)
|
||||
}
|
||||
|
||||
// Verify content type
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType != "application/json" {
|
||||
t.Errorf("Expected Content-Type 'application/json', got %s", contentType)
|
||||
}
|
||||
|
||||
// Parse and verify request
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
var req map[string]any
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
t.Errorf("Failed to parse request body: %v", err)
|
||||
}
|
||||
|
||||
// Verify URL
|
||||
if reqURL := req["url"]; reqURL != "https://example.com" {
|
||||
t.Errorf("Expected URL 'https://example.com', got %v", reqURL)
|
||||
}
|
||||
|
||||
// Verify title/name
|
||||
if reqName := req["name"]; reqName != "Test Article" {
|
||||
t.Errorf("Expected name 'Test Article', got %v", reqName)
|
||||
}
|
||||
|
||||
// Verify collection is not present when nil
|
||||
if _, ok := req["collection"]; ok {
|
||||
t.Error("Expected collection field to be omitted when collectionId is nil")
|
||||
}
|
||||
|
||||
// Return success response
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "123",
|
||||
"url": "https://example.com",
|
||||
"name": "Test Article",
|
||||
})
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "successful bookmark creation with collection",
|
||||
baseURL: "",
|
||||
apiKey: "test-api-key",
|
||||
collectionID: model.OptionalNumber(int64(42)),
|
||||
entryURL: "https://example.com/article",
|
||||
entryTitle: "Test Article With Collection",
|
||||
serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionID *int64) {
|
||||
// Verify authorization header
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth != "Bearer test-api-key" {
|
||||
t.Errorf("Expected Authorization header 'Bearer test-api-key', got %s", auth)
|
||||
}
|
||||
|
||||
// Parse and verify request
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
var req map[string]any
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
t.Errorf("Failed to parse request body: %v", err)
|
||||
}
|
||||
|
||||
// Verify URL
|
||||
if reqURL := req["url"]; reqURL != "https://example.com/article" {
|
||||
t.Errorf("Expected URL 'https://example.com/article', got %v", reqURL)
|
||||
}
|
||||
|
||||
// Verify title/name
|
||||
if reqName := req["name"]; reqName != "Test Article With Collection" {
|
||||
t.Errorf("Expected name 'Test Article With Collection', got %v", reqName)
|
||||
}
|
||||
|
||||
// Verify collection is present and correct
|
||||
if collection, ok := req["collection"]; ok {
|
||||
collectionMap, ok := collection.(map[string]any)
|
||||
if !ok {
|
||||
t.Error("Expected collection to be a map")
|
||||
}
|
||||
if collectionID, ok := collectionMap["id"]; ok {
|
||||
// JSON numbers are float64
|
||||
if collectionIDFloat, ok := collectionID.(float64); !ok || int64(collectionIDFloat) != 42 {
|
||||
t.Errorf("Expected collection id 42, got %v", collectionID)
|
||||
}
|
||||
} else {
|
||||
t.Error("Expected collection to have 'id' field")
|
||||
}
|
||||
} else {
|
||||
t.Error("Expected collection field to be present when collectionId is set")
|
||||
}
|
||||
|
||||
// Return success response
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "124",
|
||||
"url": "https://example.com/article",
|
||||
"name": "Test Article With Collection",
|
||||
})
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing API key",
|
||||
baseURL: "",
|
||||
apiKey: "",
|
||||
collectionID: nil,
|
||||
entryURL: "https://example.com",
|
||||
entryTitle: "Test",
|
||||
serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
|
||||
// Should not be called
|
||||
t.Error("Server should not be called when API key is missing")
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "missing base URL or API key",
|
||||
},
|
||||
{
|
||||
name: "server error",
|
||||
baseURL: "",
|
||||
apiKey: "test-api-key",
|
||||
collectionID: nil,
|
||||
entryURL: "https://example.com",
|
||||
entryTitle: "Test",
|
||||
serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(`{"error": "Internal server error"}`))
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "unable to create link: status=500",
|
||||
},
|
||||
{
|
||||
name: "bad request with null collection id error",
|
||||
baseURL: "",
|
||||
apiKey: "test-api-key",
|
||||
collectionID: nil,
|
||||
entryURL: "https://example.com",
|
||||
entryTitle: "Test",
|
||||
serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(`{"response":"Error: Expected number, received null [collection, id]"}`))
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "unable to create link: status=400",
|
||||
},
|
||||
{
|
||||
name: "unauthorized",
|
||||
baseURL: "",
|
||||
apiKey: "invalid-key",
|
||||
collectionID: nil,
|
||||
entryURL: "https://example.com",
|
||||
entryTitle: "Test",
|
||||
serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(`{"error": "Unauthorized"}`))
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "unable to create link: status=401",
|
||||
},
|
||||
{
|
||||
name: "invalid base URL",
|
||||
baseURL: ":",
|
||||
apiKey: "test-api-key",
|
||||
collectionID: nil,
|
||||
entryURL: "https://example.com",
|
||||
entryTitle: "Test",
|
||||
serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
|
||||
// Should not be called
|
||||
t.Error("Server should not be called when base URL is invalid")
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "invalid API endpoint",
|
||||
},
|
||||
{
|
||||
name: "missing base URL",
|
||||
baseURL: "",
|
||||
apiKey: "",
|
||||
collectionID: nil,
|
||||
entryURL: "https://example.com",
|
||||
entryTitle: "Test",
|
||||
serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
|
||||
// Should not be called
|
||||
t.Error("Server should not be called when base URL is missing")
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "missing base URL or API key",
|
||||
},
|
||||
{
|
||||
name: "network connection error",
|
||||
baseURL: "http://localhost:1", // Invalid port that should fail to connect
|
||||
apiKey: "test-api-key",
|
||||
collectionID: nil,
|
||||
entryURL: "https://example.com",
|
||||
entryTitle: "Test",
|
||||
serverResponse: func(w http.ResponseWriter, r *http.Request, t *testing.T, collectionId *int64) {
|
||||
// Should not be called due to connection failure
|
||||
t.Error("Server should not be called when connection fails")
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "unable to send request",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create test server only if we have a valid apiKey and don't have a custom baseURL for error testing
|
||||
var server *httptest.Server
|
||||
if tt.apiKey != "" && tt.baseURL != ":" && tt.baseURL != "http://localhost:1" {
|
||||
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tt.serverResponse(w, r, t, tt.collectionID)
|
||||
}))
|
||||
defer server.Close()
|
||||
}
|
||||
|
||||
// Use test server URL if baseURL is empty and we have a server
|
||||
baseURL := tt.baseURL
|
||||
if baseURL == "" && server != nil {
|
||||
baseURL = server.URL
|
||||
}
|
||||
|
||||
// Create client
|
||||
client := NewClient(baseURL, tt.apiKey, tt.collectionID)
|
||||
|
||||
// Call CreateBookmark
|
||||
err := client.CreateBookmark(tt.entryURL, tt.entryTitle)
|
||||
|
||||
// Check error
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
} else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("Expected error to contain '%s', got '%s'", tt.errContains, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
baseURL string
|
||||
apiKey string
|
||||
collectionID *int64
|
||||
}{
|
||||
{
|
||||
name: "client without collection",
|
||||
baseURL: "https://linkwarden.example.com",
|
||||
apiKey: "test-key",
|
||||
collectionID: nil,
|
||||
},
|
||||
{
|
||||
name: "client with collection",
|
||||
baseURL: "https://linkwarden.example.com",
|
||||
apiKey: "test-key",
|
||||
collectionID: model.OptionalNumber(int64(123)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := NewClient(tt.baseURL, tt.apiKey, tt.collectionID)
|
||||
|
||||
if client.baseURL != tt.baseURL {
|
||||
t.Errorf("Expected baseURL %s, got %s", tt.baseURL, client.baseURL)
|
||||
}
|
||||
|
||||
if client.apiKey != tt.apiKey {
|
||||
t.Errorf("Expected apiKey %s, got %s", tt.apiKey, client.apiKey)
|
||||
}
|
||||
|
||||
if tt.collectionID == nil {
|
||||
if client.collectionID != nil {
|
||||
t.Errorf("Expected collectionId to be nil, got %v", *client.collectionID)
|
||||
}
|
||||
} else {
|
||||
if client.collectionID == nil {
|
||||
t.Error("Expected collectionId to be set, got nil")
|
||||
} else if *client.collectionID != *tt.collectionID {
|
||||
t.Errorf("Expected collectionId %d, got %d", *tt.collectionID, *client.collectionID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package readeck
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateBookmark(t *testing.T) {
|
||||
entryURL := "https://example.com/article"
|
||||
entryTitle := "Example Title"
|
||||
entryContent := "<p>Some HTML content</p>"
|
||||
labels := "tag1,tag2"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
onlyURL bool
|
||||
baseURL string
|
||||
apiKey string
|
||||
labels string
|
||||
entryURL string
|
||||
entryTitle string
|
||||
entryContent string
|
||||
serverResponse func(w http.ResponseWriter, r *http.Request)
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "successful bookmark creation with only URL",
|
||||
onlyURL: true,
|
||||
labels: labels,
|
||||
entryURL: entryURL,
|
||||
entryTitle: entryTitle,
|
||||
entryContent: entryContent,
|
||||
serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/bookmarks/" {
|
||||
t.Errorf("expected path /api/bookmarks/, got %s", r.URL.Path)
|
||||
}
|
||||
if got := r.Header.Get("Authorization"); !strings.HasPrefix(got, "Bearer ") {
|
||||
t.Errorf("expected Authorization Bearer header, got %q", got)
|
||||
}
|
||||
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
|
||||
t.Errorf("expected Content-Type application/json, got %s", ct)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
t.Fatalf("failed to parse JSON body: %v", err)
|
||||
}
|
||||
if u := payload["url"]; u != entryURL {
|
||||
t.Errorf("expected url %s, got %v", entryURL, u)
|
||||
}
|
||||
if title := payload["title"]; title != entryTitle {
|
||||
t.Errorf("expected title %s, got %v", entryTitle, title)
|
||||
}
|
||||
// Labels should be split into an array
|
||||
if raw := payload["labels"]; raw == nil {
|
||||
t.Errorf("expected labels to be set")
|
||||
} else if arr, ok := raw.([]any); ok {
|
||||
if len(arr) != 2 || arr[0] != "tag1" || arr[1] != "tag2" {
|
||||
t.Errorf("unexpected labels: %#v", arr)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("labels should be an array, got %T", raw)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "successful bookmark creation with content (multipart)",
|
||||
onlyURL: false,
|
||||
labels: labels,
|
||||
entryURL: entryURL,
|
||||
entryTitle: entryTitle,
|
||||
entryContent: entryContent,
|
||||
serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("expected POST, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/api/bookmarks/" {
|
||||
t.Errorf("expected path /api/bookmarks/, got %s", r.URL.Path)
|
||||
}
|
||||
if got := r.Header.Get("Authorization"); !strings.HasPrefix(got, "Bearer ") {
|
||||
t.Errorf("expected Authorization Bearer header, got %q", got)
|
||||
}
|
||||
ct := r.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(ct, "multipart/form-data;") {
|
||||
t.Errorf("expected multipart/form-data, got %s", ct)
|
||||
}
|
||||
boundaryIdx := strings.Index(ct, "boundary=")
|
||||
if boundaryIdx == -1 {
|
||||
t.Fatalf("missing multipart boundary in Content-Type: %s", ct)
|
||||
}
|
||||
boundary := ct[boundaryIdx+len("boundary="):]
|
||||
mr := multipart.NewReader(r.Body, boundary)
|
||||
|
||||
seenLabels := []string{}
|
||||
var seenURL, seenTitle, seenFeature string
|
||||
var resourceHeader map[string]any
|
||||
var resourceBody string
|
||||
|
||||
for {
|
||||
part, err := mr.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("reading multipart: %v", err)
|
||||
}
|
||||
name := part.FormName()
|
||||
data, _ := io.ReadAll(part)
|
||||
switch name {
|
||||
case "url":
|
||||
seenURL = string(data)
|
||||
case "title":
|
||||
seenTitle = string(data)
|
||||
case "feature_find_main":
|
||||
seenFeature = string(data)
|
||||
case "labels":
|
||||
seenLabels = append(seenLabels, string(data))
|
||||
case "resource":
|
||||
// First line is JSON header, then newline, then content
|
||||
all := string(data)
|
||||
idx := strings.IndexByte(all, '\n')
|
||||
if idx == -1 {
|
||||
t.Fatalf("resource content missing header separator")
|
||||
}
|
||||
headerJSON := all[:idx]
|
||||
resourceBody = all[idx+1:]
|
||||
if err := json.Unmarshal([]byte(headerJSON), &resourceHeader); err != nil {
|
||||
t.Fatalf("invalid resource header JSON: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if seenURL != entryURL {
|
||||
t.Errorf("expected url %s, got %s", entryURL, seenURL)
|
||||
}
|
||||
if seenTitle != entryTitle {
|
||||
t.Errorf("expected title %s, got %s", entryTitle, seenTitle)
|
||||
}
|
||||
if seenFeature != "false" {
|
||||
t.Errorf("expected feature_find_main to be 'false', got %s", seenFeature)
|
||||
}
|
||||
if len(seenLabels) != 2 || seenLabels[0] != "tag1" || seenLabels[1] != "tag2" {
|
||||
t.Errorf("unexpected labels: %#v", seenLabels)
|
||||
}
|
||||
if resourceHeader == nil {
|
||||
t.Fatalf("missing resource header")
|
||||
}
|
||||
if hURL, _ := resourceHeader["url"].(string); hURL != entryURL {
|
||||
t.Errorf("expected resource header url %s, got %v", entryURL, hURL)
|
||||
}
|
||||
if headers, ok := resourceHeader["headers"].(map[string]any); ok {
|
||||
if ct, _ := headers["content-type"].(string); ct != "text/html; charset=utf-8" {
|
||||
t.Errorf("expected resource header content-type text/html; charset=utf-8, got %v", ct)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("missing resource header 'headers' field")
|
||||
}
|
||||
if resourceBody != entryContent {
|
||||
t.Errorf("expected resource body %q, got %q", entryContent, resourceBody)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error when server returns 400",
|
||||
onlyURL: true,
|
||||
labels: labels,
|
||||
entryURL: entryURL,
|
||||
entryTitle: entryTitle,
|
||||
entryContent: entryContent,
|
||||
serverResponse: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "unable to create bookmark",
|
||||
},
|
||||
{
|
||||
name: "error when missing baseURL or apiKey",
|
||||
onlyURL: true,
|
||||
baseURL: "",
|
||||
apiKey: "",
|
||||
labels: labels,
|
||||
entryURL: entryURL,
|
||||
entryTitle: entryTitle,
|
||||
entryContent: entryContent,
|
||||
serverResponse: nil,
|
||||
wantErr: true,
|
||||
errContains: "missing base URL or API key",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var serverURL string
|
||||
if tt.serverResponse != nil {
|
||||
srv := httptest.NewServer(http.HandlerFunc(tt.serverResponse))
|
||||
defer srv.Close()
|
||||
serverURL = srv.URL
|
||||
}
|
||||
baseURL := tt.baseURL
|
||||
if baseURL == "" {
|
||||
baseURL = serverURL
|
||||
}
|
||||
apiKey := tt.apiKey
|
||||
if apiKey == "" {
|
||||
apiKey = "test-api-key"
|
||||
}
|
||||
|
||||
client := NewClient(baseURL, apiKey, tt.labels, tt.onlyURL)
|
||||
err := client.CreateBookmark(tt.entryURL, tt.entryTitle, tt.entryContent)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got none")
|
||||
}
|
||||
if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
||||
t.Fatalf("expected error containing %q, got %q", tt.errContains, err.Error())
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
baseURL := "https://readeck.example.com"
|
||||
apiKey := "key"
|
||||
labels := "tag1,tag2"
|
||||
onlyURL := true
|
||||
|
||||
c := NewClient(baseURL, apiKey, labels, onlyURL)
|
||||
if c.baseURL != baseURL {
|
||||
t.Errorf("expected baseURL %s, got %s", baseURL, c.baseURL)
|
||||
}
|
||||
if c.apiKey != apiKey {
|
||||
t.Errorf("expected apiKey %s, got %s", apiKey, c.apiKey)
|
||||
}
|
||||
if c.labels != labels {
|
||||
t.Errorf("expected labels %s, got %s", labels, c.labels)
|
||||
}
|
||||
if c.onlyURL != onlyURL {
|
||||
t.Errorf("expected onlyURL %v, got %v", onlyURL, c.onlyURL)
|
||||
}
|
||||
}
|
||||
@@ -266,6 +266,7 @@
|
||||
"form.integration.linkwarden_activate": "Artikel in Linkwarden speichern",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden-API-Schlüssel",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden-Base-URL",
|
||||
"form.integration.linkwarden_collection_id": "Linkwarden-Sammlungs-ID",
|
||||
"form.integration.matrix_bot_activate": "Neue Artikel in Matrix übertragen",
|
||||
"form.integration.matrix_bot_chat_id": "ID des Matrix-Raums",
|
||||
"form.integration.matrix_bot_password": "Passwort für Matrix-Benutzer",
|
||||
@@ -306,6 +307,7 @@
|
||||
"form.integration.readeck_endpoint": "Readeck-URL",
|
||||
"form.integration.readeck_labels": "Readeck-Labels",
|
||||
"form.integration.readeck_only_url": "Nur URL senden (anstelle des vollständigen Inhalts)",
|
||||
"form.integration.readeck_push_activate": "Neue Artikel automatisch in Readeck speichern",
|
||||
"form.integration.readwise_activate": "Artikel in Readwise Reader speichern",
|
||||
"form.integration.readwise_api_key": "Readwise-Reader-Zugangstoken",
|
||||
"form.integration.readwise_api_key_link": "Erhalten Sie Ihren Readwise-Zugangstoken",
|
||||
@@ -370,10 +372,10 @@
|
||||
"form.prefs.label.theme": "Thema",
|
||||
"form.prefs.label.timezone": "Zeitzone",
|
||||
"form.prefs.select.alphabetical": "Alphabetisch",
|
||||
"form.prefs.select.browser": "Browser",
|
||||
"form.prefs.select.browser": "Systembrowser",
|
||||
"form.prefs.select.created_time": "Artikel erstellt am",
|
||||
"form.prefs.select.fullscreen": "Vollbildschirm",
|
||||
"form.prefs.select.minimal_ui": "Minimal",
|
||||
"form.prefs.select.minimal_ui": "Minimale Oberfläche",
|
||||
"form.prefs.select.none": "Keine",
|
||||
"form.prefs.select.older_first": "Ältere Artikel zuerst",
|
||||
"form.prefs.select.publish_time": "Artikel veröffentlicht am",
|
||||
|
||||
@@ -254,9 +254,9 @@
|
||||
"form.integration.linkding_endpoint": "Τελικό σημείο Linkding API",
|
||||
"form.integration.linkding_tags": "Ετικέτες Linkding",
|
||||
"form.integration.linktaco_activate": "Αποθήκευση καταχωρήσεων στο LinkTaco",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token": "Διακριτικό API LinkTaco",
|
||||
"form.integration.linktaco_api_token_hint": "Λάβετε το προσωπικό σας διακριτικό πρόσβασης στο",
|
||||
"form.integration.linktaco_org_slug": "Organization Slug",
|
||||
"form.integration.linktaco_org_slug": "Σύντομο όνομα οργανισμού",
|
||||
"form.integration.linktaco_tags": "Ετικέτες (μέγιστο 10, διαχωρισμένες με κόμμα)",
|
||||
"form.integration.linktaco_tags_hint": "Μέγιστο 10 ετικέτες, διαχωρισμένες με κόμμα",
|
||||
"form.integration.linktaco_visibility": "Ορατότητα",
|
||||
@@ -266,6 +266,7 @@
|
||||
"form.integration.linkwarden_activate": "Αποθήκευση άρθρων στο Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Κλειδί API Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "URL βάσης Linkwarden",
|
||||
"form.integration.linkwarden_collection_id": "ID συλλογής Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Μεταφορά νέων άρθρων στο Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Αναγνωριστικό της αίθουσας Matrix",
|
||||
"form.integration.matrix_bot_password": "Κωδικός πρόσβασης για τον χρήστη Matrix",
|
||||
@@ -306,6 +307,7 @@
|
||||
"form.integration.readeck_endpoint": "Τελικό σημείο Readeck API",
|
||||
"form.integration.readeck_labels": "Ετικέτες Readeck",
|
||||
"form.integration.readeck_only_url": "Αποστολή μόνο URL (αντί για πλήρες περιεχόμενο)",
|
||||
"form.integration.readeck_push_activate": "Αυτόματη αποστολή νέων καταχωρήσεων στο Readeck",
|
||||
"form.integration.readwise_activate": "Αποθήκευση καταχωρήσεων στο Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Διακριτικό πρόσβασης Readwise Reader",
|
||||
"form.integration.readwise_api_key_link": "Λήψη του διακριτικού πρόσβασης Readwise",
|
||||
@@ -335,7 +337,7 @@
|
||||
"form.integration.wallabag_only_url": "Αποστολή μόνο URL (αντί για πλήρες περιεχόμενο)",
|
||||
"form.integration.wallabag_password": "Wallabag Κωδικός Πρόσβασης",
|
||||
"form.integration.wallabag_username": "Όνομα Χρήστη Wallabag",
|
||||
"form.integration.wallabag_tags": "Wallabag Tags",
|
||||
"form.integration.wallabag_tags": "Ετικέτες Wallabag",
|
||||
"form.integration.webhook_activate": "Ενεργοποίηση Webhooks",
|
||||
"form.integration.webhook_secret": "Μυστικό Webhooks",
|
||||
"form.integration.webhook_url": "Προεπιλεγμένη διεύθυνση URL Webhook",
|
||||
@@ -434,7 +436,7 @@
|
||||
"page.about.title": "Περί",
|
||||
"page.about.version": "Έκδοση:",
|
||||
"page.add_feed.choose_feed": "Επιλέξτε μια συνδρομή",
|
||||
"page.add_feed.label.url": "URL",
|
||||
"page.add_feed.label.url": "Διεύθυνση URL",
|
||||
"page.add_feed.legend.advanced_options": "Προχωρημένες Επιλογές",
|
||||
"page.add_feed.no_category": "Δεν υπάρχει κατηγορία. Πρέπει να έχετε τουλάχιστον μία κατηγορία.",
|
||||
"page.add_feed.submit": "Βρείτε μια συνδρομή",
|
||||
@@ -444,7 +446,7 @@
|
||||
"page.api_keys.table.created_at": "Ημερομηνία Δημιουργίας",
|
||||
"page.api_keys.table.description": "Περιγραφή",
|
||||
"page.api_keys.table.last_used_at": "Τελευταία Χρήση",
|
||||
"page.api_keys.table.token": "Token",
|
||||
"page.api_keys.table.token": "Διακριτικό",
|
||||
"page.api_keys.title": "Κλειδιά API",
|
||||
"page.categories.entries": "Άρθρα",
|
||||
"page.categories.feed_count": [
|
||||
@@ -476,14 +478,14 @@
|
||||
"page.feeds.next_check": "Επόμενος έλεγχος:",
|
||||
"page.feeds.read_counter": "Αριθμός αναγνωσμένων καταχωρήσεων",
|
||||
"page.feeds.title": "Ροές",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "Επιστροφή στην κορυφή",
|
||||
"page.history.title": "Ιστορικό",
|
||||
"page.import.title": "Εισαγωγή",
|
||||
"page.integration.bookmarklet": "Bookmarklet",
|
||||
"page.integration.bookmarklet": "Σελιδοδείκτης (bookmarklet)",
|
||||
"page.integration.bookmarklet.help": "Αυτός ο ειδικός σύνδεσμος σάς επιτρέπει να εγγραφείτε απευθείας σε έναν ιστότοπο χρησιμοποιώντας ένα σελιδοδείκτη στο πρόγραμμα περιήγησης ιστού σας.",
|
||||
"page.integration.bookmarklet.instructions": "Σύρετε και αποθέστε αυτόν τον σύνδεσμο στους σελιδοδείκτες σας.",
|
||||
"page.integration.bookmarklet.name": "Προσθήκη στο Miniflux",
|
||||
"page.integration.miniflux_api": "Miniflux API",
|
||||
"page.integration.miniflux_api": "API του Miniflux",
|
||||
"page.integration.miniflux_api_endpoint": "Τελικό σημείο API",
|
||||
"page.integration.miniflux_api_password": "Κωδικός",
|
||||
"page.integration.miniflux_api_password_value": "Ο κωδικός πρόσβασης του λογαριασμού σας",
|
||||
@@ -546,7 +548,7 @@
|
||||
"page.sessions.table.current_session": "Τρέχουσα Συνεδρία",
|
||||
"page.sessions.table.date": "Ημερομηνία",
|
||||
"page.sessions.table.ip": "Διεύθυνση IP",
|
||||
"page.sessions.table.user_agent": "User Agent",
|
||||
"page.sessions.table.user_agent": "Πρόγραμμα περιήγησης (User Agent)",
|
||||
"page.sessions.title": "Συνεδρίες",
|
||||
"page.settings.link_google_account": "Σύνδεση του λογαριασμό μου Google",
|
||||
"page.settings.link_oidc_account": "Σύνδεση του λογαριασμού μου %s",
|
||||
@@ -629,4 +631,4 @@
|
||||
"time_elapsed.yesterday": "χθες",
|
||||
"tooltip.keyboard_shortcuts": "Συντόμευση πληκτρολογίου: % s",
|
||||
"tooltip.logged_user": "Συνδεδεμένος/η ως %s"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +266,7 @@
|
||||
"form.integration.linkwarden_activate": "Save entries to Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API key",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden Base URL",
|
||||
"form.integration.linkwarden_collection_id": "Linkwarden Collection ID",
|
||||
"form.integration.matrix_bot_activate": "Push new entries to Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID of Matrix Room",
|
||||
"form.integration.matrix_bot_password": "Password for Matrix user",
|
||||
@@ -306,6 +307,7 @@
|
||||
"form.integration.readeck_endpoint": "Readeck URL",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "Send only URL (instead of full content)",
|
||||
"form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
|
||||
"form.integration.readwise_activate": "Save entries to Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
@@ -629,4 +631,4 @@
|
||||
"time_elapsed.yesterday": "yesterday",
|
||||
"tooltip.keyboard_shortcuts": "Keyboard Shortcut: %s",
|
||||
"tooltip.logged_user": "Logged in as %s"
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,7 @@
|
||||
"form.api_key.label.description": "Etiqueta de clave API",
|
||||
"form.category.hide_globally": "Ocultar artículos en la lista global de no leídos",
|
||||
"form.category.label.title": "Título",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.general": "Generalidades",
|
||||
"form.feed.fieldset.integration": "Servicios de terceros",
|
||||
"form.feed.fieldset.network_settings": "Ajustes de red",
|
||||
"form.feed.fieldset.rules": "Reglas",
|
||||
@@ -245,7 +245,7 @@
|
||||
"form.integration.linkace_activate": "Guardar artículos en LinkAce",
|
||||
"form.integration.linkace_api_key": "Clave API de LinkAce",
|
||||
"form.integration.linkace_check_disabled": "Deshabilitar la comprobación de enlace",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_endpoint": "Extremo de la API de LinkAce",
|
||||
"form.integration.linkace_is_private": "Marcar enlace como privado",
|
||||
"form.integration.linkace_tags": "Etiquetas de LinkAce",
|
||||
"form.integration.linkding_activate": "Enviar artículos a Linkding",
|
||||
@@ -254,9 +254,9 @@
|
||||
"form.integration.linkding_endpoint": "Acceso API de Linkding",
|
||||
"form.integration.linkding_tags": "Etiquetas de Linkding",
|
||||
"form.integration.linktaco_activate": "Guardar entradas en LinkTaco",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token": "Token de la API de LinkTaco",
|
||||
"form.integration.linktaco_api_token_hint": "Obtenga su token de acceso personal en",
|
||||
"form.integration.linktaco_org_slug": "Organization Slug",
|
||||
"form.integration.linktaco_org_slug": "Slug de la organización",
|
||||
"form.integration.linktaco_tags": "Etiquetas (máx. 10, separadas por comas)",
|
||||
"form.integration.linktaco_tags_hint": "Máximo 10 etiquetas, separadas por comas",
|
||||
"form.integration.linktaco_visibility": "Visibilidad",
|
||||
@@ -266,6 +266,7 @@
|
||||
"form.integration.linkwarden_activate": "Enviar artículos a Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Clave de API de Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "URL base de Linkwarden",
|
||||
"form.integration.linkwarden_collection_id": "ID de colección de Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Transferir nuevos artículos a Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID de la sala de Matrix",
|
||||
"form.integration.matrix_bot_password": "Contraseña para el usuario de Matrix",
|
||||
@@ -277,7 +278,7 @@
|
||||
"form.integration.ntfy_activate": "Enviar artículos a ntfy",
|
||||
"form.integration.ntfy_api_token": "Token de API de Ntfy (opcional)",
|
||||
"form.integration.ntfy_icon_url": "URL del icono de Ntfy (opcional)",
|
||||
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
|
||||
"form.integration.ntfy_internal_links": "Usar enlaces internos al hacer clic (opcional)",
|
||||
"form.integration.ntfy_password": "Contraseña de Ntfy (opcional)",
|
||||
"form.integration.ntfy_topic": "Tema Ntfy (por defecto, si no se establece en el feed)",
|
||||
"form.integration.ntfy_url": "URL de Ntfy (opcional, la predeterminada es ntfy.sh)",
|
||||
@@ -300,17 +301,18 @@
|
||||
"form.integration.raindrop_activate": "Guardar artículos en Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Colección ID",
|
||||
"form.integration.raindrop_tags": "Etiquetas (separadas por comas)",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.raindrop_token": "Token (prueba)",
|
||||
"form.integration.readeck_activate": "Enviar artículos a Readeck",
|
||||
"form.integration.readeck_api_key": "Clave de API de Readeck",
|
||||
"form.integration.readeck_endpoint": "Acceso API de Readeck",
|
||||
"form.integration.readeck_labels": "Etiquetas de Readeck",
|
||||
"form.integration.readeck_only_url": "Enviar solo URL (en lugar de contenido completo)",
|
||||
"form.integration.readeck_push_activate": "Enviar automáticamente nuevas entradas a Readeck",
|
||||
"form.integration.readwise_activate": "Guardar artículos en Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Token de acceso a Readwise Reader",
|
||||
"form.integration.readwise_api_key_link": "Obtener tu token de acceso a Readwise",
|
||||
"form.integration.rssbridge_activate": "Vericar RSS-Bridge al agregar suscripciones",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_token": "Token de autenticación de RSS-Bridge",
|
||||
"form.integration.rssbridge_url": "URL del servidro RSS-Bridge",
|
||||
"form.integration.shaarli_activate": "Guardar artículos en Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Secreto API de Shaarli",
|
||||
@@ -327,7 +329,7 @@
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Deshabilitar la vista previa de la página web",
|
||||
"form.integration.telegram_bot_token": "Token de bot",
|
||||
"form.integration.telegram_chat_id": "ID de chat",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
"form.integration.telegram_topic_id": "ID de tema",
|
||||
"form.integration.wallabag_activate": "Enviar artículos a Wallabag",
|
||||
"form.integration.wallabag_client_id": "ID de cliente de Wallabag",
|
||||
"form.integration.wallabag_client_secret": "Secreto de cliente de Wallabag",
|
||||
@@ -335,7 +337,7 @@
|
||||
"form.integration.wallabag_only_url": "Enviar solo URL (en lugar de contenido completo)",
|
||||
"form.integration.wallabag_password": "Contraseña de Wallabag",
|
||||
"form.integration.wallabag_username": "Nombre de usuario de Wallabag",
|
||||
"form.integration.wallabag_tags": "Wallabag Tags",
|
||||
"form.integration.wallabag_tags": "Etiquetas de Wallabag",
|
||||
"form.integration.webhook_activate": "Habilitar Webhooks",
|
||||
"form.integration.webhook_secret": "Secreto de Webhooks",
|
||||
"form.integration.webhook_url": "Defecto URL de Webhook",
|
||||
@@ -426,7 +428,7 @@
|
||||
"page.about.build_date": "Fecha de compilación:",
|
||||
"page.about.credits": "Créditos",
|
||||
"page.about.db_usage": "Tamaño de la base de datos:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.git_commit": "Commit de Git:",
|
||||
"page.about.global_config_options": "Opciones de configuración global",
|
||||
"page.about.go_version": "Go versión:",
|
||||
"page.about.license": "Licencia:",
|
||||
@@ -476,7 +478,7 @@
|
||||
"page.feeds.next_check": "Próxima verificación:",
|
||||
"page.feeds.read_counter": "Número de artículos leídos",
|
||||
"page.feeds.title": "Fuentes",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "Volver arriba",
|
||||
"page.history.title": "Historial",
|
||||
"page.import.title": "Importar",
|
||||
"page.integration.bookmarklet": "Marcapáginas",
|
||||
@@ -629,4 +631,4 @@
|
||||
"time_elapsed.yesterday": "ayer",
|
||||
"tooltip.keyboard_shortcuts": "Atajo de teclado: %s",
|
||||
"tooltip.logged_user": "Registrado como %s"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,56 +104,56 @@
|
||||
"error.feed_url_not_empty": "Syötteen URL-osoite ei voi olla tyhjä.",
|
||||
"error.fields_mandatory": "Kaikki kentät ovat pakollisia.",
|
||||
"error.http_bad_gateway": "Verkkosivusto ei ole tällä hetkellä saatavilla huonon yhdyskäytävän virheen vuoksi. Ongelma ei ole Miniflux-puolella. Yritä uudelleen myöhemmin.",
|
||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_client_error": "HTTP client error: %v.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||
"error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).",
|
||||
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_body_read": "HTTP-rungon lukeminen epäonnistui: %v.",
|
||||
"error.http_client_error": "HTTP-asiakasvirhe: %v.",
|
||||
"error.http_empty_response": "HTTP-vastaus on tyhjä. Sivusto saattaa käyttää bottisuojausta?",
|
||||
"error.http_empty_response_body": "HTTP-vastauksen runko on tyhjä.",
|
||||
"error.http_forbidden": "Pääsy tälle sivustolle on kielletty. Sivustolla saattaa olla bottisuojaus?",
|
||||
"error.http_gateway_timeout": "Sivusto ei ole nyt käytettävissä yhdyskäytävän aikakatkaisun vuoksi. Ongelma ei ole Minifluxin puolella. Yritä myöhemmin uudelleen.",
|
||||
"error.http_internal_server_error": "Sivusto ei ole nyt käytettävissä palvelinvirheen vuoksi. Ongelma ei ole Minifluxin puolella. Yritä myöhemmin uudelleen.",
|
||||
"error.http_not_authorized": "Pääsy tälle sivustolle ei ole sallittu. Käyttäjänimi tai salasana voi olla väärä.",
|
||||
"error.http_resource_not_found": "Pyydettyä resurssia ei löytynyt. Tarkista URL-osoite.",
|
||||
"error.http_response_too_large": "HTTP-vastaus on liian suuri. Voit kasvattaa rajan yleisasetuksissa (vaatii palvelimen uudelleenkäynnistyksen).",
|
||||
"error.http_service_unavailable": "Sivusto ei ole nyt käytettävissä sisäisen palvelinvirheen vuoksi. Ongelma ei ole Minifluxin puolella. Yritä myöhemmin uudelleen.",
|
||||
"error.http_too_many_requests": "Miniflux lähetti liikaa pyyntöjä tälle sivustolle. Yritä myöhemmin uudelleen tai muuta sovelluksen asetuksia.",
|
||||
"error.http_unexpected_status_code": "Sivusto ei ole nyt käytettävissä odottamattoman HTTP-tilakoodin %d vuoksi. Ongelma ei ole Minifluxin puolella. Yritä myöhemmin uudelleen.",
|
||||
"error.invalid_categories_sorting_order": "Virheellinen kategorioiden lajittelujärjestys.",
|
||||
"error.invalid_default_home_page": "Väärä oletusarvoinen kotisivu!",
|
||||
"error.invalid_display_mode": "Virheellinen verkkosovelluksen näyttötila.",
|
||||
"error.invalid_entry_direction": "Invalid entry direction.",
|
||||
"error.invalid_entry_direction": "Virheellinen merkintäsuunta.",
|
||||
"error.invalid_entry_order": "Virheellinen artikkelin lajittelu.",
|
||||
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
|
||||
"error.invalid_feed_proxy_url": "Virheellinen välityspalvelimen URL.",
|
||||
"error.invalid_feed_url": "Virheellinen syötteen URL-osoite.",
|
||||
"error.invalid_gesture_nav": "Virheellinen ele-navigointi.",
|
||||
"error.invalid_language": "Virheellinen kieli.",
|
||||
"error.invalid_site_url": "Virheellinen sivuston URL-osoite.",
|
||||
"error.invalid_theme": "Virheellinen teema.",
|
||||
"error.invalid_timezone": "Virheellinen aikavyöhyke.",
|
||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||
"error.network_operation": "Miniflux ei tavoita tätä sivustoa verkkovirheen vuoksi: %v.",
|
||||
"error.network_timeout": "Tämä sivusto on liian hidas ja pyyntö aikakatkaistiin: %v",
|
||||
"error.password_min_length": "Salasanassa on oltava vähintään 6 merkkiä.",
|
||||
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.proxy_url_not_empty": "Välityspalvelimen URL ei voi olla tyhjä.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Virheellinen estosääntö: säännöltä #%d puuttuu kelvollinen kentän nimi (vaihtoehdot: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Virheellinen estosääntö: säännön #%d kuvio ei ole kelvollinen regex",
|
||||
"error.settings_block_rule_regex_required": "Virheellinen estosääntö: säännöltä #%d puuttuu kuvio",
|
||||
"error.settings_block_rule_separator_required": "Virheellinen estosääntö: säännön #%d kuvio tulee erottaa merkillä '='",
|
||||
"error.settings_invalid_domain_list": "Virheellinen verkkotunnuslista. Anna välilyönnein eroteltu luettelo.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Virheellinen säilytyssääntö: säännöltä #%d puuttuu kelvollinen kentän nimi (vaihtoehdot: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Virheellinen säilytyssääntö: säännön #%d kuvio ei ole kelvollinen regex",
|
||||
"error.settings_keep_rule_regex_required": "Virheellinen säilytyssääntö: säännöltä #%d puuttuu kuvio",
|
||||
"error.settings_keep_rule_separator_required": "Virheellinen säilytyssääntö: säännön #%d kuvio tulee erottaa merkillä '='",
|
||||
"error.settings_mandatory_fields": "Käyttäjätunnus, teema, kieli ja aikavyöhyke ovat pakollisia.",
|
||||
"error.settings_media_playback_rate_range": "Toistonopeus on alueen ulkopuolella",
|
||||
"error.settings_reading_speed_is_positive": "Lukunopeuksien on oltava positiivisia kokonaislukuja.",
|
||||
"error.site_url_not_empty": "Sivuston URL-osoite ei voi olla tyhjä.",
|
||||
"error.subscription_not_found": "Tilausta ei löydy.",
|
||||
"error.title_required": "Otsikko on pakollinen.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "TLS-virhe: %q. Voit halutessasi poistaa TLS-tarkistuksen syöteasetuksista.",
|
||||
"error.unable_to_create_api_key": "API-avainta ei voi luoda.",
|
||||
"error.unable_to_create_category": "Kategoriaa ei voi luoda.",
|
||||
"error.unable_to_create_user": "Käyttäjää ei voi luoda.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.unable_to_detect_rssbridge": "Syötettä ei voitu havaita RSS-Bridgea käyttäen: %v.",
|
||||
"error.unable_to_parse_feed": "Tätä syötettä ei voitu jäsentää: %v.",
|
||||
"error.unable_to_update_category": "Kategoriaa ei voi päivittää.",
|
||||
"error.unable_to_update_feed": "Syötettä ei voi päivittää.",
|
||||
"error.unable_to_update_user": "Käyttäjää ei voi päivittää.",
|
||||
@@ -161,22 +161,22 @@
|
||||
"error.user_already_exists": "Käyttäjä on jo olemassa.",
|
||||
"error.user_mandatory_fields": "Käyttäjätunnus on pakollinen.",
|
||||
"error.linktaco_missing_required_fields": "LinkTaco API Token ja Organization Slug vaaditaan",
|
||||
"form.api_key.label.description": "API Key Label",
|
||||
"form.api_key.label.description": "API-avaimen nimi",
|
||||
"form.category.hide_globally": "Piilota artikkelit lukemattomien listassa",
|
||||
"form.category.label.title": "Otsikko",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.integration": "Third-Party Services",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.general": "Yleiset",
|
||||
"form.feed.fieldset.integration": "Kolmannen osapuolen palvelut",
|
||||
"form.feed.fieldset.network_settings": "Verkkoasetukset",
|
||||
"form.feed.fieldset.rules": "Säännöt",
|
||||
"form.feed.label.allow_self_signed_certificates": "Salli itseallekirjoitetut tai virheelliset varmenteet",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.apprise_service_urls": "Apprise-palvelujen URL-osoitteet pilkuilla eroteltuna",
|
||||
"form.feed.label.block_filter_entry_rules": "Merkinnän estosäännöt",
|
||||
"form.feed.label.blocklist_rules": "Regex-pohjaiset estosuodattimet",
|
||||
"form.feed.label.category": "Kategoria",
|
||||
"form.feed.label.cookie": "Aseta evästeet",
|
||||
"form.feed.label.crawler": "Nouda alkuperäinen sisältö",
|
||||
"form.feed.label.description": "Kuvaus",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.disable_http2": "Poista HTTP/2 käytöstä sormenjälkien välttämiseksi",
|
||||
"form.feed.label.disabled": "Älä päivitä tätä syötettä",
|
||||
"form.feed.label.feed_password": "Syötteen salasana",
|
||||
"form.feed.label.feed_url": "Syötteen URL-osoite",
|
||||
@@ -186,43 +186,43 @@
|
||||
"form.feed.label.ignore_http_cache": "Ohita HTTP-välimuisti",
|
||||
"form.feed.label.keep_filter_entry_rules": "Merkinnän sallimissäännöt",
|
||||
"form.feed.label.keeplist_rules": "Regex-pohjaiset säilytyssuodattimet",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.no_media_player": "Ei mediasoitinta (ääni/video)",
|
||||
"form.feed.label.ntfy_activate": "Lähetä merkinnät ntfy-palveluun",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy-oletusprioriteetti",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy-korkea prioriteetti",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy-matala prioriteetti",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy-enimmäisprioriteetti",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy-vähimmäisprioriteetti",
|
||||
"form.feed.label.ntfy_priority": "Ntfy-prioriteetti",
|
||||
"form.feed.label.ntfy_topic": "Ntfy-aihe (valinnainen)",
|
||||
"form.feed.label.proxy_url": "Välityspalvelimen URL",
|
||||
"form.feed.label.pushover_activate": "Lähetä merkinnät pushover.net-palveluun",
|
||||
"form.feed.label.pushover_default_priority": "Pushover-oletusprioriteetti",
|
||||
"form.feed.label.pushover_high_priority": "Pushover-korkea prioriteetti",
|
||||
"form.feed.label.pushover_low_priority": "Pushover-matala prioriteetti",
|
||||
"form.feed.label.pushover_max_priority": "Pushover-enimmäisprioriteetti",
|
||||
"form.feed.label.pushover_min_priority": "Pushover-vähimmäisprioriteetti",
|
||||
"form.feed.label.pushover_priority": "Pushover-viestin prioriteetti",
|
||||
"form.feed.label.rewrite_rules": "Sisällön uudelleenkirjoitussäännöt",
|
||||
"form.feed.label.scraper_rules": "Scraper-säännöt",
|
||||
"form.feed.label.site_url": "Sivuston URL-osoite",
|
||||
"form.feed.label.title": "Otsikko",
|
||||
"form.feed.label.urlrewrite_rules": "URL-osoitteen uudelleenkirjoitussäännöt",
|
||||
"form.feed.label.user_agent": "Ohita oletuskäyttäjäagentti",
|
||||
"form.feed.label.webhook_url": "Override webhook url",
|
||||
"form.feed.label.webhook_url": "Ohita oletus-webhook-osoite",
|
||||
"form.import.label.file": "OPML-tiedosto",
|
||||
"form.import.label.url": "URL",
|
||||
"form.import.label.url": "URL-osoite",
|
||||
"form.integration.archiveorg_activate": "Työnnä merkinnät osoitteeseen archive.org",
|
||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||
"form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.integration.discord_activate": "Push entries to Discord",
|
||||
"form.integration.discord_webhook_link": "Discord Webhook link",
|
||||
"form.integration.apprise_activate": "Lähetä merkinnät Appriseen",
|
||||
"form.integration.apprise_services_url": "Pilkuilla eroteltu Apprise-palvelujen URL-lista",
|
||||
"form.integration.apprise_url": "Apprise API -osoite",
|
||||
"form.integration.betula_activate": "Tallenna merkinnät Betulaan",
|
||||
"form.integration.betula_token": "Betula-tunnus",
|
||||
"form.integration.betula_url": "Betula-palvelimen URL",
|
||||
"form.integration.cubox_activate": "Tallenna merkinnät Cuboxiin",
|
||||
"form.integration.cubox_api_link": "Cubox API -linkki",
|
||||
"form.integration.discord_activate": "Lähetä merkinnät Discordiin",
|
||||
"form.integration.discord_webhook_link": "Discord-webhook-linkki",
|
||||
"form.integration.espial_activate": "Tallenna artikkelit Espialiin",
|
||||
"form.integration.espial_api_key": "Espial API-avain",
|
||||
"form.integration.espial_endpoint": "Espial API-päätepiste",
|
||||
@@ -241,47 +241,48 @@
|
||||
"form.integration.karakeep_activate": "Tallenna artikkelit Karakeepiin",
|
||||
"form.integration.karakeep_api_key": "Karakeep API-avain",
|
||||
"form.integration.karakeep_url": "Karakeep API-päätepiste",
|
||||
"form.integration.karakeep_tags": "Karakeep Tags",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_is_private": "Mark link as private",
|
||||
"form.integration.linkace_tags": "LinkAce Tags",
|
||||
"form.integration.karakeep_tags": "Karakeep-tunnisteet",
|
||||
"form.integration.linkace_activate": "Tallenna merkinnät LinkAceen",
|
||||
"form.integration.linkace_api_key": "LinkAce API -avain",
|
||||
"form.integration.linkace_check_disabled": "Poista linkkitarkistus käytöstä",
|
||||
"form.integration.linkace_endpoint": "LinkAce API -päätepiste",
|
||||
"form.integration.linkace_is_private": "Merkitse linkki yksityiseksi",
|
||||
"form.integration.linkace_tags": "LinkAce-tunnisteet",
|
||||
"form.integration.linkding_activate": "Tallenna artikkelit Linkkiin",
|
||||
"form.integration.linkding_api_key": "Linkding API-avain",
|
||||
"form.integration.linkding_bookmark": "Merkitse kirjanmerkki lukemattomaksi",
|
||||
"form.integration.linkding_endpoint": "Linkding API-päätepiste",
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkding_tags": "Linkding-tunnisteet",
|
||||
"form.integration.linktaco_activate": "Tallenna kirjoituksia LinkTacoon",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token_hint": "Hanki henkilökohtainen pääsytunnistesi osoitteesta",
|
||||
"form.integration.linktaco_org_slug": "Organization Slug",
|
||||
"form.integration.linktaco_tags": "Tagit (enintään 10, pilkuilla erotettu)",
|
||||
"form.integration.linktaco_tags_hint": "Enintään 10 tagia, pilkuilla erotettu",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API -tunnus",
|
||||
"form.integration.linktaco_api_token_hint": "Hanki henkilökohtainen pääsytunniste osoitteesta",
|
||||
"form.integration.linktaco_org_slug": "Organisaation slug",
|
||||
"form.integration.linktaco_tags": "Tunnisteet (enintään 10, pilkuilla eroteltu)",
|
||||
"form.integration.linktaco_tags_hint": "Enintään 10 tunnistetta, pilkuilla eroteltuna",
|
||||
"form.integration.linktaco_visibility": "Näkyvyys",
|
||||
"form.integration.linktaco_visibility_public": "Julkinen",
|
||||
"form.integration.linktaco_visibility_private": "Yksityinen",
|
||||
"form.integration.linktaco_visibility_hint": "YKSITYINEN näkyvyys vaatii maksullisen LinkTaco-tilin",
|
||||
"form.integration.linkwarden_activate": "Tallenna artikkelit Linkkiin",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API-avain",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden Base URL",
|
||||
"form.integration.linktaco_visibility_hint": "Yksityinen näkyvyys vaatii maksullisen LinkTaco-tilin",
|
||||
"form.integration.linkwarden_activate": "Tallenna artikkelit Linkwardeniin",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API -avain",
|
||||
"form.integration.linkwarden_endpoint": "Linkwardenin perus-URL",
|
||||
"form.integration.linkwarden_collection_id": "Linkwarden-kokoelman tunnus",
|
||||
"form.integration.matrix_bot_activate": "Siirrä uudet artikkelit Matrixiin",
|
||||
"form.integration.matrix_bot_chat_id": "Matrix-huoneen tunnus",
|
||||
"form.integration.matrix_bot_password": "Matrix-käyttäjän salasana",
|
||||
"form.integration.matrix_bot_url": "Matrix-palvelimen URL-osoite",
|
||||
"form.integration.matrix_bot_user": "Matrixin käyttäjätunnus",
|
||||
"form.integration.notion_activate": "Save entries to Notion",
|
||||
"form.integration.notion_page_id": "Notion Page ID",
|
||||
"form.integration.notion_token": "Notion Secret Token",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_topic": "Ntfy topic (default used if not set in feed)",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.notion_activate": "Tallenna merkinnät Notioniin",
|
||||
"form.integration.notion_page_id": "Notion-sivun tunnus",
|
||||
"form.integration.notion_token": "Notion-salaisuustunnus",
|
||||
"form.integration.ntfy_activate": "Lähetä merkinnät ntfy-palveluun",
|
||||
"form.integration.ntfy_api_token": "Ntfy API -tunnus (valinnainen)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy-kuvakkeen URL (valinnainen)",
|
||||
"form.integration.ntfy_internal_links": "Käytä sisäisiä linkkejä napsautettaessa (valinnainen)",
|
||||
"form.integration.ntfy_password": "Ntfy-salasana (valinnainen)",
|
||||
"form.integration.ntfy_topic": "Ntfy-aihe (oletus jos ei määritetty syötteessä)",
|
||||
"form.integration.ntfy_url": "Ntfy-URL (valinnainen, oletus ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Ntfy-käyttäjätunnus (valinnainen)",
|
||||
"form.integration.nunux_keeper_activate": "Tallenna artikkelit Nunux Keeperiin",
|
||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper API-avain",
|
||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API-päätepiste",
|
||||
@@ -292,59 +293,60 @@
|
||||
"form.integration.pinboard_bookmark": "Merkitse kirjanmerkki lukemattomaksi",
|
||||
"form.integration.pinboard_tags": "Pinboard-tagit",
|
||||
"form.integration.pinboard_token": "Pinboard API-tunnus",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
"form.integration.pushover_token": "Pushover application API token",
|
||||
"form.integration.pushover_user": "Pushover user key",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.pushover_activate": "Lähetä merkinnät Pushoveriin",
|
||||
"form.integration.pushover_device": "Pushover-laite (valinnainen)",
|
||||
"form.integration.pushover_prefix": "Pushover-URL:n etuliite (valinnainen)",
|
||||
"form.integration.pushover_token": "Pushover-sovelluksen API-tunnus",
|
||||
"form.integration.pushover_user": "Pushover-käyttäjän avain",
|
||||
"form.integration.raindrop_activate": "Tallenna merkinnät Raindropiin",
|
||||
"form.integration.raindrop_collection_id": "Kokoelman tunnus",
|
||||
"form.integration.raindrop_tags": "Tunnisteet (pilkuilla eroteltu)",
|
||||
"form.integration.raindrop_token": "(Testi) tunnus",
|
||||
"form.integration.readeck_activate": "Tallenna artikkelit Readeckiin",
|
||||
"form.integration.readeck_api_key": "Readeck API-avain",
|
||||
"form.integration.readeck_endpoint": "Readeck API-päätepiste",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_labels": "Readeck-tunnisteet",
|
||||
"form.integration.readeck_only_url": "Lähetä vain URL-osoite (koko sisällön sijaan)",
|
||||
"form.integration.readwise_activate": "Save entries to Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shiori_activate": "Save articles to Shiori",
|
||||
"form.integration.shiori_endpoint": "Shiori API Endpoint",
|
||||
"form.integration.shiori_password": "Shiori Password",
|
||||
"form.integration.shiori_username": "Shiori Username",
|
||||
"form.integration.slack_activate": "Push entries to Slack",
|
||||
"form.integration.slack_webhook_link": "Slack Webhook link",
|
||||
"form.integration.readeck_push_activate": "Lähetä uudet merkinnät automaattisesti Readeckiin",
|
||||
"form.integration.readwise_activate": "Tallenna merkinnät Readwise Readeriin",
|
||||
"form.integration.readwise_api_key": "Readwise Reader -pääsytunnus",
|
||||
"form.integration.readwise_api_key_link": "Hanki Readwise-pääsytunnus",
|
||||
"form.integration.rssbridge_activate": "Tarkista RSS-Bridge tilauksia lisättäessä",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge-todennustunnus",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge-palvelimen URL",
|
||||
"form.integration.shaarli_activate": "Tallenna artikkelit Shaarliin",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API -salaisuus",
|
||||
"form.integration.shaarli_endpoint": "Shaarli-osoite",
|
||||
"form.integration.shiori_activate": "Tallenna artikkelit Shioriin",
|
||||
"form.integration.shiori_endpoint": "Shiori API -päätepiste",
|
||||
"form.integration.shiori_password": "Shiori-salasana",
|
||||
"form.integration.shiori_username": "Shiori-käyttäjätunnus",
|
||||
"form.integration.slack_activate": "Lähetä merkinnät Slackiin",
|
||||
"form.integration.slack_webhook_link": "Slack-webhook-linkki",
|
||||
"form.integration.telegram_bot_activate": "Lähetä uusia artikkeleita Telegram-chatiin",
|
||||
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||
"form.integration.telegram_bot_disable_notification": "Disable notification",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
|
||||
"form.integration.telegram_bot_disable_buttons": "Poista painikkeet käytöstä",
|
||||
"form.integration.telegram_bot_disable_notification": "Poista ilmoitukset käytöstä",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Poista sivun esikatselu käytöstä",
|
||||
"form.integration.telegram_bot_token": "Bot-tunnus",
|
||||
"form.integration.telegram_chat_id": "Chat ID",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
"form.integration.telegram_chat_id": "Keskustelun tunnus",
|
||||
"form.integration.telegram_topic_id": "Aiheen tunnus",
|
||||
"form.integration.wallabag_activate": "Tallenna artikkelit Wallabagiin",
|
||||
"form.integration.wallabag_client_id": "Wallabag Client ID",
|
||||
"form.integration.wallabag_client_secret": "Wallabag Client Secret",
|
||||
"form.integration.wallabag_client_id": "Wallabag-asiakastunnus",
|
||||
"form.integration.wallabag_client_secret": "Wallabag-asiakassalaisuus",
|
||||
"form.integration.wallabag_endpoint": "Wallabagin perus-URL-osoite",
|
||||
"form.integration.wallabag_only_url": "Lähetä vain URL-osoite (koko sisällön sijaan)",
|
||||
"form.integration.wallabag_password": "Wallabag-salasana",
|
||||
"form.integration.wallabag_username": "Wallabag-käyttäjätunnus",
|
||||
"form.integration.wallabag_tags": "Wallabag Tags",
|
||||
"form.integration.webhook_activate": "Enable Webhooks",
|
||||
"form.integration.webhook_secret": "Webhooks Secret",
|
||||
"form.integration.webhook_url": "Default Webhook URL",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Read articles by opening external links",
|
||||
"form.integration.wallabag_tags": "Wallabag-tunnisteet",
|
||||
"form.integration.webhook_activate": "Ota webhookit käyttöön",
|
||||
"form.integration.webhook_secret": "Webhookien salaisuus",
|
||||
"form.integration.webhook_url": "Oletus-webhook-URL",
|
||||
"form.prefs.fieldset.application_settings": "Sovellusasetukset",
|
||||
"form.prefs.fieldset.authentication_settings": "Todennusasetukset",
|
||||
"form.prefs.fieldset.global_feed_settings": "Syötteiden yleisasetukset",
|
||||
"form.prefs.fieldset.reader_settings": "Lukija-asetukset",
|
||||
"form.prefs.help.external_font_hosts": "Sallittujen ulkoisten fonttipalvelinten lista välilyönnein eroteltuna. Esimerkiksi: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Lue artikkelit avaamalla ulkoiset linkit",
|
||||
"form.prefs.label.categories_sorting_order": "Kategorioiden lajittelu",
|
||||
"form.prefs.label.cjk_reading_speed": "Kiinan, Korean ja Japanin lukunopeus (merkkejä minuutissa)",
|
||||
"form.prefs.label.custom_css": "Mukautettu CSS",
|
||||
@@ -356,14 +358,14 @@
|
||||
"form.prefs.label.entry_order": "Lajittele sarakkeen mukaan",
|
||||
"form.prefs.label.entry_sorting": "Lajittelu",
|
||||
"form.prefs.label.entry_swipe": "Ota syöttöpyyhkäisy käyttöön kosketusnäytöissä",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.label.external_font_hosts": "Ulkoiset fonttipalvelimet",
|
||||
"form.prefs.label.gesture_nav": "Ele siirtyäksesi merkintöjen välillä",
|
||||
"form.prefs.label.keyboard_shortcuts": "Ota pikanäppäimet käyttöön",
|
||||
"form.prefs.label.language": "Kieli",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Merkitse merkinnät luetuiksi manuaalisesti",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Merkitse luetuksi vasta, kun ääni/video on 90%% toistettu",
|
||||
"form.prefs.label.mark_read_on_view": "Merkitse kohdat automaattisesti luetuiksi, kun niitä tarkastellaan",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Merkitse merkinnät luetuiksi katsottaessa. Ääni/videolle merkitse 90%% toistettuna",
|
||||
"form.prefs.label.media_playback_rate": "Äänen/videon toistonopeus",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Avaa ulkoiset linkit uuteen välilehteen (lisää target=\"_blank\" linkkeihin)",
|
||||
"form.prefs.label.show_reading_time": "Näytä artikkeleiden arvioitu lukuaika",
|
||||
@@ -419,14 +421,14 @@
|
||||
"menu.show_only_starred_entries": "Näytä vain suosikit",
|
||||
"menu.show_only_unread_entries": "Näytä vain lukemattomat artikkelit",
|
||||
"menu.starred": "Suosikit",
|
||||
"menu.title": "Menu",
|
||||
"menu.title": "Valikko",
|
||||
"menu.unread": "Lukemattomat",
|
||||
"menu.users": "Käyttäjät",
|
||||
"page.about.author": "Tekijä:",
|
||||
"page.about.build_date": "Valmistuspäivä:",
|
||||
"page.about.credits": "Kiitokset",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.db_usage": "Tietokannan koko:",
|
||||
"page.about.git_commit": "Git-toimite:",
|
||||
"page.about.global_config_options": "Yleiset asetukset",
|
||||
"page.about.go_version": "Go-versio:",
|
||||
"page.about.license": "Lisenssi:",
|
||||
@@ -455,10 +457,10 @@
|
||||
"page.categories.no_feed": "Ei syötettä.",
|
||||
"page.categories.title": "Kategoriat",
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories"
|
||||
"%d kategoria",
|
||||
"%d kategoriaa"
|
||||
],
|
||||
"page.category_label": "Category: %s",
|
||||
"page.category_label": "Kategoria: %s",
|
||||
"page.edit_category.title": "Muokkaa kategoria: %s",
|
||||
"page.edit_feed.etag_header": "ETag-otsikko:",
|
||||
"page.edit_feed.last_check": "Viimeisin tarkistus:",
|
||||
@@ -473,17 +475,17 @@
|
||||
"%d virhettä"
|
||||
],
|
||||
"page.feeds.last_check": "Viimeisin tarkistus:",
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.next_check": "Seuraava tarkistus:",
|
||||
"page.feeds.read_counter": "Luettujen artikkeleiden määrä",
|
||||
"page.feeds.title": "Syötteet",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "Takaisin ylös",
|
||||
"page.history.title": "Historia",
|
||||
"page.import.title": "Tuo",
|
||||
"page.integration.bookmarklet": "Sovelluskirjanmerkki",
|
||||
"page.integration.bookmarklet.help": "This special link allows you to subscribe to a website directly by using a bookmark in your web browser.",
|
||||
"page.integration.bookmarklet.help": "Tämä erityinen linkki antaa sinun tilata verkkosivuston suoraan selaimen kirjanmerkillä.",
|
||||
"page.integration.bookmarklet.instructions": "Vedä ja pudota tämä linkki kirjanmerkkeihisi.",
|
||||
"page.integration.bookmarklet.name": "Lisää Minifluxiin",
|
||||
"page.integration.miniflux_api": "Miniflux API",
|
||||
"page.integration.miniflux_api": "Minifluxin API",
|
||||
"page.integration.miniflux_api_endpoint": "API-päätepiste",
|
||||
"page.integration.miniflux_api_password": "Salasana",
|
||||
"page.integration.miniflux_api_password_value": "Tilisi salasana",
|
||||
@@ -522,7 +524,7 @@
|
||||
"page.keyboard_shortcuts.subtitle.sections": "Osion navigointi",
|
||||
"page.keyboard_shortcuts.title": "Pikanäppäimet",
|
||||
"page.keyboard_shortcuts.toggle_star_status": "Vaihda kirjanmerkki",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Avaa tai sulje merkinnän liitteet",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "Vaihda luettu/lukematon, keskity seuraavaksi",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Vaihda luettu/lukematon, keskity edelliseen",
|
||||
"page.login.google_signin": "Kirjaudu sisään Googlella",
|
||||
@@ -530,7 +532,7 @@
|
||||
"page.login.title": "Kirjaudu sisään",
|
||||
"page.login.webauthn_login": "Kirjaudu sisään salasanalla",
|
||||
"page.login.webauthn_login.error": "Ei voida kirjautua sisään salasanalla",
|
||||
"page.login.webauthn_login.help": "Please enter your username if you're using a security key. This is not required if you are using a Passkey (discoverable credentials).",
|
||||
"page.login.webauthn_login.help": "Jos käytät turva-avainta, kirjoita käyttäjätunnus. Passkeytä käyttäessä tämä ei ole tarpeen.",
|
||||
"page.new_api_key.title": "Uusi API-avain",
|
||||
"page.new_category.title": "Uusi kategoria",
|
||||
"page.new_user.title": "Uusi käyttäjä",
|
||||
@@ -538,8 +540,8 @@
|
||||
"page.offline.refresh_page": "Yritä päivittää sivu",
|
||||
"page.offline.title": "Offline-tila",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries"
|
||||
"%d luettu merkintä",
|
||||
"%d luettua merkintää"
|
||||
],
|
||||
"page.search.title": "Hakutulokset",
|
||||
"page.sessions.table.actions": "Toiminnot",
|
||||
@@ -553,35 +555,35 @@
|
||||
"page.settings.title": "Asetukset",
|
||||
"page.settings.unlink_google_account": "Poista Google-tilini linkitys",
|
||||
"page.settings.unlink_oidc_account": "Poista %s -tilini linkitys",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.added_on": "Added On",
|
||||
"page.settings.webauthn.actions": "Toiminnot",
|
||||
"page.settings.webauthn.added_on": "Lisätty",
|
||||
"page.settings.webauthn.delete": [
|
||||
"Poista %d salasana",
|
||||
"Poista %d salasanaa"
|
||||
],
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.last_seen_on": "Viimeksi käytetty",
|
||||
"page.settings.webauthn.passkey_name": "Passkey-nimi",
|
||||
"page.settings.webauthn.passkeys": "Passkeyt",
|
||||
"page.settings.webauthn.register": "Rekisteröi salasana",
|
||||
"page.settings.webauthn.register.error": "Salasanaa ei voi rekisteröidä",
|
||||
"page.shared_entries.title": "Jaetut artikkelit",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries"
|
||||
"%d jaettu merkintä",
|
||||
"%d jaettua merkintää"
|
||||
],
|
||||
"page.starred.title": "Suosikit",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries"
|
||||
"%d suosikkimerkintä",
|
||||
"%d suosikkimerkintää"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total"
|
||||
"Yhteensä %d merkintä",
|
||||
"Yhteensä %d merkintää"
|
||||
],
|
||||
"page.unread.title": "Lukemattomat",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries"
|
||||
"%d lukematon merkintä",
|
||||
"%d lukematonta merkintää"
|
||||
],
|
||||
"page.users.actions": "Toiminnot",
|
||||
"page.users.admin.no": "Ei",
|
||||
@@ -591,7 +593,7 @@
|
||||
"page.users.never_logged": "Ei koskaan",
|
||||
"page.users.title": "Käyttäjät",
|
||||
"page.users.username": "Käyttäjätunnus",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"page.webauthn_rename.title": "Nimeä passkey uudelleen",
|
||||
"pagination.first": "Ensimmäinen",
|
||||
"pagination.last": "Viimeinen",
|
||||
"pagination.next": "Seuraava",
|
||||
@@ -629,4 +631,4 @@
|
||||
"time_elapsed.yesterday": "eilen",
|
||||
"tooltip.keyboard_shortcuts": "Pikanäppäin: %s",
|
||||
"tooltip.logged_user": "Kirjautunut %s-käyttäjänä"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,15 +257,16 @@
|
||||
"form.integration.linktaco_api_token": "Token API LinkTaco",
|
||||
"form.integration.linktaco_api_token_hint": "Obtenez votre token d'accès personnel sur",
|
||||
"form.integration.linktaco_org_slug": "Slug de l'organisation",
|
||||
"form.integration.linktaco_tags": "Tags (max. 10, séparés par des virgules)",
|
||||
"form.integration.linktaco_tags_hint": "Maximum 10 tags, séparés par des virgules",
|
||||
"form.integration.linktaco_tags": "Libellés (max. 10, séparés par des virgules)",
|
||||
"form.integration.linktaco_tags_hint": "Maximum 10 libellés, séparés par des virgules",
|
||||
"form.integration.linktaco_visibility": "Visibilité",
|
||||
"form.integration.linktaco_visibility_public": "Public",
|
||||
"form.integration.linktaco_visibility_public": "Publique",
|
||||
"form.integration.linktaco_visibility_private": "Privé",
|
||||
"form.integration.linktaco_visibility_hint": "La visibilité PRIVÉE nécessite un compte LinkTaco payant",
|
||||
"form.integration.linkwarden_activate": "Sauvegarder les articles vers Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Clé d'API de Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "URL de base de Linkwarden",
|
||||
"form.integration.linkwarden_collection_id": "ID de collection Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Envoyer les nouveaux articles vers Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Identifiant de la salle Matrix",
|
||||
"form.integration.matrix_bot_password": "Mot de passe de l'utilisateur Matrix",
|
||||
@@ -306,6 +307,7 @@
|
||||
"form.integration.readeck_endpoint": "URL de l'API de Readeck",
|
||||
"form.integration.readeck_labels": "Libellés Readeck",
|
||||
"form.integration.readeck_only_url": "Envoyer uniquement l'URL (au lieu du contenu complet)",
|
||||
"form.integration.readeck_push_activate": "Envoyer automatiquement les nouvelles entrées vers Readeck",
|
||||
"form.integration.readwise_activate": "Enregistrer les entrées vers Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Jeton d'accès au lecteur Readwise",
|
||||
"form.integration.readwise_api_key_link": "Obtenez votre jeton d'accès Readwise",
|
||||
@@ -373,7 +375,7 @@
|
||||
"form.prefs.select.browser": "Navigateur",
|
||||
"form.prefs.select.created_time": "Heure de création de l'entrée",
|
||||
"form.prefs.select.fullscreen": "Plein écran",
|
||||
"form.prefs.select.minimal_ui": "Minimal",
|
||||
"form.prefs.select.minimal_ui": "Minimaliste",
|
||||
"form.prefs.select.none": "Aucun",
|
||||
"form.prefs.select.older_first": "Anciens éléments en premier",
|
||||
"form.prefs.select.publish_time": "Heure de publication de l'entrée",
|
||||
@@ -479,7 +481,7 @@
|
||||
"page.footer.elevator": "Retour en haut",
|
||||
"page.history.title": "Historique",
|
||||
"page.import.title": "Importation",
|
||||
"page.integration.bookmarklet": "Bookmarklet",
|
||||
"page.integration.bookmarklet": "Signet (bookmarklet)",
|
||||
"page.integration.bookmarklet.help": "Ce lien spécial vous permet de vous abonner à un site web directement en utilisant un marque page dans votre navigateur web.",
|
||||
"page.integration.bookmarklet.instructions": "Glisser-déposer ce lien dans vos favoris.",
|
||||
"page.integration.bookmarklet.name": "Ajouter à Miniflux",
|
||||
@@ -629,4 +631,4 @@
|
||||
"time_elapsed.yesterday": "hier",
|
||||
"tooltip.keyboard_shortcuts": "Raccourci clavier : %s",
|
||||
"tooltip.logged_user": "Connecté en tant que %s"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,14 +109,14 @@
|
||||
"error.http_empty_response": "HTTP प्रतिक्रिया खाली है। शायद यह वेबसाइट बॉट सुरक्षा तंत्र का उपयोग कर रही है?",
|
||||
"error.http_empty_response_body": "HTTP प्रतिक्रिया बॉडी खाली है।",
|
||||
"error.http_forbidden": "इस वेबसाइट तक पहुंच वर्जित है। शायद इस वेबसाइट में बॉट सुरक्षा तंत्र है?",
|
||||
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||
"error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).",
|
||||
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_gateway_timeout": "गेटवे टाइमआउट त्रुटि के कारण वेबसाइट फिलहाल उपलब्ध नहीं है। समस्या मिनीफ्लक्स की तरफ नहीं है। कृपया बाद में पुनः प्रयास करें।",
|
||||
"error.http_internal_server_error": "सर्वर त्रुटि के कारण वेबसाइट फिलहाल उपलब्ध नहीं है। समस्या मिनीफ्लक्स की तरफ नहीं है। कृपया बाद में पुनः प्रयास करें।",
|
||||
"error.http_not_authorized": "इस वेबसाइट तक पहुंच अधिकृत नहीं है। यह गलत उपयोगकर्ता नाम या पासवर्ड हो सकता है।",
|
||||
"error.http_resource_not_found": "अनुरोधित संसाधन नहीं मिला। कृपया यूआरएल सत्यापित करें।",
|
||||
"error.http_response_too_large": "HTTP प्रतिक्रिया बहुत बड़ी है। आप वैश्विक सेटिंग्स में HTTP प्रतिक्रिया आकार सीमा बढ़ा सकते हैं (सर्वर पुनःआरंभ आवश्यक)।",
|
||||
"error.http_service_unavailable": "आंतरिक सर्वर त्रुटि के कारण वेबसाइट फिलहाल उपलब्ध नहीं है। समस्या मिनीफ्लक्स की तरफ नहीं है। कृपया बाद में पुनः प्रयास करें।",
|
||||
"error.http_too_many_requests": "मिनीफ्लक्स ने इस वेबसाइट पर बहुत अधिक अनुरोध भेजे हैं। कृपया बाद में पुनः प्रयास करें या एप्लिकेशन कॉन्फ़िगरेशन बदलें।",
|
||||
"error.http_unexpected_status_code": "अप्रत्याशित HTTP स्थिति कोड %d के कारण वेबसाइट उपलब्ध नहीं है। समस्या मिनीफ्लक्स की तरफ नहीं है। कृपया बाद में पुनः प्रयास करें।",
|
||||
"error.invalid_categories_sorting_order": "अमान्य श्रेणी क्रम।",
|
||||
"error.invalid_default_home_page": "अमान्य डिफ़ॉल्ट मुखपृष्ठ!",
|
||||
"error.invalid_display_mode": "अमान्य वेब ऐप्लिकेशन प्रदर्शन मोड.",
|
||||
@@ -129,26 +129,26 @@
|
||||
"error.invalid_site_url": "अमान्य साइट यूआरएल",
|
||||
"error.invalid_theme": "अमान्य थीम.",
|
||||
"error.invalid_timezone": "अमान्य समयक्षेत्र.",
|
||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||
"error.network_operation": "नेटवर्क त्रुटि के कारण मिनीफ्लक्स इस वेबसाइट तक नहीं पहुँच पा रहा: %v.",
|
||||
"error.network_timeout": "यह वेबसाइट बहुत धीमी है और अनुरोध का समय समाप्त हो गया: %v",
|
||||
"error.password_min_length": "पासवर्ड में कम से कम 6 अक्षर होने चाहिए।",
|
||||
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.proxy_url_not_empty": "प्रॉक्सी यूआरएल खाली नहीं हो सकता।",
|
||||
"error.settings_block_rule_fieldname_invalid": "अमान्य ब्लॉक नियम: नियम #%d में मान्य फील्ड नाम नहीं है (विकल्प: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "अमान्य ब्लॉक नियम: नियम #%d का पैटर्न मान्य रेगेक्स नहीं है",
|
||||
"error.settings_block_rule_regex_required": "अमान्य ब्लॉक नियम: नियम #%d का पैटर्न प्रदान नहीं किया गया",
|
||||
"error.settings_block_rule_separator_required": "अमान्य ब्लॉक नियम: नियम #%d के पैटर्न को '=' द्वारा अलग होना आवश्यक है",
|
||||
"error.settings_invalid_domain_list": "अमान्य डोमेन सूची। कृपया स्पेस से अलग किए गए डोमेन दें।",
|
||||
"error.settings_keep_rule_fieldname_invalid": "अमान्य रखने का नियम: नियम #%d में मान्य फील्ड नाम नहीं है (विकल्प: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "अमान्य रखने का नियम: नियम #%d का पैटर्न मान्य रेगेक्स नहीं है",
|
||||
"error.settings_keep_rule_regex_required": "अमान्य रखने का नियम: नियम #%d का पैटर्न नहीं दिया गया",
|
||||
"error.settings_keep_rule_separator_required": "अमान्य रखने का नियम: नियम #%d के पैटर्न को '=' से अलग किया जाना आवश्यक है",
|
||||
"error.settings_mandatory_fields": "उपयोगकर्ता नाम, विषयवस्तु, भाषा और समयक्षेत्र फ़ील्ड अनिवार्य हैं।",
|
||||
"error.settings_media_playback_rate_range": "प्लेबैक गति सीमा से बाहर है",
|
||||
"error.settings_reading_speed_is_positive": "पढ़ने की गति सकारात्मक पूर्णांक होनी चाहिए।",
|
||||
"error.site_url_not_empty": "साइट का यूआरएल खाली नहीं हो सकता.",
|
||||
"error.subscription_not_found": "कोई सदस्यता ढूँढने में असमर्थ.",
|
||||
"error.title_required": "शीर्षक अनिवार्य है।",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "TLS त्रुटि: %q. यदि आप चाहें तो फ़ीड सेटिंग्स में TLS सत्यापन अक्षम कर सकते हैं।",
|
||||
"error.unable_to_create_api_key": "यह एपीआई कुंजी बनाने में असमर्थ।",
|
||||
"error.unable_to_create_category": "यह श्रेणी बनाने में असमर्थ.",
|
||||
"error.unable_to_create_user": "इस उपयोगकर्ता को बनाने में असमर्थ।",
|
||||
@@ -164,19 +164,19 @@
|
||||
"form.api_key.label.description": "एपीआई कुंजी लेबल",
|
||||
"form.category.hide_globally": "वैश्विक अपठित सूची में प्रविष्टियां छिपाएं",
|
||||
"form.category.label.title": "शीर्षक",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.integration": "Third-Party Services",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.general": "सामान्य",
|
||||
"form.feed.fieldset.integration": "तृतीय-पक्ष सेवाएँ",
|
||||
"form.feed.fieldset.network_settings": "नेटवर्क सेटिंग्स",
|
||||
"form.feed.fieldset.rules": "नियम",
|
||||
"form.feed.label.allow_self_signed_certificates": "स्व-हस्ताक्षरित या अमान्य प्रमाणपत्रों की अनुमति दें",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.apprise_service_urls": "Apprise सेवा URL की कॉमा से अलग सूची",
|
||||
"form.feed.label.block_filter_entry_rules": "प्रविष्टि अवरोधन नियम",
|
||||
"form.feed.label.blocklist_rules": "रेगेक्स-आधारित अवरोधन फिल्टर",
|
||||
"form.feed.label.category": "श्रेणी",
|
||||
"form.feed.label.cookie": "कुकीज़ सेट करें",
|
||||
"form.feed.label.crawler": "मूल सामग्री प्राप्त करें",
|
||||
"form.feed.label.description": "विवरण",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.disable_http2": "फिंगरप्रिंटिंग से बचने के लिए HTTP/2 अक्षम करें",
|
||||
"form.feed.label.disabled": "इस फ़ीड को रीफ़्रेश न करें",
|
||||
"form.feed.label.feed_password": "फ़ीड पासवर्ड",
|
||||
"form.feed.label.feed_url": "फ़ीड यूआरएल",
|
||||
@@ -186,43 +186,43 @@
|
||||
"form.feed.label.ignore_http_cache": "एचटीटीपी कैश पर ध्यान न दें",
|
||||
"form.feed.label.keep_filter_entry_rules": "प्रविष्टि अनुमति नियम",
|
||||
"form.feed.label.keeplist_rules": "रेगेक्स-आधारित रखने वाले फिल्टर",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.no_media_player": "कोई मीडिया प्लेयर नहीं (ऑडियो/वीडियो)",
|
||||
"form.feed.label.ntfy_activate": "प्रविष्टियाँ ntfy पर भेजें",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy डिफ़ॉल्ट प्राथमिकता",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy उच्च प्राथमिकता",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy निम्न प्राथमिकता",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy अधिकतम प्राथमिकता",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy न्यूनतम प्राथमिकता",
|
||||
"form.feed.label.ntfy_priority": "Ntfy प्राथमिकता",
|
||||
"form.feed.label.ntfy_topic": "Ntfy विषय (वैकल्पिक)",
|
||||
"form.feed.label.proxy_url": "प्रॉक्सी URL",
|
||||
"form.feed.label.pushover_activate": "प्रविष्टियाँ pushover.net पर भेजें",
|
||||
"form.feed.label.pushover_default_priority": "Pushover डिफ़ॉल्ट प्राथमिकता",
|
||||
"form.feed.label.pushover_high_priority": "Pushover उच्च प्राथमिकता",
|
||||
"form.feed.label.pushover_low_priority": "Pushover निम्न प्राथमिकता",
|
||||
"form.feed.label.pushover_max_priority": "Pushover अधिकतम प्राथमिकता",
|
||||
"form.feed.label.pushover_min_priority": "Pushover न्यूनतम प्राथमिकता",
|
||||
"form.feed.label.pushover_priority": "Pushover संदेश प्राथमिकता",
|
||||
"form.feed.label.rewrite_rules": "सामग्री पुनर्लेखन नियम",
|
||||
"form.feed.label.scraper_rules": "खुरचनी नियम",
|
||||
"form.feed.label.site_url": "साइट यूआरएल",
|
||||
"form.feed.label.title": "शीर्षक",
|
||||
"form.feed.label.urlrewrite_rules": " यूआरएल पुनर्लेखन नियम",
|
||||
"form.feed.label.user_agent": "डिफ़ॉल्ट उपयोगकर्ता एजेंट को ओवरराइड करें",
|
||||
"form.feed.label.webhook_url": "Override webhook url",
|
||||
"form.feed.label.webhook_url": "वेबहुक URL को अधिलेखित करें",
|
||||
"form.import.label.file": "ओपीएमएल फ़ाइल",
|
||||
"form.import.label.url": "यूआरएल",
|
||||
"form.integration.archiveorg_activate": "प्रविष्टियों को archive.org पर भेजें",
|
||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||
"form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.integration.discord_activate": "Push entries to Discord",
|
||||
"form.integration.discord_webhook_link": "Discord Webhook link",
|
||||
"form.integration.apprise_activate": "प्रविष्टियाँ Apprise पर भेजें",
|
||||
"form.integration.apprise_services_url": "Apprise सेवा URLs की कॉमा से पृथक सूची",
|
||||
"form.integration.apprise_url": "Apprise API यूआरएल",
|
||||
"form.integration.betula_activate": "प्रविष्टियाँ Betula में सहेजें",
|
||||
"form.integration.betula_token": "Betula टोकन",
|
||||
"form.integration.betula_url": "Betula सर्वर URL",
|
||||
"form.integration.cubox_activate": "प्रविष्टियाँ Cubox में सहेजें",
|
||||
"form.integration.cubox_api_link": "Cubox API लिंक",
|
||||
"form.integration.discord_activate": "प्रविष्टियाँ Discord पर भेजें",
|
||||
"form.integration.discord_webhook_link": "Discord वेबहुक लिंक",
|
||||
"form.integration.espial_activate": "विषय-वस्तु को जासूसी में सहेजें",
|
||||
"form.integration.espial_api_key": "जासूसी एपीआई कुंजी",
|
||||
"form.integration.espial_endpoint": "जासूसी एपीआई समापन बिंदु",
|
||||
@@ -238,96 +238,98 @@
|
||||
"form.integration.instapaper_activate": "विषय-वस्तु को इंस्टापेपर में सहेजें",
|
||||
"form.integration.instapaper_password": "इंस्टापेपर पासवर्ड",
|
||||
"form.integration.instapaper_username": "इंस्टापेपर यूजरनेम",
|
||||
"form.integration.karakeep_activate": "Save entries to Karakeep",
|
||||
"form.integration.karakeep_api_key": "Karakeep API key",
|
||||
"form.integration.karakeep_url": "Karakeep API Endpoint",
|
||||
"form.integration.karakeep_tags": "Karakeep Labels",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_is_private": "Mark link as private",
|
||||
"form.integration.linkace_tags": "LinkAce Tags",
|
||||
"form.integration.karakeep_activate": "प्रविष्टियाँ Karakeep में सहेजें",
|
||||
"form.integration.karakeep_api_key": "Karakeep API कुंजी",
|
||||
"form.integration.karakeep_url": "Karakeep API समापन बिंदु",
|
||||
"form.integration.karakeep_tags": "Karakeep लेबल",
|
||||
"form.integration.linkace_activate": "प्रविष्टियाँ LinkAce में सहेजें",
|
||||
"form.integration.linkace_api_key": "LinkAce API कुंजी",
|
||||
"form.integration.linkace_check_disabled": "लिंक जांच अक्षम करें",
|
||||
"form.integration.linkace_endpoint": "LinkAce API समापन बिंदु",
|
||||
"form.integration.linkace_is_private": "लिंक को निजी चिह्नित करें",
|
||||
"form.integration.linkace_tags": "LinkAce टैग",
|
||||
"form.integration.linkding_activate": "लिंक्डिन में विषयवस्तु सहेजें",
|
||||
"form.integration.linkding_api_key": "लिंकिंग एपीआई कुंजी",
|
||||
"form.integration.linkding_bookmark": "बुकमार्क को अपठित के रूप में चिह्नित करें",
|
||||
"form.integration.linkding_endpoint": "लिंकिंग एपीआई समापन बिंदु",
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkding_tags": "Linkding टैग",
|
||||
"form.integration.linktaco_activate": "LinkTaco में प्रविष्टियाँ सहेजें",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API टोकन",
|
||||
"form.integration.linktaco_api_token_hint": "अपना व्यक्तिगत पहुँच टोकन प्राप्त करें",
|
||||
"form.integration.linktaco_org_slug": "Organization Slug",
|
||||
"form.integration.linktaco_org_slug": "संगठन स्लग",
|
||||
"form.integration.linktaco_tags": "टैग (अधिकतम 10, कॉमा से अलग किए गए)",
|
||||
"form.integration.linktaco_tags_hint": "अधिकतम 10 टैग, कॉमा से अलग किए गए",
|
||||
"form.integration.linktaco_visibility": "दृश्यता",
|
||||
"form.integration.linktaco_visibility_public": "सार्वजनिक",
|
||||
"form.integration.linktaco_visibility_private": "निजी",
|
||||
"form.integration.linktaco_visibility_hint": "निजी दृश्यता के लिए भुगतान LinkTaco खाता आवश्यक है",
|
||||
"form.integration.linkwarden_activate": "Save entries to Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API key",
|
||||
"form.integration.linkwarden_endpoint": "लिंकवर्डन बेस यूआरएलL",
|
||||
"form.integration.linkwarden_activate": "प्रविष्टियाँ Linkwarden में सहेजें",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API कुंजी",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden बेस URL",
|
||||
"form.integration.linkwarden_collection_id": "Linkwarden संग्रह ID",
|
||||
"form.integration.matrix_bot_activate": "नए लेखों को मैट्रिक्स में स्थानांतरित करें",
|
||||
"form.integration.matrix_bot_chat_id": "मैट्रिक्स रूम की आईडी",
|
||||
"form.integration.matrix_bot_password": "मैट्रिक्स उपयोगकर्ता के लिए पासवर्ड",
|
||||
"form.integration.matrix_bot_url": "मैट्रिक्स सर्वर URL",
|
||||
"form.integration.matrix_bot_user": "मैट्रिक्स के लिए उपयोगकर्ता नाम",
|
||||
"form.integration.notion_activate": "Save entries to Notion",
|
||||
"form.integration.notion_page_id": "Notion Page ID",
|
||||
"form.integration.notion_token": "Notion Secret Token",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_topic": "Ntfy topic (default used if not set in feed)",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.notion_activate": "प्रविष्टियाँ Notion में सहेजें",
|
||||
"form.integration.notion_page_id": "Notion पेज ID",
|
||||
"form.integration.notion_token": "Notion गुप्त टोकन",
|
||||
"form.integration.ntfy_activate": "प्रविष्टियाँ ntfy पर भेजें",
|
||||
"form.integration.ntfy_api_token": "Ntfy API टोकन (वैकल्पिक)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy आइकन URL (वैकल्पिक)",
|
||||
"form.integration.ntfy_internal_links": "क्लिक पर आंतरिक लिंक इस्तेमाल करें (वैकल्पिक)",
|
||||
"form.integration.ntfy_password": "Ntfy पासवर्ड (वैकल्पिक)",
|
||||
"form.integration.ntfy_topic": "Ntfy विषय (यदि फ़ीड में नहीं सेट है तो डिफ़ॉल्ट)",
|
||||
"form.integration.ntfy_url": "Ntfy URL (वैकल्पिक, डिफ़ॉल्ट ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Ntfy उपयोगकर्ता नाम (वैकल्पिक)",
|
||||
"form.integration.nunux_keeper_activate": "विषय-वस्तु को ननक्स कीपर में सहेजें",
|
||||
"form.integration.nunux_keeper_api_key": "ननक्स कीपर एपीआई कुंजी",
|
||||
"form.integration.nunux_keeper_endpoint": "ननक्स कीपर एपीआई समापन बिंदु",
|
||||
"form.integration.omnivore_activate": "Save entries to Omnivore",
|
||||
"form.integration.omnivore_api_key": "Omnivore API key",
|
||||
"form.integration.omnivore_url": "Omnivore API Endpoint",
|
||||
"form.integration.omnivore_activate": "प्रविष्टियाँ Omnivore में सहेजें",
|
||||
"form.integration.omnivore_api_key": "Omnivore API कुंजी",
|
||||
"form.integration.omnivore_url": "Omnivore API समापन बिंदु",
|
||||
"form.integration.pinboard_activate": "सहेजें विषयवस्तु प्रति का बोर्ड ",
|
||||
"form.integration.pinboard_bookmark": "बुकमार्क को अपठित के रूप में चिह्नित करें",
|
||||
"form.integration.pinboard_tags": "पिनबोर्ड टैग",
|
||||
"form.integration.pinboard_token": "पिनबोर्ड एपीआई टोकन",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
"form.integration.pushover_token": "Pushover application API token",
|
||||
"form.integration.pushover_user": "Pushover user key",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.pushover_activate": "प्रविष्टियाँ Pushover पर भेजें",
|
||||
"form.integration.pushover_device": "Pushover डिवाइस (वैकल्पिक)",
|
||||
"form.integration.pushover_prefix": "Pushover URL उपसर्ग (वैकल्पिक)",
|
||||
"form.integration.pushover_token": "Pushover ऐप्लिकेशन API टोकन",
|
||||
"form.integration.pushover_user": "Pushover उपयोगकर्ता कुंजी",
|
||||
"form.integration.raindrop_activate": "प्रविष्टियाँ Raindrop में सहेजें",
|
||||
"form.integration.raindrop_collection_id": "संग्रह ID",
|
||||
"form.integration.raindrop_tags": "टैग (कॉमा से अलग)",
|
||||
"form.integration.raindrop_token": "(टेस्ट) टोकन",
|
||||
"form.integration.readeck_activate": "Readeck में विषयवस्तु सहेजें",
|
||||
"form.integration.readeck_api_key": "Readeck एपीआई कुंजी",
|
||||
"form.integration.readeck_endpoint": "Readeck यूआरएल",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_labels": "Readeck लेबल",
|
||||
"form.integration.readeck_only_url": "केवल URL भेजें (पूर्ण सामग्री के बजाय)",
|
||||
"form.integration.readwise_activate": "Save entries to Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shiori_activate": "Save articles to Shiori",
|
||||
"form.integration.shiori_endpoint": "Shiori API Endpoint",
|
||||
"form.integration.shiori_password": "Shiori Password",
|
||||
"form.integration.shiori_username": "Shiori Username",
|
||||
"form.integration.slack_activate": "Push entries to Slack",
|
||||
"form.integration.slack_webhook_link": "Slack Webhook link",
|
||||
"form.integration.readeck_push_activate": "नई प्रविष्टियाँ स्वतः Readeck पर भेजें",
|
||||
"form.integration.readwise_activate": "प्रविष्टियाँ Readwise Reader में सहेजें",
|
||||
"form.integration.readwise_api_key": "Readwise Reader एक्सेस टोकन",
|
||||
"form.integration.readwise_api_key_link": "अपना Readwise एक्सेस टोकन प्राप्त करें",
|
||||
"form.integration.rssbridge_activate": "सदस्यता जोड़ते समय RSS-Bridge जांचें",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge प्रमाणीकरण टोकन",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge सर्वर URL",
|
||||
"form.integration.shaarli_activate": "लेखों को Shaarli में सहेजें",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API रहस्य",
|
||||
"form.integration.shaarli_endpoint": "Shaarli यूआरएल",
|
||||
"form.integration.shiori_activate": "लेखों को Shiori में सहेजें",
|
||||
"form.integration.shiori_endpoint": "Shiori API समापन बिंदु",
|
||||
"form.integration.shiori_password": "Shiori पासवर्ड",
|
||||
"form.integration.shiori_username": "Shiori उपयोगकर्ता नाम",
|
||||
"form.integration.slack_activate": "प्रविष्टियाँ Slack पर भेजें",
|
||||
"form.integration.slack_webhook_link": "Slack वेबहुक लिंक",
|
||||
"form.integration.telegram_bot_activate": "टेलीग्राम चैट के लिए नई विषय-कविता पुश करें",
|
||||
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||
"form.integration.telegram_bot_disable_notification": "Disable notification",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
|
||||
"form.integration.telegram_bot_disable_buttons": "बटन अक्षम करें",
|
||||
"form.integration.telegram_bot_disable_notification": "सूचनाएँ अक्षम करें",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "वेब पृष्ठ पूर्वावलोकन अक्षम करें",
|
||||
"form.integration.telegram_bot_token": "बॉट टोकन",
|
||||
"form.integration.telegram_chat_id": "चैट आईडी",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
"form.integration.telegram_topic_id": "टॉपिक ID",
|
||||
"form.integration.wallabag_activate": "विषय सहेजें वालाबाग में ",
|
||||
"form.integration.wallabag_client_id": "वालाबैग क्लाइंट आईडी",
|
||||
"form.integration.wallabag_client_secret": "वालाबैग क्लाइंट सीक्रेट",
|
||||
@@ -335,16 +337,16 @@
|
||||
"form.integration.wallabag_only_url": "केवल URL भेजें (पूर्ण सामग्री के बजाय)",
|
||||
"form.integration.wallabag_password": "वालाबैग पासवर्ड",
|
||||
"form.integration.wallabag_username": "वालाबैग उपयोगकर्ता नाम",
|
||||
"form.integration.wallabag_tags": "Wallabag Tags",
|
||||
"form.integration.webhook_activate": "Enable Webhooks",
|
||||
"form.integration.webhook_secret": "Webhooks Secret",
|
||||
"form.integration.webhook_url": "Default Webhook URL",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Read articles by opening external links",
|
||||
"form.integration.wallabag_tags": "Wallabag टैग",
|
||||
"form.integration.webhook_activate": "वेबहुक सक्षम करें",
|
||||
"form.integration.webhook_secret": "वेबहुक रहस्य",
|
||||
"form.integration.webhook_url": "डिफ़ॉल्ट वेबहुक URL",
|
||||
"form.prefs.fieldset.application_settings": "एप्लिकेशन सेटिंग्स",
|
||||
"form.prefs.fieldset.authentication_settings": "प्रमाणीकरण सेटिंग्स",
|
||||
"form.prefs.fieldset.global_feed_settings": "वैश्विक फ़ीड सेटिंग्स",
|
||||
"form.prefs.fieldset.reader_settings": "रीडर सेटिंग्स",
|
||||
"form.prefs.help.external_font_hosts": "अनुमति प्राप्त बाहरी फ़ॉन्ट होस्ट की सूची (स्पेस से पृथक). उदाहरण: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "बाहरी लिंक खोलकर लेख पढ़ें",
|
||||
"form.prefs.label.categories_sorting_order": "श्रेणियाँ छँटाई",
|
||||
"form.prefs.label.cjk_reading_speed": "चीनी, कोरियाई और जापानी के लिए पढ़ने की गति (प्रति मिनट वर्ण)",
|
||||
"form.prefs.label.custom_css": "कस्टम सीएसएस",
|
||||
@@ -356,14 +358,14 @@
|
||||
"form.prefs.label.entry_order": "प्रवेश छँटाई कॉलम",
|
||||
"form.prefs.label.entry_sorting": "प्रवेश छँटाई",
|
||||
"form.prefs.label.entry_swipe": "टच स्क्रीन पर एंट्री स्वाइप सक्षम करें",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.label.external_font_hosts": "बाहरी फ़ॉन्ट होस्ट",
|
||||
"form.prefs.label.gesture_nav": "प्रविष्टियों के बीच नेविगेट करने के लिए इशारा",
|
||||
"form.prefs.label.keyboard_shortcuts": "कीबोर्ड शॉर्टकट सक्षम करें",
|
||||
"form.prefs.label.language": "भाषाओं",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "प्रविष्टियों को मैन्युअल रूप से पढ़ा हुआ चिह्नित करें",
|
||||
"form.prefs.label.mark_read_on_media_completion": "केवल तब पढ़ा हुआ चिह्नित करें जब ऑडियो/वीडियो का 90%% चल चुका हो",
|
||||
"form.prefs.label.mark_read_on_view": "देखे जाने पर स्वचालित रूप से प्रविष्टियों को पढ़ने के रूप में चिह्नित करें",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "देखने पर पढ़ा हुआ चिह्नित करें; ऑडियो/वीडियो 90%% पर पढ़ा हुआ करें",
|
||||
"form.prefs.label.media_playback_rate": "ऑडियो/वीडियो की प्लेबैक गति",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "बाहरी लिंक को एक नए टैब में खोलें (लिंक में target=\"_blank\" जोड़ता है)",
|
||||
"form.prefs.label.show_reading_time": "विषय के लिए अनुमानित पढ़ने का समय दिखाएं",
|
||||
@@ -402,7 +404,7 @@
|
||||
"menu.feeds": "फ़ीड",
|
||||
"menu.flush_history": "इतिहास मिटाएँ",
|
||||
"menu.history": "इतिहास",
|
||||
"menu.home_page": "Home page",
|
||||
"menu.home_page": "मुखपृष्ठ",
|
||||
"menu.import": "आयात करे",
|
||||
"menu.integrations": "एकीकरण",
|
||||
"menu.logout": "लॉग आउट",
|
||||
@@ -416,17 +418,17 @@
|
||||
"menu.settings": "समायोजन",
|
||||
"menu.shared_entries": "साझा प्रविष्टियां",
|
||||
"menu.show_all_entries": "सभी प्रविष्टियाँ दिखाए",
|
||||
"menu.show_only_starred_entries": "Show only starred entries",
|
||||
"menu.show_only_starred_entries": "केवल पसंदीदा प्रविष्टियाँ दिखाएं",
|
||||
"menu.show_only_unread_entries": "सभी अपठित प्रविष्टियाँ दिखाए",
|
||||
"menu.starred": "तारांकित",
|
||||
"menu.title": "Menu",
|
||||
"menu.title": "मेनू",
|
||||
"menu.unread": "अपठित",
|
||||
"menu.users": "उपयोगकर्ताओं",
|
||||
"page.about.author": "रचयिता:",
|
||||
"page.about.build_date": "बनाने की तिथि:",
|
||||
"page.about.credits": "आभार सूची",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.db_usage": "डेटाबेस आकार:",
|
||||
"page.about.git_commit": "Git कमिट:",
|
||||
"page.about.global_config_options": "वैश्विक विन्यास विकल्प",
|
||||
"page.about.go_version": "गो संस्करण:",
|
||||
"page.about.license": "अनुज्ञा:",
|
||||
@@ -455,10 +457,10 @@
|
||||
"page.categories.no_feed": "कोई फ़ीड नहीं है।",
|
||||
"page.categories.title": "श्रेणियाँ",
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories"
|
||||
"%d श्रेणी",
|
||||
"%d श्रेणियाँ"
|
||||
],
|
||||
"page.category_label": "Category: %s",
|
||||
"page.category_label": "श्रेणी: %s",
|
||||
"page.edit_category.title": "%s श्रेणी संपाद करे",
|
||||
"page.edit_feed.etag_header": "ईटाग हैडर:",
|
||||
"page.edit_feed.last_check": "अंतिम जांच:",
|
||||
@@ -473,10 +475,10 @@
|
||||
"%d समस्याए"
|
||||
],
|
||||
"page.feeds.last_check": "आखरी जाँच",
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.next_check": "अगली जाँच:",
|
||||
"page.feeds.read_counter": "पड़े हुए विषयवस्तुया",
|
||||
"page.feeds.title": "फ़ीड",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "ऊपर जाएँ",
|
||||
"page.history.title": "इतिहास",
|
||||
"page.import.title": "आयात",
|
||||
"page.integration.bookmarklet": "बुकमार्कलेट",
|
||||
@@ -522,7 +524,7 @@
|
||||
"page.keyboard_shortcuts.subtitle.sections": "अनुभाग नेविगेशन",
|
||||
"page.keyboard_shortcuts.title": "कुंजीपटल अल्प मार्ग",
|
||||
"page.keyboard_shortcuts.toggle_star_status": "बुकमार्क टॉगल करें",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "प्रविष्टि संलग्नक खोलें/बंद करें",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "पढ़ें/अपठित टॉगल करें, अगला फ़ोकस करें",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "पढ़ें/अपठित टॉगल करें, पिछला फ़ोकस करें",
|
||||
"page.login.google_signin": "गूगल के साथ साइन इन करें",
|
||||
@@ -530,7 +532,7 @@
|
||||
"page.login.title": "साइन इन करें",
|
||||
"page.login.webauthn_login": "पासकी से लॉगिन करें",
|
||||
"page.login.webauthn_login.error": "पासकी से लॉगिन करने में असमर्थ",
|
||||
"page.login.webauthn_login.help": "Please enter your username if you're using a security key. This is not required if you are using a Passkey (discoverable credentials).",
|
||||
"page.login.webauthn_login.help": "यदि आप सुरक्षा कुंजी का उपयोग कर रहे हैं तो कृपया अपना उपयोगकर्ता नाम दर्ज करें। पासकी (discoverable credentials) के लिए यह आवश्यक नहीं है।",
|
||||
"page.new_api_key.title": "नई एपीआई कुंजी",
|
||||
"page.new_category.title": "नया श्रेणी",
|
||||
"page.new_user.title": "नया उपभोक्ता",
|
||||
@@ -538,8 +540,8 @@
|
||||
"page.offline.refresh_page": "पृष्ठ को ताज़ा करने का प्रयास करें",
|
||||
"page.offline.title": "ऑफ़लाइन मोड",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries"
|
||||
"%d पढ़ी गई प्रविष्टि",
|
||||
"%d पढ़ी गई प्रविष्टियाँ"
|
||||
],
|
||||
"page.search.title": "खोज का परिणाम",
|
||||
"page.sessions.table.actions": "कार्रवाई",
|
||||
@@ -553,35 +555,35 @@
|
||||
"page.settings.title": "समायोजन",
|
||||
"page.settings.unlink_google_account": "मेरा गूगल खाता हटाय",
|
||||
"page.settings.unlink_oidc_account": "मेरा ओपन-ईद खाता हटाय (%s)",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.added_on": "Added On",
|
||||
"page.settings.webauthn.actions": "कार्रवाई",
|
||||
"page.settings.webauthn.added_on": "जोड़ा गया",
|
||||
"page.settings.webauthn.delete": [
|
||||
"%d पासकुंजी निकालें",
|
||||
"%d पासकी हटाएं"
|
||||
],
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.last_seen_on": "अंतिम उपयोग",
|
||||
"page.settings.webauthn.passkey_name": "पासकी का नाम",
|
||||
"page.settings.webauthn.passkeys": "पासकी",
|
||||
"page.settings.webauthn.register": "रजिस्टर पासकी",
|
||||
"page.settings.webauthn.register.error": "पासकी पंजीकृत करने में असमर्थ",
|
||||
"page.shared_entries.title": "साझा किया हुआ प्रविष्टि",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries"
|
||||
"%d साझा प्रविष्टि",
|
||||
"%d साझा प्रविष्टियाँ"
|
||||
],
|
||||
"page.starred.title": "तारांकित",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries"
|
||||
"%d तारांकित प्रविष्टि",
|
||||
"%d तारांकित प्रविष्टियाँ"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total"
|
||||
"कुल %d प्रविष्टि",
|
||||
"कुल %d प्रविष्टियाँ"
|
||||
],
|
||||
"page.unread.title": "अपठित",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries"
|
||||
"%d अपठित प्रविष्टि",
|
||||
"%d अपठित प्रविष्टियाँ"
|
||||
],
|
||||
"page.users.actions": "कार्रवाई",
|
||||
"page.users.admin.no": "नहीं",
|
||||
@@ -591,7 +593,7 @@
|
||||
"page.users.never_logged": "कभी नहीं",
|
||||
"page.users.title": "उपभोक्ता",
|
||||
"page.users.username": "यूसर्नेम",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"page.webauthn_rename.title": "पासकी का नाम बदलें",
|
||||
"pagination.first": "पहला",
|
||||
"pagination.last": "अंतिम",
|
||||
"pagination.next": "अगला",
|
||||
@@ -629,4 +631,4 @@
|
||||
"time_elapsed.yesterday": "कल",
|
||||
"tooltip.keyboard_shortcuts": "कुंजीपटल शॉर्टकट: %s",
|
||||
"tooltip.logged_user": "%s के रूप में लॉग इन किया"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
"form.feed.label.webhook_url": "Timpa URL Webhook",
|
||||
"form.import.label.file": "Berkas OPML",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.archiveorg_activate": "Push entries to archive.org",
|
||||
"form.integration.archiveorg_activate": "Kirim entri ke archive.org",
|
||||
"form.integration.apprise_activate": "Kirim artikel ke Apprise",
|
||||
"form.integration.apprise_services_url": "Daftar yang dipisahkan koma untuk URL layanan Apprise",
|
||||
"form.integration.apprise_url": "URL API Apprise",
|
||||
@@ -251,9 +251,9 @@
|
||||
"form.integration.linkding_endpoint": "Titik URL API Linkding",
|
||||
"form.integration.linkding_tags": "Tanda Linkding",
|
||||
"form.integration.linktaco_activate": "Simpan entri ke LinkTaco",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token": "Token API LinkTaco",
|
||||
"form.integration.linktaco_api_token_hint": "Dapatkan token akses pribadi Anda di",
|
||||
"form.integration.linktaco_org_slug": "Organization Slug",
|
||||
"form.integration.linktaco_org_slug": "Slug organisasi",
|
||||
"form.integration.linktaco_tags": "Tag (maksimal 10, dipisahkan koma)",
|
||||
"form.integration.linktaco_tags_hint": "Maksimal 10 tag, dipisahkan koma",
|
||||
"form.integration.linktaco_visibility": "Visibilitas",
|
||||
@@ -263,6 +263,7 @@
|
||||
"form.integration.linkwarden_activate": "Simpan artikel ke Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Kunci API Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "URL Dasar Linkwarden",
|
||||
"form.integration.linkwarden_collection_id": "ID koleksi Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Kirim entri baru ke Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID Ruang Matrix",
|
||||
"form.integration.matrix_bot_password": "Kata Sandi Matrix",
|
||||
@@ -303,6 +304,7 @@
|
||||
"form.integration.readeck_endpoint": "Titik URL API Readeck",
|
||||
"form.integration.readeck_labels": "Tagar Readeck",
|
||||
"form.integration.readeck_only_url": "Kirim hanya URL (alih-alih konten penuh)",
|
||||
"form.integration.readeck_push_activate": "Kirim otomatis entri baru ke Readeck",
|
||||
"form.integration.readwise_activate": "Simpan artikel ke Readwise",
|
||||
"form.integration.readwise_api_key": "Token Akses Readwise",
|
||||
"form.integration.readwise_api_key_link": "Dapatkan Token Akses Readwise Anda",
|
||||
@@ -332,7 +334,7 @@
|
||||
"form.integration.wallabag_only_url": "Kirim hanya URL (alih-alih konten penuh)",
|
||||
"form.integration.wallabag_password": "Kata Sandi Wallabag",
|
||||
"form.integration.wallabag_username": "Nama Pengguna Wallabag",
|
||||
"form.integration.wallabag_tags": "Wallabag Tags",
|
||||
"form.integration.wallabag_tags": "Tag Wallabag",
|
||||
"form.integration.webhook_activate": "Aktifkan Webhook",
|
||||
"form.integration.webhook_secret": "Rahasia Webhook",
|
||||
"form.integration.webhook_url": "URL Webhook baku",
|
||||
@@ -370,7 +372,7 @@
|
||||
"form.prefs.select.browser": "Peramban",
|
||||
"form.prefs.select.created_time": "Waktu entri dibuat",
|
||||
"form.prefs.select.fullscreen": "Layar Penuh",
|
||||
"form.prefs.select.minimal_ui": "Minimal",
|
||||
"form.prefs.select.minimal_ui": "Antarmuka minimal",
|
||||
"form.prefs.select.none": "Tidak ada",
|
||||
"form.prefs.select.older_first": "Entri tertua dulu",
|
||||
"form.prefs.select.publish_time": "Waktu entri dipublikasikan",
|
||||
@@ -381,7 +383,7 @@
|
||||
"form.prefs.select.unread_count": "Jumlah yang belum dibaca",
|
||||
"form.submit.loading": "Memuat...",
|
||||
"form.submit.saving": "Menyimpan...",
|
||||
"form.user.label.admin": "Administrator",
|
||||
"form.user.label.admin": "Admin",
|
||||
"form.user.label.confirmation": "Konfirmasi Kata Sandi",
|
||||
"form.user.label.password": "Kata Sandi",
|
||||
"form.user.label.username": "Nama Pengguna",
|
||||
@@ -422,8 +424,8 @@
|
||||
"page.about.author": "Pengembang:",
|
||||
"page.about.build_date": "Tanggal Penyusunan:",
|
||||
"page.about.credits": "Pengembang",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.db_usage": "Ukuran basis data:",
|
||||
"page.about.git_commit": "Komit Git:",
|
||||
"page.about.global_config_options": "Pengaturan Konfigurasi Global",
|
||||
"page.about.go_version": "Versi Go:",
|
||||
"page.about.license": "Lisensi:",
|
||||
@@ -453,7 +455,7 @@
|
||||
"page.categories_count": [
|
||||
"%d kategori"
|
||||
],
|
||||
"page.category_label": "Category: %s",
|
||||
"page.category_label": "Kategori: %s",
|
||||
"page.edit_category.title": "Sunting Kategori: %s",
|
||||
"page.edit_feed.etag_header": "Tajuk ETag:",
|
||||
"page.edit_feed.last_check": "Terakhir diperiksa:",
|
||||
@@ -470,10 +472,10 @@
|
||||
"page.feeds.next_check": "Akan diperiksa kembali:",
|
||||
"page.feeds.read_counter": "Jumlah entri yang telah dibaca",
|
||||
"page.feeds.title": "Umpan",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "Kembali ke atas",
|
||||
"page.history.title": "Riwayat",
|
||||
"page.import.title": "Impor",
|
||||
"page.integration.bookmarklet": "Bookmarklet",
|
||||
"page.integration.bookmarklet": "Penanda (bookmarklet)",
|
||||
"page.integration.bookmarklet.help": "Tautan spesial ini memperbolehkan Anda untuk berlangganan ke situs langsung dengan menggunakan markah di peramban web Anda.",
|
||||
"page.integration.bookmarklet.instructions": "Seret dan tempatkan tautan ini ke markah Anda.",
|
||||
"page.integration.bookmarklet.name": "Tambahkan ke Miniflux",
|
||||
@@ -574,7 +576,7 @@
|
||||
"page.users.actions": "Tindakan",
|
||||
"page.users.admin.no": "Tidak",
|
||||
"page.users.admin.yes": "Ya",
|
||||
"page.users.is_admin": "Administrator",
|
||||
"page.users.is_admin": "Admin",
|
||||
"page.users.last_login": "Terakhir Masuk",
|
||||
"page.users.never_logged": "Tidak Pernah",
|
||||
"page.users.title": "Pengguna",
|
||||
@@ -611,4 +613,4 @@
|
||||
"time_elapsed.yesterday": "kemarin",
|
||||
"tooltip.keyboard_shortcuts": "Pintasan Papan Tik: %s",
|
||||
"tooltip.logged_user": "Masuk sebagai %s"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,14 +109,14 @@
|
||||
"error.http_empty_response": "La risposta HTTP è vuota. Forse questo sito web utilizza un meccanismo di protezione dai bot?",
|
||||
"error.http_empty_response_body": "Il corpo della risposta HTTP è vuoto.",
|
||||
"error.http_forbidden": "L'accesso a questo sito web è vietato. Forse questo sito web ha un meccanismo di protezione dai bot?",
|
||||
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||
"error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).",
|
||||
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_gateway_timeout": "Il sito web non è disponibile a causa di un timeout del gateway. Il problema non è lato Miniflux. Riprova più tardi.",
|
||||
"error.http_internal_server_error": "Il sito web non è disponibile a causa di un errore del server. Il problema non è lato Miniflux. Riprova più tardi.",
|
||||
"error.http_not_authorized": "L'accesso a questo sito web non è autorizzato. Potrebbero essere errati nome utente o password.",
|
||||
"error.http_resource_not_found": "La risorsa richiesta non è stata trovata. Verifica l'URL.",
|
||||
"error.http_response_too_large": "La risposta HTTP è troppo grande. Puoi aumentare il limite di dimensione nelle impostazioni globali (richiede il riavvio del server).",
|
||||
"error.http_service_unavailable": "Il sito web non è disponibile a causa di un errore interno del server. Il problema non è lato Miniflux. Riprova più tardi.",
|
||||
"error.http_too_many_requests": "Miniflux ha generato troppe richieste verso questo sito. Riprova più tardi o modifica la configurazione dell'applicazione.",
|
||||
"error.http_unexpected_status_code": "Il sito web non è disponibile a causa di un codice di stato HTTP inatteso: %d. Il problema non è lato Miniflux. Riprova più tardi.",
|
||||
"error.invalid_categories_sorting_order": "L'ordinamento delle categorie non è valido.",
|
||||
"error.invalid_default_home_page": "Pagina iniziale predefinita non valida!",
|
||||
"error.invalid_display_mode": "Modalità di visualizzazione web app non valida.",
|
||||
@@ -133,22 +133,22 @@
|
||||
"error.network_timeout": "Questo sito web è troppo lento e la richiesta è scaduta: %v",
|
||||
"error.password_min_length": "La password deve contenere almeno 6 caratteri.",
|
||||
"error.proxy_url_not_empty": "L'URL del proxy non può essere vuoto.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_block_rule_fieldname_invalid": "Regola di blocco non valida: la regola #%d non ha un nome di campo valido (opzioni: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Regola di blocco non valida: il pattern della regola #%d non è una regex valida",
|
||||
"error.settings_block_rule_regex_required": "Regola di blocco non valida: il pattern della regola #%d non è stato fornito",
|
||||
"error.settings_block_rule_separator_required": "Regola di blocco non valida: il pattern della regola #%d deve essere separato da '='",
|
||||
"error.settings_invalid_domain_list": "Elenco di domini non valido. Fornisci domini separati da spazi.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Regola di mantenimento non valida: la regola #%d non ha un nome di campo valido (opzioni: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Regola di mantenimento non valida: il pattern della regola #%d non è una regex valida",
|
||||
"error.settings_keep_rule_regex_required": "Regola di mantenimento non valida: il pattern della regola #%d non è stato fornito",
|
||||
"error.settings_keep_rule_separator_required": "Regola di mantenimento non valida: il pattern della regola #%d deve essere separato da '='",
|
||||
"error.settings_mandatory_fields": "Il nome utente, il tema, la lingua ed il fuso orario sono campi obbligatori.",
|
||||
"error.settings_media_playback_rate_range": "La velocità di riproduzione non rientra nell'intervallo",
|
||||
"error.settings_reading_speed_is_positive": "Le velocità di lettura devono essere numeri interi positivi.",
|
||||
"error.site_url_not_empty": "L'URL del sito non può essere vuoto.",
|
||||
"error.subscription_not_found": "Non ho trovato nessun feed.",
|
||||
"error.title_required": "Il titolo è obbligatorio.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "Errore TLS: %q. Puoi disabilitare la verifica TLS nelle impostazioni del feed se preferisci.",
|
||||
"error.unable_to_create_api_key": "Impossibile creare questa chiave API.",
|
||||
"error.unable_to_create_category": "Non sono riuscito ad aggiungere questa categoria.",
|
||||
"error.unable_to_create_user": "Non sono riuscito ad aggiungere questo user.",
|
||||
@@ -164,19 +164,19 @@
|
||||
"form.api_key.label.description": "Etichetta chiave API",
|
||||
"form.category.hide_globally": "Nascondere le voci nella lista globale dei non letti",
|
||||
"form.category.label.title": "Titolo",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.integration": "Third-Party Services",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.general": "Generale",
|
||||
"form.feed.fieldset.integration": "Servizi di terze parti",
|
||||
"form.feed.fieldset.network_settings": "Impostazioni di rete",
|
||||
"form.feed.fieldset.rules": "Regole",
|
||||
"form.feed.label.allow_self_signed_certificates": "Consenti certificati autofirmati o non validi",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.apprise_service_urls": "Elenco di URL di servizi Apprise separati da virgola",
|
||||
"form.feed.label.block_filter_entry_rules": "Regole di Blocco delle Voci",
|
||||
"form.feed.label.blocklist_rules": "Filtri di Blocco Basati su Regex",
|
||||
"form.feed.label.category": "Categoria",
|
||||
"form.feed.label.cookie": "Installare i cookies",
|
||||
"form.feed.label.crawler": "Scarica il contenuto integrale",
|
||||
"form.feed.label.description": "Descrizione",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.disable_http2": "Disabilita HTTP/2 per evitare il fingerprinting",
|
||||
"form.feed.label.disabled": "Non aggiornare questo feed",
|
||||
"form.feed.label.feed_password": "Password del feed",
|
||||
"form.feed.label.feed_url": "URL del feed",
|
||||
@@ -186,43 +186,43 @@
|
||||
"form.feed.label.ignore_http_cache": "Ignora cache HTTP",
|
||||
"form.feed.label.keep_filter_entry_rules": "Regole di Permesso delle Voci",
|
||||
"form.feed.label.keeplist_rules": "Filtri di Mantenimento Basati su Regex",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.no_media_player": "Nessun lettore multimediale (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Invia le voci a ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Priorità predefinita ntfy",
|
||||
"form.feed.label.ntfy_high_priority": "Priorità alta ntfy",
|
||||
"form.feed.label.ntfy_low_priority": "Priorità bassa ntfy",
|
||||
"form.feed.label.ntfy_max_priority": "Priorità massima ntfy",
|
||||
"form.feed.label.ntfy_min_priority": "Priorità minima ntfy",
|
||||
"form.feed.label.ntfy_priority": "Priorità ntfy",
|
||||
"form.feed.label.ntfy_topic": "Topic ntfy (opzionale)",
|
||||
"form.feed.label.proxy_url": "URL del proxy",
|
||||
"form.feed.label.pushover_activate": "Invia le voci a pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Priorità predefinita Pushover",
|
||||
"form.feed.label.pushover_high_priority": "Priorità alta Pushover",
|
||||
"form.feed.label.pushover_low_priority": "Priorità bassa Pushover",
|
||||
"form.feed.label.pushover_max_priority": "Priorità massima Pushover",
|
||||
"form.feed.label.pushover_min_priority": "Priorità minima Pushover",
|
||||
"form.feed.label.pushover_priority": "Priorità del messaggio Pushover",
|
||||
"form.feed.label.rewrite_rules": "Regole di Riscrittura del Contenuto",
|
||||
"form.feed.label.scraper_rules": "Regole di estrazione del contenuto",
|
||||
"form.feed.label.site_url": "URL del sito",
|
||||
"form.feed.label.title": "Titolo",
|
||||
"form.feed.label.urlrewrite_rules": "Regole di riscrittura URL",
|
||||
"form.feed.label.user_agent": "Usa user agent personalizzato",
|
||||
"form.feed.label.webhook_url": "Override webhook url",
|
||||
"form.feed.label.webhook_url": "Sovrascrivi l'URL del webhook",
|
||||
"form.import.label.file": "File OPML",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.archiveorg_activate": "Invia le voci ad archive.org",
|
||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||
"form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.integration.discord_activate": "Push entries to Discord",
|
||||
"form.integration.discord_webhook_link": "Discord Webhook link",
|
||||
"form.integration.apprise_activate": "Invia le voci ad Apprise",
|
||||
"form.integration.apprise_services_url": "Elenco di URL di servizi Apprise separati da virgole",
|
||||
"form.integration.apprise_url": "URL API di Apprise",
|
||||
"form.integration.betula_activate": "Salva le voci in Betula",
|
||||
"form.integration.betula_token": "Token Betula",
|
||||
"form.integration.betula_url": "URL del server Betula",
|
||||
"form.integration.cubox_activate": "Salva le voci in Cubox",
|
||||
"form.integration.cubox_api_link": "Link API di Cubox",
|
||||
"form.integration.discord_activate": "Invia le voci a Discord",
|
||||
"form.integration.discord_webhook_link": "Link webhook di Discord",
|
||||
"form.integration.espial_activate": "Salva gli articoli su Espial",
|
||||
"form.integration.espial_api_key": "API key dell'account Espial",
|
||||
"form.integration.espial_endpoint": "Endpoint dell'API di Espial",
|
||||
@@ -241,47 +241,48 @@
|
||||
"form.integration.karakeep_activate": "Salva gli articoli su Karakeep",
|
||||
"form.integration.karakeep_api_key": "API key dell'account Karakeep",
|
||||
"form.integration.karakeep_url": "Endpoint dell'API di Karakeep",
|
||||
"form.integration.karakeep_tags": "Karakeep Tags",
|
||||
"form.integration.linkace_activate": "Salva gli articoli su LinkAce",
|
||||
"form.integration.linkace_api_key": "API key dell'account LinkAce",
|
||||
"form.integration.linkace_check_disabled": "Disabilita i controlli",
|
||||
"form.integration.linkace_endpoint": "Endpoint dell'API di LinkAce",
|
||||
"form.integration.linkace_is_private": "Rendi i link privati",
|
||||
"form.integration.linkace_tags": "LinkAce Tags",
|
||||
"form.integration.karakeep_tags": "Etichette Karakeep",
|
||||
"form.integration.linkace_activate": "Salva le voci su LinkAce",
|
||||
"form.integration.linkace_api_key": "Chiave API di LinkAce",
|
||||
"form.integration.linkace_check_disabled": "Disabilita il controllo dei link",
|
||||
"form.integration.linkace_endpoint": "Endpoint API di LinkAce",
|
||||
"form.integration.linkace_is_private": "Segna il link come privato",
|
||||
"form.integration.linkace_tags": "Tag di LinkAce",
|
||||
"form.integration.linkding_activate": "Salva gli articoli su Linkding",
|
||||
"form.integration.linkding_api_key": "API key dell'account Linkding",
|
||||
"form.integration.linkding_bookmark": "Segna i preferiti come non letti",
|
||||
"form.integration.linkding_endpoint": "Endpoint dell'API di Linkding",
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkding_tags": "Tag di Linkding",
|
||||
"form.integration.linktaco_activate": "Salva le voci in LinkTaco",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token": "Token API di LinkTaco",
|
||||
"form.integration.linktaco_api_token_hint": "Ottieni il tuo token di accesso personale su",
|
||||
"form.integration.linktaco_org_slug": "Organization Slug",
|
||||
"form.integration.linktaco_org_slug": "Slug dell'organizzazione",
|
||||
"form.integration.linktaco_tags": "Tag (massimo 10, separati da virgola)",
|
||||
"form.integration.linktaco_tags_hint": "Massimo 10 tag, separati da virgola",
|
||||
"form.integration.linktaco_visibility": "Visibilità",
|
||||
"form.integration.linktaco_visibility_public": "Pubblico",
|
||||
"form.integration.linktaco_visibility_private": "Privato",
|
||||
"form.integration.linktaco_visibility_hint": "La visibilità PRIVATA richiede un account LinkTaco a pagamento",
|
||||
"form.integration.linkwarden_activate": "Salva gli articoli su Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "API key dell'account Linkwarden",
|
||||
"form.integration.linkwarden_activate": "Salva le voci su Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Chiave API di Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "URL di base di Linkwarden",
|
||||
"form.integration.linkwarden_collection_id": "ID collezione Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Trasferimento di nuovi articoli a Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID della stanza Matrix",
|
||||
"form.integration.matrix_bot_password": "Password per l'utente Matrix",
|
||||
"form.integration.matrix_bot_url": "URL del server Matrix",
|
||||
"form.integration.matrix_bot_user": "Nome utente per Matrix",
|
||||
"form.integration.notion_activate": "Save entries to Notion",
|
||||
"form.integration.notion_page_id": "Notion Page ID",
|
||||
"form.integration.notion_token": "Notion Secret Token",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_topic": "Ntfy topic (default used if not set in feed)",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.notion_activate": "Salva le voci in Notion",
|
||||
"form.integration.notion_page_id": "ID pagina Notion",
|
||||
"form.integration.notion_token": "Token segreto Notion",
|
||||
"form.integration.ntfy_activate": "Invia le voci a ntfy",
|
||||
"form.integration.ntfy_api_token": "Token API ntfy (opzionale)",
|
||||
"form.integration.ntfy_icon_url": "URL icona ntfy (opzionale)",
|
||||
"form.integration.ntfy_internal_links": "Usa link interni al clic (opzionale)",
|
||||
"form.integration.ntfy_password": "Password ntfy (opzionale)",
|
||||
"form.integration.ntfy_topic": "Topic ntfy (predefinito se non impostato nel feed)",
|
||||
"form.integration.ntfy_url": "URL ntfy (opzionale, predefinito ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Username ntfy (opzionale)",
|
||||
"form.integration.nunux_keeper_activate": "Salva gli articoli su Nunux Keeper",
|
||||
"form.integration.nunux_keeper_api_key": "API key dell'account Nunux Keeper",
|
||||
"form.integration.nunux_keeper_endpoint": "Endpoint dell'API di Nunux Keeper",
|
||||
@@ -292,42 +293,43 @@
|
||||
"form.integration.pinboard_bookmark": "Segna i preferiti come non letti",
|
||||
"form.integration.pinboard_tags": "Tag di Pinboard",
|
||||
"form.integration.pinboard_token": "Token dell'API di Pinboard",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
"form.integration.pushover_token": "Pushover application API token",
|
||||
"form.integration.pushover_user": "Pushover user key",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.pushover_activate": "Invia le voci a Pushover",
|
||||
"form.integration.pushover_device": "Dispositivo Pushover (opzionale)",
|
||||
"form.integration.pushover_prefix": "Prefisso URL Pushover (opzionale)",
|
||||
"form.integration.pushover_token": "Token API dell'app Pushover",
|
||||
"form.integration.pushover_user": "Chiave utente Pushover",
|
||||
"form.integration.raindrop_activate": "Salva le voci su Raindrop",
|
||||
"form.integration.raindrop_collection_id": "ID collezione",
|
||||
"form.integration.raindrop_tags": "Tag (separati da virgola)",
|
||||
"form.integration.raindrop_token": "(Test) token",
|
||||
"form.integration.readeck_activate": "Salva gli articoli su Readeck",
|
||||
"form.integration.readeck_api_key": "API key dell'account Readeck",
|
||||
"form.integration.readeck_endpoint": "Endpoint dell'API di Readeck",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_labels": "Etichette Readeck",
|
||||
"form.integration.readeck_only_url": "Invia solo URL (invece del contenuto completo)",
|
||||
"form.integration.readwise_activate": "Save entries to Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.readeck_push_activate": "Invia automaticamente le nuove voci a Readeck",
|
||||
"form.integration.readwise_activate": "Salva le voci in Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Token di accesso a Readwise Reader",
|
||||
"form.integration.readwise_api_key_link": "Ottieni il tuo token di accesso Readwise",
|
||||
"form.integration.rssbridge_activate": "Controlla RSS-Bridge quando aggiungi sottoscrizioni",
|
||||
"form.integration.rssbridge_token": "Token di autenticazione RSS-Bridge",
|
||||
"form.integration.rssbridge_url": "URL del server RSS-Bridge",
|
||||
"form.integration.shaarli_activate": "Salva gli articoli su Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Segreto API di Shaarli",
|
||||
"form.integration.shaarli_endpoint": "URL di Shaarli",
|
||||
"form.integration.shiori_activate": "Salva gli articoli su Shiori",
|
||||
"form.integration.shiori_endpoint": "Endpoint dell'API di Shiori",
|
||||
"form.integration.shiori_password": "Password dell'account Shiori",
|
||||
"form.integration.shiori_username": "Nome utente dell'account Shiori",
|
||||
"form.integration.slack_activate": "Push entries to Slack",
|
||||
"form.integration.slack_webhook_link": "Slack Webhook link",
|
||||
"form.integration.slack_activate": "Invia le voci a Slack",
|
||||
"form.integration.slack_webhook_link": "Link webhook di Slack",
|
||||
"form.integration.telegram_bot_activate": "Invia nuovi articoli alla chat di Telegram",
|
||||
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||
"form.integration.telegram_bot_disable_notification": "Disable notification",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
|
||||
"form.integration.telegram_bot_disable_buttons": "Disabilita i pulsanti",
|
||||
"form.integration.telegram_bot_disable_notification": "Disabilita le notifiche",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Disabilita l'anteprima delle pagine",
|
||||
"form.integration.telegram_bot_token": "Token bot",
|
||||
"form.integration.telegram_chat_id": "ID chat",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
"form.integration.telegram_topic_id": "ID argomento",
|
||||
"form.integration.wallabag_activate": "Salva gli articoli su Wallabag",
|
||||
"form.integration.wallabag_client_id": "Client ID dell'account Wallabag",
|
||||
"form.integration.wallabag_client_secret": "Client secret dell'account Wallabag",
|
||||
@@ -335,16 +337,16 @@
|
||||
"form.integration.wallabag_only_url": "Invia solo URL (invece del contenuto completo)",
|
||||
"form.integration.wallabag_password": "Password dell'account Wallabag",
|
||||
"form.integration.wallabag_username": "Nome utente dell'account Wallabag",
|
||||
"form.integration.wallabag_tags": "Wallabag Tags",
|
||||
"form.integration.webhook_activate": "Enable Webhooks",
|
||||
"form.integration.webhook_secret": "Webhooks Secret",
|
||||
"form.integration.webhook_url": "Default Webhook URL",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Read articles by opening external links",
|
||||
"form.integration.wallabag_tags": "Tag di Wallabag",
|
||||
"form.integration.webhook_activate": "Abilita i webhook",
|
||||
"form.integration.webhook_secret": "Segreto dei webhook",
|
||||
"form.integration.webhook_url": "URL webhook predefinito",
|
||||
"form.prefs.fieldset.application_settings": "Impostazioni applicazione",
|
||||
"form.prefs.fieldset.authentication_settings": "Impostazioni di autenticazione",
|
||||
"form.prefs.fieldset.global_feed_settings": "Impostazioni globali dei feed",
|
||||
"form.prefs.fieldset.reader_settings": "Impostazioni del lettore",
|
||||
"form.prefs.help.external_font_hosts": "Elenco, separato da spazi, degli host di font esterni consentiti. Ad esempio: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Leggi gli articoli aprendo i link esterni",
|
||||
"form.prefs.label.categories_sorting_order": "Ordinamento delle categorie",
|
||||
"form.prefs.label.cjk_reading_speed": "Velocità di lettura per cinese, coreano e giapponese (caratteri al minuto)",
|
||||
"form.prefs.label.custom_css": "CSS personalizzati",
|
||||
@@ -356,14 +358,14 @@
|
||||
"form.prefs.label.entry_order": "Colonna di ordinamento delle voci",
|
||||
"form.prefs.label.entry_sorting": "Ordinamento articoli",
|
||||
"form.prefs.label.entry_swipe": "Abilita lo scorrimento della voce sui touch screen",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.label.external_font_hosts": "Host di font esterni",
|
||||
"form.prefs.label.gesture_nav": "Gesto per navigare tra le voci",
|
||||
"form.prefs.label.keyboard_shortcuts": "Abilita le scorciatoie da tastiera",
|
||||
"form.prefs.label.language": "Lingua",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Contrassegna manualmente le voci come lette",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Segna come letto solo quando audio/video raggiunge il 90%%",
|
||||
"form.prefs.label.mark_read_on_view": "Contrassegna automaticamente le voci come lette quando visualizzate",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Segna le voci lette alla visualizzazione; per audio/video al 90%%",
|
||||
"form.prefs.label.media_playback_rate": "Velocità di riproduzione dell'audio/video",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Apri i link esterni in una nuova scheda (aggiunge target=\"_blank\" ai link)",
|
||||
"form.prefs.label.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli",
|
||||
@@ -386,7 +388,7 @@
|
||||
"form.submit.saving": "Salvataggio in corso...",
|
||||
"form.user.label.admin": "Amministratore",
|
||||
"form.user.label.confirmation": "Conferma password",
|
||||
"form.user.label.password": "Password",
|
||||
"form.user.label.password": "Parola d'accesso",
|
||||
"form.user.label.username": "Nome utente",
|
||||
"menu.about": "Informazioni",
|
||||
"menu.add_feed": "Aggiungi feed",
|
||||
@@ -402,7 +404,7 @@
|
||||
"menu.feeds": "Feed",
|
||||
"menu.flush_history": "Svuota la cronologia",
|
||||
"menu.history": "Cronologia",
|
||||
"menu.home_page": "Home page",
|
||||
"menu.home_page": "Pagina iniziale",
|
||||
"menu.import": "Importa",
|
||||
"menu.integrations": "Integrazioni",
|
||||
"menu.logout": "Esci",
|
||||
@@ -419,14 +421,14 @@
|
||||
"menu.show_only_starred_entries": "Mostra solo voci preferiti",
|
||||
"menu.show_only_unread_entries": "Mostra solo voci non lette",
|
||||
"menu.starred": "Preferiti",
|
||||
"menu.title": "Menu",
|
||||
"menu.title": "Menù",
|
||||
"menu.unread": "Da leggere",
|
||||
"menu.users": "Utenti",
|
||||
"page.about.author": "Autore:",
|
||||
"page.about.build_date": "Data della build:",
|
||||
"page.about.credits": "Crediti",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.db_usage": "Dimensione del database:",
|
||||
"page.about.git_commit": "Commit Git:",
|
||||
"page.about.global_config_options": "Opzioni di configurazione globali",
|
||||
"page.about.go_version": "Go versione:",
|
||||
"page.about.license": "Licenza:",
|
||||
@@ -455,10 +457,10 @@
|
||||
"page.categories.no_feed": "Nessun feed.",
|
||||
"page.categories.title": "Categorie",
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories"
|
||||
"%d categoria",
|
||||
"%d categorie"
|
||||
],
|
||||
"page.category_label": "Category: %s",
|
||||
"page.category_label": "Categoria: %s",
|
||||
"page.edit_category.title": "Modifica categoria: %s",
|
||||
"page.edit_feed.etag_header": "Header ETag:",
|
||||
"page.edit_feed.last_check": "Ultimo controllo:",
|
||||
@@ -473,10 +475,10 @@
|
||||
"%d errori"
|
||||
],
|
||||
"page.feeds.last_check": "Ultimo controllo:",
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.next_check": "Prossimo controllo:",
|
||||
"page.feeds.read_counter": "Numero di voci lette",
|
||||
"page.feeds.title": "Feed",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "Torna su",
|
||||
"page.history.title": "Cronologia",
|
||||
"page.import.title": "Importa",
|
||||
"page.integration.bookmarklet": "Segnalibro",
|
||||
@@ -485,7 +487,7 @@
|
||||
"page.integration.bookmarklet.name": "Aggiungi a Miniflux",
|
||||
"page.integration.miniflux_api": "API di Miniflux",
|
||||
"page.integration.miniflux_api_endpoint": "Endpoint dell'API di Miniflux",
|
||||
"page.integration.miniflux_api_password": "Password",
|
||||
"page.integration.miniflux_api_password": "Password dell'API",
|
||||
"page.integration.miniflux_api_password_value": "La password del tuo account",
|
||||
"page.integration.miniflux_api_username": "Nome utente",
|
||||
"page.integrations.title": "Integrazioni",
|
||||
@@ -522,7 +524,7 @@
|
||||
"page.keyboard_shortcuts.subtitle.sections": "Navigazione sezioni",
|
||||
"page.keyboard_shortcuts.title": "Scorciatoie da tastiera",
|
||||
"page.keyboard_shortcuts.toggle_star_status": "Aggiungi/rimuovi dai preferiti",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Apri/chiudi gli allegati dell'articolo",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "Cambia lo stato di lettura (letto/da leggere), concentrati dopo",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Cambia lo stato di lettura (letto/da leggere), focus precedente",
|
||||
"page.login.google_signin": "Accedi tramite Google",
|
||||
@@ -530,7 +532,7 @@
|
||||
"page.login.title": "Accedi",
|
||||
"page.login.webauthn_login": "Accedi con passkey",
|
||||
"page.login.webauthn_login.error": "Impossibile accedere con passkey",
|
||||
"page.login.webauthn_login.help": "Please enter your username if you're using a security key. This is not required if you are using a Passkey (discoverable credentials).",
|
||||
"page.login.webauthn_login.help": "Inserisci il tuo nome utente se stai usando una chiave di sicurezza. Non è necessario con una Passkey (credenziali rilevabili).",
|
||||
"page.new_api_key.title": "Nuova chiave API",
|
||||
"page.new_category.title": "Nuova categoria",
|
||||
"page.new_user.title": "Nuovo utente",
|
||||
@@ -538,60 +540,60 @@
|
||||
"page.offline.refresh_page": "Prova ad aggiornare la pagina",
|
||||
"page.offline.title": "Modalità offline",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries"
|
||||
"%d voce letta",
|
||||
"%d voci lette"
|
||||
],
|
||||
"page.search.title": "Risultati della ricerca",
|
||||
"page.sessions.table.actions": "Azioni",
|
||||
"page.sessions.table.current_session": "Sessione corrente",
|
||||
"page.sessions.table.date": "Data",
|
||||
"page.sessions.table.ip": "Indirizzo IP",
|
||||
"page.sessions.table.user_agent": "User Agent",
|
||||
"page.sessions.table.user_agent": "User agent",
|
||||
"page.sessions.title": "Sessioni",
|
||||
"page.settings.link_google_account": "Collega il mio account Google",
|
||||
"page.settings.link_oidc_account": "Collega il mio account %s",
|
||||
"page.settings.title": "Impostazioni",
|
||||
"page.settings.unlink_google_account": "Scollega il mio account Google",
|
||||
"page.settings.unlink_oidc_account": "Scollega il mio account %s",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.added_on": "Added On",
|
||||
"page.settings.webauthn.actions": "Azioni",
|
||||
"page.settings.webauthn.added_on": "Aggiunta il",
|
||||
"page.settings.webauthn.delete": [
|
||||
"Rimuovi %d passkey",
|
||||
"Rimuovi %d passkey"
|
||||
],
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.last_seen_on": "Ultimo uso",
|
||||
"page.settings.webauthn.passkey_name": "Nome passkey",
|
||||
"page.settings.webauthn.passkeys": "Passkey",
|
||||
"page.settings.webauthn.register": "Registra la chiave di accesso",
|
||||
"page.settings.webauthn.register.error": "Impossibile registrare la passkey",
|
||||
"page.shared_entries.title": "Voci condivise",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries"
|
||||
"%d voce condivisa",
|
||||
"%d voci condivise"
|
||||
],
|
||||
"page.starred.title": "Preferiti",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries"
|
||||
"%d voce preferita",
|
||||
"%d voci preferite"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total"
|
||||
"%d voce in totale",
|
||||
"%d voci in totale"
|
||||
],
|
||||
"page.unread.title": "Da leggere",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries"
|
||||
"%d voce non letta",
|
||||
"%d voci non lette"
|
||||
],
|
||||
"page.users.actions": "Azioni",
|
||||
"page.users.admin.no": "No",
|
||||
"page.users.admin.no": "Niente",
|
||||
"page.users.admin.yes": "Sì",
|
||||
"page.users.is_admin": "Amministratore",
|
||||
"page.users.last_login": "Ultimo accesso",
|
||||
"page.users.never_logged": "Mai",
|
||||
"page.users.title": "Utenti",
|
||||
"page.users.username": "Nome utente",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"page.webauthn_rename.title": "Rinomina passkey",
|
||||
"pagination.first": "Primo",
|
||||
"pagination.last": "Ultimo",
|
||||
"pagination.next": "Successivo",
|
||||
@@ -629,4 +631,4 @@
|
||||
"time_elapsed.yesterday": "ieri",
|
||||
"tooltip.keyboard_shortcuts": "Scorciatoia da tastiera: %s",
|
||||
"tooltip.logged_user": "Autenticato come %s"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,14 +106,14 @@
|
||||
"error.http_empty_response": "HTTP応答が空です。おそらく、このウェブサイトはボット保護メカニズムを使用していますか?",
|
||||
"error.http_empty_response_body": "HTTP応答本文が空です。",
|
||||
"error.http_forbidden": "このウェブサイトへのアクセスは禁止されています。おそらく、このウェブサイトはボット保護メカニズムを持っていますか?",
|
||||
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||
"error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).",
|
||||
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_gateway_timeout": "ゲートウェイタイムアウトのため現在このウェブサイトは利用できません。問題は Miniflux 側にはありません。しばらくしてから再度お試しください。",
|
||||
"error.http_internal_server_error": "サーバーエラーのため現在このウェブサイトは利用できません。問題は Miniflux 側にはありません。しばらくしてから再度お試しください。",
|
||||
"error.http_not_authorized": "このウェブサイトへのアクセスが許可されていません。ユーザー名またはパスワードが正しくない可能性があります。",
|
||||
"error.http_resource_not_found": "要求されたリソースが見つかりません。URL を確認してください。",
|
||||
"error.http_response_too_large": "HTTP 応答が大きすぎます。グローバル設定で HTTP 応答サイズの上限を引き上げることができます(サーバー再起動が必要)。",
|
||||
"error.http_service_unavailable": "内部サーバーエラーのため現在このウェブサイトは利用できません。問題は Miniflux 側にはありません。しばらくしてから再度お試しください。",
|
||||
"error.http_too_many_requests": "Miniflux がこのウェブサイトに対してリクエストを送りすぎました。しばらく待つか、アプリケーション設定を変更してください。",
|
||||
"error.http_unexpected_status_code": "予期しない HTTP ステータスコード (%d) により現在このウェブサイトは利用できません。問題は Miniflux 側にはありません。しばらくしてから再度お試しください。",
|
||||
"error.invalid_categories_sorting_order": "カテゴリの表示順が無効です。",
|
||||
"error.invalid_default_home_page": "デフォルトのトップページが無効です",
|
||||
"error.invalid_display_mode": "Web アプリの表示モードが無効です。",
|
||||
@@ -130,22 +130,22 @@
|
||||
"error.network_timeout": "このウェブサイトは応答が遅すぎるためタイムアウトしました: %v",
|
||||
"error.password_min_length": "パスワードは6文字以上である必要があります。",
|
||||
"error.proxy_url_not_empty": "プロキシURLを空にすることはできません。",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_block_rule_fieldname_invalid": "ブロックルールが無効です: ルール #%d に有効なフィールド名がありません (オプション: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "ブロックルールが無効です: ルール #%d のパターンが正規表現として無効です",
|
||||
"error.settings_block_rule_regex_required": "ブロックルールが無効です: ルール #%d にパターンが指定されていません",
|
||||
"error.settings_block_rule_separator_required": "ブロックルールが無効です: ルール #%d のパターンは '=' で区切る必要があります",
|
||||
"error.settings_invalid_domain_list": "ドメインリストが無効です。ドメインをスペース区切りで指定してください。",
|
||||
"error.settings_keep_rule_fieldname_invalid": "キープルールが無効です: ルール #%d に有効なフィールド名がありません (オプション: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "キープルールが無効です: ルール #%d のパターンが正規表現として無効です",
|
||||
"error.settings_keep_rule_regex_required": "キープルールが無効です: ルール #%d にパターンが指定されていません",
|
||||
"error.settings_keep_rule_separator_required": "キープルールが無効です: ルール #%d のパターンは '=' で区切る必要があります",
|
||||
"error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンのすべてが必要です。",
|
||||
"error.settings_media_playback_rate_range": "再生速度が範囲外",
|
||||
"error.settings_reading_speed_is_positive": "読書速度は正の整数である必要があります。",
|
||||
"error.site_url_not_empty": "サイトの URL を空にすることはできません。",
|
||||
"error.subscription_not_found": "フィードが見つかりません。",
|
||||
"error.title_required": "タイトルが必要です。",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "TLS エラー: %q。必要であればフィード設定で TLS 検証を無効にできます。",
|
||||
"error.unable_to_create_api_key": "この API キーを作成できません。",
|
||||
"error.unable_to_create_category": "このカテゴリは作成できません。",
|
||||
"error.unable_to_create_user": "このユーザーは作成できません。",
|
||||
@@ -161,19 +161,19 @@
|
||||
"form.api_key.label.description": "API キーラベル",
|
||||
"form.category.hide_globally": "未読一覧に記事を表示しない",
|
||||
"form.category.label.title": "タイトル",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.integration": "Third-Party Services",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.general": "一般",
|
||||
"form.feed.fieldset.integration": "サードパーティサービス",
|
||||
"form.feed.fieldset.network_settings": "ネットワーク設定",
|
||||
"form.feed.fieldset.rules": "ルール",
|
||||
"form.feed.label.allow_self_signed_certificates": "自己署名証明書または無効な証明書を許可する",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.apprise_service_urls": "Apprise サービス URL のカンマ区切りリスト",
|
||||
"form.feed.label.block_filter_entry_rules": "エントリブロッキングルール",
|
||||
"form.feed.label.blocklist_rules": "正規表現ベースのブロッキングフィルター",
|
||||
"form.feed.label.category": "カテゴリ",
|
||||
"form.feed.label.cookie": "Cookie の設定",
|
||||
"form.feed.label.crawler": "オリジナルの内容を取得",
|
||||
"form.feed.label.description": "説明",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.disable_http2": "フィンガープリンティング回避のため HTTP/2 を無効化",
|
||||
"form.feed.label.disabled": "このフィードを更新しない",
|
||||
"form.feed.label.feed_password": "フィードのパスワード",
|
||||
"form.feed.label.feed_url": "フィード URL",
|
||||
@@ -183,53 +183,53 @@
|
||||
"form.feed.label.ignore_http_cache": "HTTPキャッシュを無視",
|
||||
"form.feed.label.keep_filter_entry_rules": "エントリ許可ルール",
|
||||
"form.feed.label.keeplist_rules": "正規表現ベースのキープフィルター",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.no_media_player": "メディアプレーヤーなし(音声/動画)",
|
||||
"form.feed.label.ntfy_activate": "エントリを ntfy に送信",
|
||||
"form.feed.label.ntfy_default_priority": "ntfy デフォルト優先度",
|
||||
"form.feed.label.ntfy_high_priority": "ntfy 高優先度",
|
||||
"form.feed.label.ntfy_low_priority": "ntfy 低優先度",
|
||||
"form.feed.label.ntfy_max_priority": "ntfy 最大優先度",
|
||||
"form.feed.label.ntfy_min_priority": "ntfy 最小優先度",
|
||||
"form.feed.label.ntfy_priority": "ntfy 優先度",
|
||||
"form.feed.label.ntfy_topic": "ntfy トピック(任意)",
|
||||
"form.feed.label.proxy_url": "プロキシ URL",
|
||||
"form.feed.label.pushover_activate": "エントリを pushover.net に送信",
|
||||
"form.feed.label.pushover_default_priority": "Pushover 既定の優先度",
|
||||
"form.feed.label.pushover_high_priority": "Pushover 高優先度",
|
||||
"form.feed.label.pushover_low_priority": "Pushover 低優先度",
|
||||
"form.feed.label.pushover_max_priority": "Pushover 最大優先度",
|
||||
"form.feed.label.pushover_min_priority": "Pushover 最小優先度",
|
||||
"form.feed.label.pushover_priority": "Pushover メッセージ優先度",
|
||||
"form.feed.label.rewrite_rules": "コンテンツ書き換えルール",
|
||||
"form.feed.label.scraper_rules": "Scraper ルール",
|
||||
"form.feed.label.site_url": "サイト URL",
|
||||
"form.feed.label.title": "タイトル",
|
||||
"form.feed.label.urlrewrite_rules": "Rewrite URL ルール",
|
||||
"form.feed.label.user_agent": "デフォルトの User Agent を上書きする",
|
||||
"form.feed.label.webhook_url": "Override webhook url",
|
||||
"form.feed.label.webhook_url": "Webhook の URL を上書き",
|
||||
"form.import.label.file": "OPML ファイル",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.archiveorg_activate": "エントリーをarchive.orgにプッシュする",
|
||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||
"form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.integration.discord_activate": "Push entries to Discord",
|
||||
"form.integration.discord_webhook_link": "Discord Webhook link",
|
||||
"form.integration.apprise_activate": "エントリを Apprise に送信",
|
||||
"form.integration.apprise_services_url": "Apprise サービス URL のカンマ区切りリスト",
|
||||
"form.integration.apprise_url": "Apprise APIのURL",
|
||||
"form.integration.betula_activate": "エントリを Betula に保存",
|
||||
"form.integration.betula_token": "Betula トークン",
|
||||
"form.integration.betula_url": "Betula サーバー URL",
|
||||
"form.integration.cubox_activate": "エントリを Cubox に保存",
|
||||
"form.integration.cubox_api_link": "Cubox API リンク",
|
||||
"form.integration.discord_activate": "エントリを Discord に送信",
|
||||
"form.integration.discord_webhook_link": "Discord Webhook リンク",
|
||||
"form.integration.espial_activate": "Espial に記事を保存する",
|
||||
"form.integration.espial_api_key": "Espial の API key",
|
||||
"form.integration.espial_endpoint": "Espial の API Endpoint",
|
||||
"form.integration.espial_tags": "Espial の Tag",
|
||||
"form.integration.fever_activate": "Fever API を有効にする",
|
||||
"form.integration.fever_endpoint": "Fever API endpoint:",
|
||||
"form.integration.fever_endpoint": "Fever APIエンドポイント:",
|
||||
"form.integration.fever_password": "Fever のパスワード",
|
||||
"form.integration.fever_username": "Fever のユーザー名",
|
||||
"form.integration.googlereader_activate": "Google Reader API を有効にする",
|
||||
"form.integration.googlereader_endpoint": "Google Reader API endpoint:",
|
||||
"form.integration.googlereader_endpoint": "Google Reader APIエンドポイント:",
|
||||
"form.integration.googlereader_password": "Google Reader のパスワード",
|
||||
"form.integration.googlereader_username": "Google Reader のユーザー名",
|
||||
"form.integration.instapaper_activate": "Instapaper に記事を保存する",
|
||||
@@ -239,46 +239,47 @@
|
||||
"form.integration.karakeep_api_key": "Karakeep の API key",
|
||||
"form.integration.karakeep_url": "Karakeep の API Endpoint",
|
||||
"form.integration.karakeep_tags": "Karakeep の Tags",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_is_private": "Mark link as private",
|
||||
"form.integration.linkace_tags": "LinkAce Tags",
|
||||
"form.integration.linkace_activate": "エントリを LinkAce に保存",
|
||||
"form.integration.linkace_api_key": "LinkAce API キー",
|
||||
"form.integration.linkace_check_disabled": "リンクチェックを無効化",
|
||||
"form.integration.linkace_endpoint": "LinkAce API エンドポイント",
|
||||
"form.integration.linkace_is_private": "リンクを非公開にする",
|
||||
"form.integration.linkace_tags": "LinkAce タグ",
|
||||
"form.integration.linkding_activate": "Linkding に記事を保存する",
|
||||
"form.integration.linkding_api_key": "Linkding の API key",
|
||||
"form.integration.linkding_bookmark": "ブックマークを未読にする",
|
||||
"form.integration.linkding_endpoint": "Linkding の API Endpoint",
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkding_tags": "Linkding タグ",
|
||||
"form.integration.linktaco_activate": "LinkTacoでエントリを保存する",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token_hint": "パーソナルアクセストークンを取得する",
|
||||
"form.integration.linktaco_org_slug": "Organization Slug",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API トークン",
|
||||
"form.integration.linktaco_api_token_hint": "個人用アクセス トークンを取得",
|
||||
"form.integration.linktaco_org_slug": "組織スラッグ",
|
||||
"form.integration.linktaco_tags": "タグ (最大10件、カンマ区切り)",
|
||||
"form.integration.linktaco_tags_hint": "最大10件のタグ、カンマ区切り",
|
||||
"form.integration.linktaco_visibility": "公開設定",
|
||||
"form.integration.linktaco_visibility_public": "公開",
|
||||
"form.integration.linktaco_visibility_private": "非公開",
|
||||
"form.integration.linktaco_visibility_hint": "非公開設定には有料のLinkTacoアカウントが必要です",
|
||||
"form.integration.linkwarden_activate": "Linkwarden に記事を保存する",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden の API key",
|
||||
"form.integration.linkwarden_endpoint": "リンクワーデン ベース URL",
|
||||
"form.integration.linkwarden_activate": "Linkwarden に記事を保存",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden の API キー",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden ベース URL",
|
||||
"form.integration.linkwarden_collection_id": "Linkwarden コレクション ID",
|
||||
"form.integration.matrix_bot_activate": "新しい記事をMatrixに転送する",
|
||||
"form.integration.matrix_bot_chat_id": "MatrixルームのID",
|
||||
"form.integration.matrix_bot_password": "Matrixユーザ用パスワード",
|
||||
"form.integration.matrix_bot_url": "MatrixサーバーのURL",
|
||||
"form.integration.matrix_bot_user": "Matrixのユーザー名",
|
||||
"form.integration.notion_activate": "Save entries to Notion",
|
||||
"form.integration.notion_page_id": "Notion Page ID",
|
||||
"form.integration.notion_token": "Notion Secret Token",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_topic": "Ntfy topic (default used if not set in feed)",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.notion_activate": "エントリを Notion に保存",
|
||||
"form.integration.notion_page_id": "Notion ページ ID",
|
||||
"form.integration.notion_token": "Notion シークレット トークン",
|
||||
"form.integration.ntfy_activate": "エントリを ntfy に送信",
|
||||
"form.integration.ntfy_api_token": "ntfy API トークン(任意)",
|
||||
"form.integration.ntfy_icon_url": "ntfy アイコン URL(任意)",
|
||||
"form.integration.ntfy_internal_links": "クリック時に内部リンクを使用(任意)",
|
||||
"form.integration.ntfy_password": "ntfy パスワード(任意)",
|
||||
"form.integration.ntfy_topic": "ntfy トピック(フィードで未設定なら既定値)",
|
||||
"form.integration.ntfy_url": "ntfy URL(任意、既定 ntfy.sh)",
|
||||
"form.integration.ntfy_username": "ntfy ユーザー名(任意)",
|
||||
"form.integration.nunux_keeper_activate": "Nunux Keeper に記事を保存する",
|
||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper の API key",
|
||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper の API Endpoint",
|
||||
@@ -289,42 +290,43 @@
|
||||
"form.integration.pinboard_bookmark": "ブックマークを未読にする",
|
||||
"form.integration.pinboard_tags": "Pinboard の Tag",
|
||||
"form.integration.pinboard_token": "Pinboard の API Token",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
"form.integration.pushover_token": "Pushover application API token",
|
||||
"form.integration.pushover_user": "Pushover user key",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.pushover_activate": "エントリを Pushover に送信",
|
||||
"form.integration.pushover_device": "Pushover デバイス(任意)",
|
||||
"form.integration.pushover_prefix": "Pushover URL プレフィックス(任意)",
|
||||
"form.integration.pushover_token": "Pushover アプリ API トークン",
|
||||
"form.integration.pushover_user": "Pushover ユーザーキー",
|
||||
"form.integration.raindrop_activate": "エントリを Raindrop に保存",
|
||||
"form.integration.raindrop_collection_id": "コレクション ID",
|
||||
"form.integration.raindrop_tags": "タグ(カンマ区切り)",
|
||||
"form.integration.raindrop_token": "(テスト) トークン",
|
||||
"form.integration.readeck_activate": "Readeck に記事を保存する",
|
||||
"form.integration.readeck_api_key": "Readeck の API key",
|
||||
"form.integration.readeck_endpoint": "Readeck の API Endpoint",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_labels": "Readeck ラベル",
|
||||
"form.integration.readeck_only_url": "URL のみを送信 (完全なコンテンツではなく)",
|
||||
"form.integration.readwise_activate": "Save entries to Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shiori_activate": "Shiori に記事を保存する",
|
||||
"form.integration.shiori_endpoint": "Shiori の API Endpoint",
|
||||
"form.integration.shiori_password": "Shiori の パスワード",
|
||||
"form.integration.shiori_username": "Shiori の ユーザー名",
|
||||
"form.integration.slack_activate": "Slack entries to Discord",
|
||||
"form.integration.slack_webhook_link": "Slack Webhook link",
|
||||
"form.integration.readeck_push_activate": "新しいエントリを自動的に Readeck へ送信",
|
||||
"form.integration.readwise_activate": "エントリを Readwise Reader に保存",
|
||||
"form.integration.readwise_api_key": "Readwise Reader アクセストークン",
|
||||
"form.integration.readwise_api_key_link": "Readwise アクセストークンを取得",
|
||||
"form.integration.rssbridge_activate": "購読を追加する際に RSS-Bridge を確認",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge 認証トークン",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge サーバー URL",
|
||||
"form.integration.shaarli_activate": "記事を Shaarli に保存",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API シークレット",
|
||||
"form.integration.shaarli_endpoint": "ShaarliのURL",
|
||||
"form.integration.shiori_activate": "記事を Shiori に保存",
|
||||
"form.integration.shiori_endpoint": "Shiori API エンドポイント",
|
||||
"form.integration.shiori_password": "Shiori パスワード",
|
||||
"form.integration.shiori_username": "Shiori ユーザー名",
|
||||
"form.integration.slack_activate": "エントリを Slack に送信",
|
||||
"form.integration.slack_webhook_link": "Slack Webhook リンク",
|
||||
"form.integration.telegram_bot_activate": "新しい記事を Telegram チャットにプッシュする",
|
||||
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||
"form.integration.telegram_bot_disable_notification": "Disable notification",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
|
||||
"form.integration.telegram_bot_disable_buttons": "ボタンを無効化",
|
||||
"form.integration.telegram_bot_disable_notification": "通知を無効化",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Web ページのプレビューを無効化",
|
||||
"form.integration.telegram_bot_token": "ボットトークン",
|
||||
"form.integration.telegram_chat_id": "チャット ID",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
"form.integration.telegram_topic_id": "トピック ID",
|
||||
"form.integration.wallabag_activate": "Wallabag に記事を保存する",
|
||||
"form.integration.wallabag_client_id": "Wallabag の Client ID",
|
||||
"form.integration.wallabag_client_secret": "Wallabag の Client Secret",
|
||||
@@ -332,16 +334,16 @@
|
||||
"form.integration.wallabag_only_url": "URL のみを送信 (完全なコンテンツではなく)",
|
||||
"form.integration.wallabag_password": "Wallabag のパスワード",
|
||||
"form.integration.wallabag_username": "Wallabag のユーザー名",
|
||||
"form.integration.wallabag_tags": "Wallabag Tags",
|
||||
"form.integration.webhook_activate": "Enable Webhooks",
|
||||
"form.integration.webhook_secret": "Webhooks Secret",
|
||||
"form.integration.webhook_url": "Default Webhook URL",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Read articles by opening external links",
|
||||
"form.integration.wallabag_tags": "Wallabag タグ",
|
||||
"form.integration.webhook_activate": "Webhook を有効化",
|
||||
"form.integration.webhook_secret": "Webhook シークレット",
|
||||
"form.integration.webhook_url": "デフォルトの Webhook URL",
|
||||
"form.prefs.fieldset.application_settings": "アプリケーション設定",
|
||||
"form.prefs.fieldset.authentication_settings": "認証設定",
|
||||
"form.prefs.fieldset.global_feed_settings": "グローバルフィード設定",
|
||||
"form.prefs.fieldset.reader_settings": "リーダー設定",
|
||||
"form.prefs.help.external_font_hosts": "許可する外部フォントホストをスペース区切りで指定します。例: \"fonts.gstatic.com fonts.googleapis.com\"",
|
||||
"form.prefs.label.always_open_external_links": "外部リンクを開いて記事を読む",
|
||||
"form.prefs.label.categories_sorting_order": "カテゴリの表示順",
|
||||
"form.prefs.label.cjk_reading_speed": "中国語、韓国語、日本語の読書速度(文字数/分)",
|
||||
"form.prefs.label.custom_css": "カスタム CSS",
|
||||
@@ -353,29 +355,29 @@
|
||||
"form.prefs.label.entry_order": "記事の表示順の基準",
|
||||
"form.prefs.label.entry_sorting": "記事の表示順",
|
||||
"form.prefs.label.entry_swipe": "タッチスクリーンでスワイプ入力を有効にする",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.label.external_font_hosts": "外部フォントホスト",
|
||||
"form.prefs.label.gesture_nav": "エントリ間を移動するジェスチャー",
|
||||
"form.prefs.label.keyboard_shortcuts": "キーボードショートカットを有効にする",
|
||||
"form.prefs.label.language": "言語",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "手動で既読にする",
|
||||
"form.prefs.label.mark_read_on_media_completion": "音声/動画の再生が90%%に達したら既読にする",
|
||||
"form.prefs.label.mark_read_on_view": "表示時にエントリを自動的に既読としてマークします",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "表示時に既読にする。音声/動画は再生90%%で既読にする",
|
||||
"form.prefs.label.media_playback_rate": "オーディオ/ビデオの再生速度",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "外部リンクを新しいタブで開く(リンクに target=\"_blank\" を追加)",
|
||||
"form.prefs.label.show_reading_time": "記事の推定読書時間を表示する",
|
||||
"form.prefs.label.theme": "テーマ",
|
||||
"form.prefs.label.timezone": "タイムゾーン",
|
||||
"form.prefs.select.alphabetical": "アルファベット順",
|
||||
"form.prefs.select.browser": "Browser",
|
||||
"form.prefs.select.browser": "ブラウザ",
|
||||
"form.prefs.select.created_time": "記事の取得時刻",
|
||||
"form.prefs.select.fullscreen": "Fullscreen",
|
||||
"form.prefs.select.minimal_ui": "Minimal",
|
||||
"form.prefs.select.fullscreen": "フルスクリーン",
|
||||
"form.prefs.select.minimal_ui": "ミニマル",
|
||||
"form.prefs.select.none": "なし",
|
||||
"form.prefs.select.older_first": "古い記事を最初に",
|
||||
"form.prefs.select.publish_time": "記事の公開時刻",
|
||||
"form.prefs.select.recent_first": "新しい記事を最初に",
|
||||
"form.prefs.select.standalone": "Standalone",
|
||||
"form.prefs.select.standalone": "スタンドアロン",
|
||||
"form.prefs.select.swipe": "スワイプ",
|
||||
"form.prefs.select.tap": "ダブルタップ",
|
||||
"form.prefs.select.unread_count": "未読数",
|
||||
@@ -399,7 +401,7 @@
|
||||
"menu.feeds": "フィード一覧",
|
||||
"menu.flush_history": "履歴をクリア",
|
||||
"menu.history": "履歴",
|
||||
"menu.home_page": "Home page",
|
||||
"menu.home_page": "ホームページ",
|
||||
"menu.import": "インポート",
|
||||
"menu.integrations": "連携",
|
||||
"menu.logout": "ログアウト",
|
||||
@@ -413,17 +415,17 @@
|
||||
"menu.settings": "設定",
|
||||
"menu.shared_entries": "共有エントリ",
|
||||
"menu.show_all_entries": "すべての記事を表示",
|
||||
"menu.show_only_starred_entries": "Show only starred entries",
|
||||
"menu.show_only_starred_entries": "星付きのみを表示",
|
||||
"menu.show_only_unread_entries": "未読の記事だけを表示",
|
||||
"menu.starred": "星付き",
|
||||
"menu.title": "Menu",
|
||||
"menu.title": "メニュー",
|
||||
"menu.unread": "未読",
|
||||
"menu.users": "ユーザー一覧",
|
||||
"page.about.author": "作者:",
|
||||
"page.about.build_date": "ビルド日時:",
|
||||
"page.about.credits": "著作権表示",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.db_usage": "データベースサイズ:",
|
||||
"page.about.git_commit": "Git コミット:",
|
||||
"page.about.global_config_options": "グローバル構成オプション",
|
||||
"page.about.go_version": "Go バージョン:",
|
||||
"page.about.license": "ライセンス:",
|
||||
@@ -431,7 +433,7 @@
|
||||
"page.about.title": "ソフトウェア情報",
|
||||
"page.about.version": "バージョン:",
|
||||
"page.add_feed.choose_feed": "フィードを選択",
|
||||
"page.add_feed.label.url": "URL",
|
||||
"page.add_feed.label.url": "フィードURL",
|
||||
"page.add_feed.legend.advanced_options": "高度な設定",
|
||||
"page.add_feed.no_category": "カテゴリが存在しません。カテゴリが少なくとも1つ必要です。",
|
||||
"page.add_feed.submit": "フィードを探索して追加",
|
||||
@@ -453,7 +455,7 @@
|
||||
"page.categories_count": [
|
||||
"%d 件のカテゴリ"
|
||||
],
|
||||
"page.category_label": "Category: %s",
|
||||
"page.category_label": "カテゴリ: %s",
|
||||
"page.edit_category.title": "カテゴリを編集: %s",
|
||||
"page.edit_feed.etag_header": "ETag ヘッダー:",
|
||||
"page.edit_feed.last_check": "最終チェック:",
|
||||
@@ -467,18 +469,18 @@
|
||||
"%d 個のエラー"
|
||||
],
|
||||
"page.feeds.last_check": "最終チェック:",
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.next_check": "次回チェック:",
|
||||
"page.feeds.read_counter": "既読記事の数",
|
||||
"page.feeds.title": "フィード一覧",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "トップに戻る",
|
||||
"page.history.title": "履歴",
|
||||
"page.import.title": "インポート",
|
||||
"page.integration.bookmarklet": "ブックマークレット",
|
||||
"page.integration.bookmarklet.help": "この特別なリンクを使ってブラウザから直接ウェブサイトのフィードを購読できます。",
|
||||
"page.integration.bookmarklet.instructions": "このリンクをブラウザのブックマークへドラッグしてください。",
|
||||
"page.integration.bookmarklet.name": "Miniflux に追加",
|
||||
"page.integration.miniflux_api": "Miniflux API",
|
||||
"page.integration.miniflux_api_endpoint": "API Endpoint",
|
||||
"page.integration.miniflux_api": "MinifluxのAPI",
|
||||
"page.integration.miniflux_api_endpoint": "APIエンドポイント",
|
||||
"page.integration.miniflux_api_password": "パスワード",
|
||||
"page.integration.miniflux_api_password_value": "アカウントのパスワード",
|
||||
"page.integration.miniflux_api_username": "ユーザー名",
|
||||
@@ -524,7 +526,7 @@
|
||||
"page.login.title": "ログイン",
|
||||
"page.login.webauthn_login": "パスキーでログイン",
|
||||
"page.login.webauthn_login.error": "パスキーでログインできない",
|
||||
"page.login.webauthn_login.help": "Please enter your username if you're using a security key. This is not required if you are using a Passkey (discoverable credentials).",
|
||||
"page.login.webauthn_login.help": "セキュリティキーを使用する場合はユーザー名を入力してください。パスキー(検出可能な認証情報)の場合は不要です。",
|
||||
"page.new_api_key.title": "新しい API キー",
|
||||
"page.new_category.title": "新規カテゴリ",
|
||||
"page.new_user.title": "新規ユーザー",
|
||||
@@ -539,21 +541,21 @@
|
||||
"page.sessions.table.current_session": "現在のセッション",
|
||||
"page.sessions.table.date": "日付",
|
||||
"page.sessions.table.ip": "IP アドレス",
|
||||
"page.sessions.table.user_agent": "User Agent",
|
||||
"page.sessions.table.user_agent": "ユーザーエージェント",
|
||||
"page.sessions.title": "セッション",
|
||||
"page.settings.link_google_account": "Google アカウントと接続する",
|
||||
"page.settings.link_oidc_account": "%s アカウントと接続する",
|
||||
"page.settings.title": "設定",
|
||||
"page.settings.unlink_google_account": "Google アカウントと接続を解除する",
|
||||
"page.settings.unlink_oidc_account": "%s アカウントと接続を解除する",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.added_on": "Added On",
|
||||
"page.settings.webauthn.actions": "操作",
|
||||
"page.settings.webauthn.added_on": "追加日",
|
||||
"page.settings.webauthn.delete": [
|
||||
"%d 個のパスキーを削除"
|
||||
],
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.last_seen_on": "最終使用日",
|
||||
"page.settings.webauthn.passkey_name": "パスキー名",
|
||||
"page.settings.webauthn.passkeys": "パスキー",
|
||||
"page.settings.webauthn.register": "パスキーを登録する",
|
||||
"page.settings.webauthn.register.error": "パスキーを登録できません",
|
||||
"page.shared_entries.title": "共有エントリ",
|
||||
@@ -579,7 +581,7 @@
|
||||
"page.users.never_logged": "未ログイン",
|
||||
"page.users.title": "ユーザー一覧",
|
||||
"page.users.username": "ユーザー名",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"page.webauthn_rename.title": "パスキー名の変更",
|
||||
"pagination.first": "最初",
|
||||
"pagination.last": "最後",
|
||||
"pagination.next": "次",
|
||||
@@ -611,4 +613,4 @@
|
||||
"time_elapsed.yesterday": "昨日",
|
||||
"tooltip.keyboard_shortcuts": "キーボードショートカット: %s",
|
||||
"tooltip.logged_user": "%s としてログイン中"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,8 +167,8 @@
|
||||
"form.feed.fieldset.rules": "Kui-chek",
|
||||
"form.feed.label.allow_self_signed_certificates": "ún-chún chū chhiam ah-sī bô-hāu ê pîn-chèng",
|
||||
"form.feed.label.apprise_service_urls": "Sú-iōng tō͘-tiám keh khui ê Apprise ho̍k-bū bāng-chí lia̍t-pió",
|
||||
"form.feed.label.block_filter_entry_rules": "Entry Blocking Rules",
|
||||
"form.feed.label.blocklist_rules": "Regex-Based Blocking Filters",
|
||||
"form.feed.label.block_filter_entry_rules": "Chhōa siau-sit ê kè-kng",
|
||||
"form.feed.label.blocklist_rules": "Regex chhōa sè-khuán",
|
||||
"form.feed.label.category": "lūi-pia̍t",
|
||||
"form.feed.label.cookie": "Siat-tēng Cookies",
|
||||
"form.feed.label.crawler": "Lia̍h goân-tóe lōe-iông",
|
||||
@@ -181,8 +181,8 @@
|
||||
"form.feed.label.fetch_via_proxy": "Iōng tī su-hāu-khì siat-tēng ê proxy",
|
||||
"form.feed.label.hide_globally": "Tī choân-he̍k ah-bōe tha̍k--ê lia̍t-pió am-khàm siau-sit",
|
||||
"form.feed.label.ignore_http_cache": "Pàng-ba̍k HTTP cache",
|
||||
"form.feed.label.keep_filter_entry_rules": "Entry Allow Rules",
|
||||
"form.feed.label.keeplist_rules": "Regex-Based Keep Filters",
|
||||
"form.feed.label.keep_filter_entry_rules": "Bêng ê siau-sit hō͘-chiâⁿ kui-chek",
|
||||
"form.feed.label.keeplist_rules": "Regex pó͘-tē ê pò͘-chûn kui-chek",
|
||||
"form.feed.label.no_media_player": "Bô mûi-thé hòng-sàng khì (im-sìn, sī-sìn)",
|
||||
"form.feed.label.ntfy_activate": "Thui-sàng siau-sit khì ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy ū-siat iu-sian sūn-sū",
|
||||
@@ -192,7 +192,7 @@
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy siōng kē iu-sian sūn-sū",
|
||||
"form.feed.label.ntfy_priority": "Ntfy iu-sian sūn-sū",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (soán thiⁿ)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.proxy_url": "Proxy ê URL",
|
||||
"form.feed.label.pushover_activate": "Pó-chûn siau-sit kàu pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover ū-siat iu-sian sūn-sū",
|
||||
"form.feed.label.pushover_high_priority": "Pushover koân iu-sian sūn-sū",
|
||||
@@ -200,7 +200,7 @@
|
||||
"form.feed.label.pushover_max_priority": "Pushover siōng koân iu-sian sūn-sū",
|
||||
"form.feed.label.pushover_min_priority": "Pushover siōng kē iu-sian sūn-sū",
|
||||
"form.feed.label.pushover_priority": "Pushover siau-sit iu-sian sūn-sū",
|
||||
"form.feed.label.rewrite_rules": "Content Rewrite Rules",
|
||||
"form.feed.label.rewrite_rules": "Lōe-iông têng-siá kui-chek",
|
||||
"form.feed.label.scraper_rules": "Lia̍h ê kui-chek",
|
||||
"form.feed.label.site_url": "Bāng-chām bāng-chí",
|
||||
"form.feed.label.title": "Piau-tôe",
|
||||
@@ -208,13 +208,13 @@
|
||||
"form.feed.label.user_agent": "Ngī kái sú-iōng-lâng tāi-lí",
|
||||
"form.feed.label.webhook_url": "Ngī kái webhook bāng-chí",
|
||||
"form.import.label.file": "OPML tóng-àn",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.archiveorg_activate": "Push entries to archive.org",
|
||||
"form.import.label.url": "URL tiàm-chhī",
|
||||
"form.integration.archiveorg_activate": "Pó͘-chûn siau-sit kàu archive.org",
|
||||
"form.integration.apprise_activate": "Thui sàng siau-sit khì Apprise",
|
||||
"form.integration.apprise_services_url": "Iōng tō͘-tiám keh khui ê Apprise ho̍k-bū bāng-chí lia̍t-pió",
|
||||
"form.integration.apprise_url": "Apprise API bāng-chí",
|
||||
"form.integration.betula_activate": "Pó-chûn siau-sit kàu Betula",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.betula_token": "Betula tō͘-khíng",
|
||||
"form.integration.betula_url": "Betula su-hāu-khì bāng-chí",
|
||||
"form.integration.cubox_activate": "Pó-chûn siau-sit khì Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API liân-kiat",
|
||||
@@ -251,9 +251,9 @@
|
||||
"form.integration.linkding_endpoint": "Linkding API thâu",
|
||||
"form.integration.linkding_tags": "Linkding khan-á",
|
||||
"form.integration.linktaco_activate": "Pó-chûn siau-sit kàu LinkTaco",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API tō͘-khíng",
|
||||
"form.integration.linktaco_api_token_hint": "Chhú-tek lí ê kò-jîn chún-chhú token tī",
|
||||
"form.integration.linktaco_org_slug": "Organization Slug",
|
||||
"form.integration.linktaco_org_slug": "Chō͘-hêng hiân-jī",
|
||||
"form.integration.linktaco_tags": "khan-á (siōn-koân 10, iōng tō͘-tiám keh khui)",
|
||||
"form.integration.linktaco_tags_hint": "Siōn-koân 10 khan-á, iōng tō͘-tiám keh khui",
|
||||
"form.integration.linktaco_visibility": "Kò-chhiah-kì sìa?",
|
||||
@@ -262,15 +262,16 @@
|
||||
"form.integration.linktaco_visibility_hint": "Su-lîn sìa tík tio̍h-ài chù-hêng LinkTaco kháu-chō",
|
||||
"form.integration.linkwarden_activate": "Pó-chûn siau-sit kàu Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API só-sî",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden Base URL",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden ki-kiân bāng-chí",
|
||||
"form.integration.linkwarden_collection_id": "Linkwarden sò͘-tē ID",
|
||||
"form.integration.matrix_bot_activate": "Thui-sàng siau-sit kàu Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Matrix pâng-keng ID",
|
||||
"form.integration.matrix_bot_password": "Matrix bi̍t-bé",
|
||||
"form.integration.matrix_bot_url": "Matrix su-hāu-khìbāng-chí",
|
||||
"form.integration.matrix_bot_user": "Matrix kháu-chō miâ",
|
||||
"form.integration.notion_activate": "Pó-chûn siau-sit kàu Notion",
|
||||
"form.integration.notion_page_id": "Notion Page ID",
|
||||
"form.integration.notion_token": "Notion Secret Token",
|
||||
"form.integration.notion_page_id": "Notion iah-piⁿ ID",
|
||||
"form.integration.notion_token": "Notion bí-koān tō͘-khíng",
|
||||
"form.integration.ntfy_activate": "Thui-sàng siau-sit kàu Ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API só-sî (soán thiⁿ)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon bāng-chí (soán thiⁿ)",
|
||||
@@ -288,27 +289,28 @@
|
||||
"form.integration.pinboard_activate": "Pó-chûn siau-sit kàu Pinboard",
|
||||
"form.integration.pinboard_bookmark": "Chù chòe ah-bōe tha̍k",
|
||||
"form.integration.pinboard_tags": "Pinboard khan-á",
|
||||
"form.integration.pinboard_token": "Pinboard API Token",
|
||||
"form.integration.pinboard_token": "Pinboard API tō͘-khíng",
|
||||
"form.integration.pushover_activate": "Pó-chûn siau-sit kàu Pushover",
|
||||
"form.integration.pushover_device": "Pushover ki-hì (soán thiⁿ)",
|
||||
"form.integration.pushover_prefix": "Pushover URL tó͘-bí (soán thiⁿ)",
|
||||
"form.integration.pushover_token": "Pushover application API só-sî",
|
||||
"form.integration.pushover_user": "Pushover sú-iōng-lâng só-sî",
|
||||
"form.integration.raindrop_activate": "Pó-chûn siau-sit kàu Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "khan-á (iōng tō͘-tiám keh khui)",
|
||||
"form.integration.raindrop_token": "Raindrop Acess Token",
|
||||
"form.integration.raindrop_collection_id": "Sò͘-tē ID",
|
||||
"form.integration.raindrop_tags": "Khan-á (iōng tō͘-tiám keh khui)",
|
||||
"form.integration.raindrop_token": "Raindrop thè-khui só-sî",
|
||||
"form.integration.readeck_activate": "Pó-chûn siau-sit kàu Readeck",
|
||||
"form.integration.readeck_api_key": "Readeck API só-sî",
|
||||
"form.integration.readeck_endpoint": "Readeck API thâu",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_labels": "Readeck khan-á",
|
||||
"form.integration.readeck_only_url": "Kan-na thoân bāng-chí (m̄ sī oân-chéng ê lōe-iông)",
|
||||
"form.integration.readeck_push_activate": "Sin siau-sit thàn-lâi chūi-tō͘ thui kàu Readeck",
|
||||
"form.integration.readwise_activate": "Pó-chûn siau-sit kàu Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader Acess Token",
|
||||
"form.integration.readwise_api_key_link": "Chhú-tek lí ê Readwise Acess Token",
|
||||
"form.integration.readwise_api_key": "Readwise Reader thè-khui só-sî",
|
||||
"form.integration.readwise_api_key_link": "Chhú-tek lí ê Readwise thè-khui só-sî",
|
||||
"form.integration.rssbridge_activate": "Sin cheng-ka siau-sit lâi-goân ê sî tio̍h RSS-Bridge",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge su-hāu-khì的bāng-chí",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge chheng-bêng só-sî",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge su-hāu-khì ê bāng-chí",
|
||||
"form.integration.shaarli_activate": "Pó-chûn siau-sit kàu Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API só-sî",
|
||||
"form.integration.shaarli_endpoint": "Shaarli bāng-chí",
|
||||
@@ -323,19 +325,19 @@
|
||||
"form.integration.telegram_bot_disable_notification": "Têng iōng thong-ti",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Thêng iōng bāng-ia̍h ū-lám",
|
||||
"form.integration.telegram_bot_token": "Bot Token",
|
||||
"form.integration.telegram_chat_id": "Chat ID",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
"form.integration.telegram_chat_id": "Lîn-lūn ID",
|
||||
"form.integration.telegram_topic_id": "Siōng-tê ID",
|
||||
"form.integration.wallabag_activate": "Pó-chûn siau-sit kàu Wallabag",
|
||||
"form.integration.wallabag_client_id": "Wallabag kheh-hō͘ thâu ID",
|
||||
"form.integration.wallabag_client_secret": "Wallabag kheh-hō͘ thâu só-sî",
|
||||
"form.integration.wallabag_endpoint": "Wallabag Base URL",
|
||||
"form.integration.wallabag_endpoint": "Wallabag ki-kiân bāng-chí",
|
||||
"form.integration.wallabag_only_url": "Kan-na thoân bāng-chí (m̄ sī oân-chéng ê lōe-iông)",
|
||||
"form.integration.wallabag_password": "Wallabag bi̍t-bé",
|
||||
"form.integration.wallabag_username": "Wallabag kháu-chō miâ",
|
||||
"form.integration.wallabag_tags": "Wallabag Tags",
|
||||
"form.integration.webhook_activate": "Khai-sí iōng Webhooks",
|
||||
"form.integration.webhook_secret": "Webhooks Secret",
|
||||
"form.integration.webhook_url": "Default Webhook bāng-chí",
|
||||
"form.integration.wallabag_tags": "Wallabag khan-á",
|
||||
"form.integration.webhook_activate": "Khai-sí Webhooks",
|
||||
"form.integration.webhook_secret": "Webhooks bí-miâ",
|
||||
"form.integration.webhook_url": "Koán-tē Webhook bāng-chí",
|
||||
"form.prefs.fieldset.application_settings": "Èng-iōng thêng-sek siat-tēng",
|
||||
"form.prefs.fieldset.authentication_settings": "Sú-iōng-lâng giām-chèng siat-tēng",
|
||||
"form.prefs.fieldset.global_feed_settings": "Choân-he̍k siau-sit lâi-goân siat-tēng",
|
||||
@@ -423,7 +425,7 @@
|
||||
"page.about.build_date": "Kiàn-tì li̍t-kî:",
|
||||
"page.about.credits": "Pán-koân",
|
||||
"page.about.db_usage": "Database chhài-chhiú:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.git_commit": "Git commit:",
|
||||
"page.about.global_config_options": "Choân-he̍k siat-tēng soán-hāng",
|
||||
"page.about.go_version": "Go pán-pún:",
|
||||
"page.about.license": "Pàng-koân:",
|
||||
@@ -470,14 +472,14 @@
|
||||
"page.feeds.next_check": "Āu-pái kiám-cha sî-kan:",
|
||||
"page.feeds.read_counter": "Tha̍k kè--ê siau-sit sò͘",
|
||||
"page.feeds.title": "Siau-sit lâi-goân",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "Thâu-tiō siōng-ló͘",
|
||||
"page.history.title": "Kì-lo̍k",
|
||||
"page.import.title": "Hōe-li̍p",
|
||||
"page.integration.bookmarklet": "Chheh-chhiam ke-si",
|
||||
"page.integration.bookmarklet.help": "Lí ē-sái iōng chit ê te̍k-pia̍t ê chheh-chhiam ti̍t-chiap tēng bāng-ia̍h ê siau-sit",
|
||||
"page.integration.bookmarklet.instructions": "Kā chit ê liân-kiat thoa khì iû-lám khì ê chheh-chhiam lân",
|
||||
"page.integration.bookmarklet.name": "Siu-chông Miniflux",
|
||||
"page.integration.miniflux_api": "Miniflux API",
|
||||
"page.integration.miniflux_api": "Miniflux ê API",
|
||||
"page.integration.miniflux_api_endpoint": "API thâu",
|
||||
"page.integration.miniflux_api_password": "Bi̍t-bé",
|
||||
"page.integration.miniflux_api_password_value": "Lí ê kháu-chō ê bi̍t-bé",
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
"entry.status.title": "Verander artikelstatus",
|
||||
"entry.status.toast.read": "Gemarkeerd als gelezen",
|
||||
"entry.status.toast.unread": "Gemarkeerd als ongelezen",
|
||||
"entry.tags.label": "Tags:",
|
||||
"entry.tags.label": "Labels:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Toon %d extra tag",
|
||||
"Toon %d extra tags"
|
||||
@@ -179,7 +179,7 @@
|
||||
"form.feed.label.disable_http2": "HTTP/2 uitschakelen om fingerprinting te voorkomen",
|
||||
"form.feed.label.disabled": "Deze feed niet vernieuwen",
|
||||
"form.feed.label.feed_password": "Feed wachtwoord",
|
||||
"form.feed.label.feed_url": "Feed URL",
|
||||
"form.feed.label.feed_url": "Feed-URL",
|
||||
"form.feed.label.feed_username": "Feed gebruikersnaam",
|
||||
"form.feed.label.fetch_via_proxy": "Gebruik de proxy die op applicatieniveau is geconfigureerd",
|
||||
"form.feed.label.hide_globally": "Verberg artikelen in de globale ongelezen lijst",
|
||||
@@ -195,7 +195,7 @@
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy minimale prioriteit",
|
||||
"form.feed.label.ntfy_priority": "Ntfy prioriteit",
|
||||
"form.feed.label.ntfy_topic": "Ntfy onderwerp (optioneel)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.proxy_url": "Proxy-URL",
|
||||
"form.feed.label.pushover_activate": "Stuur artikelen naar pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover standaard prioriteit",
|
||||
"form.feed.label.pushover_high_priority": "Pushover hoge prioriteit",
|
||||
@@ -212,17 +212,17 @@
|
||||
"form.feed.label.webhook_url": "Overschrijf webhook URL",
|
||||
"form.import.label.file": "OPML-bestand",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.archiveorg_activate": "Push entries to archive.org",
|
||||
"form.integration.archiveorg_activate": "Artikelen sturen naar archive.org",
|
||||
"form.integration.apprise_activate": "Artikelen opslaan in Apprise",
|
||||
"form.integration.apprise_services_url": "Door komma's gescheiden lijst van Apprise service URL's",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.apprise_url": "Apprise API-URL",
|
||||
"form.integration.betula_activate": "Artikelen opslaan in Betula",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.betula_token": "Betula-token",
|
||||
"form.integration.betula_url": "Betula-server-URL",
|
||||
"form.integration.cubox_activate": "Artikelen opslaan in Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API-link",
|
||||
"form.integration.discord_activate": "Artikelen opslaan in Discord",
|
||||
"form.integration.discord_webhook_link": "Discord Webhook link",
|
||||
"form.integration.discord_webhook_link": "Discord-webhooklink",
|
||||
"form.integration.espial_activate": "Artikelen opslaan in Espial",
|
||||
"form.integration.espial_api_key": "Espial API-sleutel",
|
||||
"form.integration.espial_endpoint": "Espial URL",
|
||||
@@ -245,7 +245,7 @@
|
||||
"form.integration.linkace_activate": "Artikelen opslaan in LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API-sleutel",
|
||||
"form.integration.linkace_check_disabled": "Koppelingcontrole uitschakelen",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_endpoint": "LinkAce API-eindpunt",
|
||||
"form.integration.linkace_is_private": "Koppeling als privé markeren",
|
||||
"form.integration.linkace_tags": "LinkAce tags",
|
||||
"form.integration.linkding_activate": "Artikelen opslaan in Linkding",
|
||||
@@ -254,9 +254,9 @@
|
||||
"form.integration.linkding_endpoint": "Linkding URL",
|
||||
"form.integration.linkding_tags": "Linkding tags",
|
||||
"form.integration.linktaco_activate": "Artikelen opslaan in LinkTaco",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API-token",
|
||||
"form.integration.linktaco_api_token_hint": "Verkrijg uw persoonlijke toegangstoken op",
|
||||
"form.integration.linktaco_org_slug": "Organization Slug",
|
||||
"form.integration.linktaco_org_slug": "Organisatie-slug",
|
||||
"form.integration.linktaco_tags": "Tags (max 10, kommagescheiden)",
|
||||
"form.integration.linktaco_tags_hint": "Maximaal 10 tags, kommagescheiden",
|
||||
"form.integration.linktaco_visibility": "Zichtbaarheid",
|
||||
@@ -266,14 +266,15 @@
|
||||
"form.integration.linkwarden_activate": "Artikelen opslaan in Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API-sleutel",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden Basis URL",
|
||||
"form.integration.linkwarden_collection_id": "Linkwarden collectie-ID",
|
||||
"form.integration.matrix_bot_activate": "Nieuwe artikelen opslaan in Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID van Matrix-kamer",
|
||||
"form.integration.matrix_bot_password": "Wachtwoord voor Matrix-gebruiker",
|
||||
"form.integration.matrix_bot_url": "URL van de Matrix-server",
|
||||
"form.integration.matrix_bot_user": "Matrix gebruikersnaam",
|
||||
"form.integration.notion_activate": "Artikelen opslaan in Notion",
|
||||
"form.integration.notion_page_id": "Notion Page ID",
|
||||
"form.integration.notion_token": "Notion Secret Token",
|
||||
"form.integration.notion_page_id": "Notion-pagina-ID",
|
||||
"form.integration.notion_token": "Notion geheim token",
|
||||
"form.integration.ntfy_activate": "Stuur artikelen naar ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optioneel)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optioneel)",
|
||||
@@ -292,42 +293,43 @@
|
||||
"form.integration.pinboard_bookmark": "Markeer favoriet als ongelezen",
|
||||
"form.integration.pinboard_tags": "Pinboard tags",
|
||||
"form.integration.pinboard_token": "Pinboard API token",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
"form.integration.pushover_token": "Pushover application API token",
|
||||
"form.integration.pushover_user": "Pushover user key",
|
||||
"form.integration.pushover_activate": "Artikelen sturen naar Pushover",
|
||||
"form.integration.pushover_device": "Pushover-apparaat (optioneel)",
|
||||
"form.integration.pushover_prefix": "Pushover URL-prefix (optioneel)",
|
||||
"form.integration.pushover_token": "Pushover API-token van de applicatie",
|
||||
"form.integration.pushover_user": "Pushover gebruikerssleutel",
|
||||
"form.integration.raindrop_activate": "Artikelen opslaan in Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Collectie ID",
|
||||
"form.integration.raindrop_tags": "Tags (commagescheiden)",
|
||||
"form.integration.raindrop_token": "Raindrop Token",
|
||||
"form.integration.readeck_activate": "Artikelen opslaan in Readeck",
|
||||
"form.integration.readeck_api_key": "Readeck API-sleutel",
|
||||
"form.integration.readeck_endpoint": "Readeck URL",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_endpoint": "Readeck-URL",
|
||||
"form.integration.readeck_labels": "Readeck-labels",
|
||||
"form.integration.readeck_only_url": "Alleen URL verzenden (in plaats van volledige inhoud)",
|
||||
"form.integration.readeck_push_activate": "Nieuwe artikelen automatisch naar Readeck sturen",
|
||||
"form.integration.readwise_activate": "Artikelen opslaan in Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key": "Readwise Reader-toegangstoken",
|
||||
"form.integration.readwise_api_key_link": "Readwise Access Token ophalen",
|
||||
"form.integration.rssbridge_activate": "Controleer RSS-Bridge bij het toevoegen van abonnementen",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.rssbridge_token": "Authenticatietoken voor RSS-Bridge",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge-server-URL",
|
||||
"form.integration.shaarli_activate": "Artikelen opslaan in Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API-geheim",
|
||||
"form.integration.shaarli_endpoint": "Shaarli-URL",
|
||||
"form.integration.shiori_activate": "Artikelen opslaan in Shiori",
|
||||
"form.integration.shiori_endpoint": "Shiori URL",
|
||||
"form.integration.shiori_password": "Shiori wachtwoord",
|
||||
"form.integration.shiori_username": "Shiori gebruikersnaam",
|
||||
"form.integration.slack_activate": "Artikelen opslaan in Slack",
|
||||
"form.integration.slack_webhook_link": "Slack Webhook link",
|
||||
"form.integration.slack_webhook_link": "Slack-webhooklink",
|
||||
"form.integration.telegram_bot_activate": "Stuur nieuwe artikelen naar Telegram",
|
||||
"form.integration.telegram_bot_disable_buttons": "Knoppen uitschakelen",
|
||||
"form.integration.telegram_bot_disable_notification": "Notificatie uitschakelen",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Webpaginavoorbeeld uitschakelen",
|
||||
"form.integration.telegram_bot_token": "Bot token",
|
||||
"form.integration.telegram_chat_id": "Chat ID",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
"form.integration.telegram_bot_token": "Bot-token",
|
||||
"form.integration.telegram_chat_id": "Chat-ID",
|
||||
"form.integration.telegram_topic_id": "Topic-ID",
|
||||
"form.integration.wallabag_activate": "Artikelen opslaan in Wallabag",
|
||||
"form.integration.wallabag_client_id": "Wallabag Client-ID",
|
||||
"form.integration.wallabag_client_secret": "Wallabag Client-Secret",
|
||||
@@ -335,10 +337,10 @@
|
||||
"form.integration.wallabag_only_url": "Alleen URL verzenden (in plaats van volledige inhoud)",
|
||||
"form.integration.wallabag_password": "Wallabag wachtwoord",
|
||||
"form.integration.wallabag_username": "Wallabag gebruikersnaam",
|
||||
"form.integration.wallabag_tags": "Wallabag Tags",
|
||||
"form.integration.wallabag_tags": "Wallabag-tags",
|
||||
"form.integration.webhook_activate": "Webhooks activeren",
|
||||
"form.integration.webhook_secret": "Webhooks Secret",
|
||||
"form.integration.webhook_url": "Standard Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhooks geheim",
|
||||
"form.integration.webhook_url": "Standaard Webhook-URL",
|
||||
"form.prefs.fieldset.application_settings": "Applicatie Instellingen",
|
||||
"form.prefs.fieldset.authentication_settings": "Authenticatie Instellingen",
|
||||
"form.prefs.fieldset.global_feed_settings": "Globale Feed Instellingen",
|
||||
@@ -370,7 +372,7 @@
|
||||
"form.prefs.label.theme": "Thema",
|
||||
"form.prefs.label.timezone": "Tijdzone",
|
||||
"form.prefs.select.alphabetical": "Alfabetisch",
|
||||
"form.prefs.select.browser": "Browser",
|
||||
"form.prefs.select.browser": "Systeembrowser",
|
||||
"form.prefs.select.created_time": "Tijdstip van aanmaken artikel",
|
||||
"form.prefs.select.fullscreen": "Volledig scherm",
|
||||
"form.prefs.select.minimal_ui": "Minimaal",
|
||||
@@ -378,7 +380,7 @@
|
||||
"form.prefs.select.older_first": "Oudere artikelen eerst",
|
||||
"form.prefs.select.publish_time": "Tijdstip van publiceren artikel",
|
||||
"form.prefs.select.recent_first": "Recente artikelen eerst",
|
||||
"form.prefs.select.standalone": "Standalone",
|
||||
"form.prefs.select.standalone": "Standalone-modus",
|
||||
"form.prefs.select.swipe": "Vegen",
|
||||
"form.prefs.select.tap": "Dubbeltik",
|
||||
"form.prefs.select.unread_count": "Aantal ongelezen artikelen",
|
||||
@@ -399,7 +401,7 @@
|
||||
"menu.edit_feed": "Bewerken",
|
||||
"menu.export": "Exporteren",
|
||||
"menu.feed_entries": "Artikelen",
|
||||
"menu.feeds": "Feeds",
|
||||
"menu.feeds": "Abonnementen",
|
||||
"menu.flush_history": "Verwijder geschiedenis",
|
||||
"menu.history": "Geschiedenis",
|
||||
"menu.home_page": "Startpagina",
|
||||
@@ -426,7 +428,7 @@
|
||||
"page.about.build_date": "Compilatiedatum:",
|
||||
"page.about.credits": "Credits",
|
||||
"page.about.db_usage": "Databasegrootte:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.git_commit": "Git-commit:",
|
||||
"page.about.global_config_options": "Globale Configuratie Opties",
|
||||
"page.about.go_version": "Go versie:",
|
||||
"page.about.license": "Licentie:",
|
||||
@@ -434,7 +436,7 @@
|
||||
"page.about.title": "Over",
|
||||
"page.about.version": "Versie:",
|
||||
"page.add_feed.choose_feed": "Feed kiezen",
|
||||
"page.add_feed.label.url": "URL",
|
||||
"page.add_feed.label.url": "URL-adres",
|
||||
"page.add_feed.legend.advanced_options": "Geavanceerde opties",
|
||||
"page.add_feed.no_category": "Er is geen categorie. Je moet minstens één categorie hebben.",
|
||||
"page.add_feed.submit": "Feed zoeken",
|
||||
@@ -444,7 +446,7 @@
|
||||
"page.api_keys.table.created_at": "Aanmaakdatum",
|
||||
"page.api_keys.table.description": "Omschrijving",
|
||||
"page.api_keys.table.last_used_at": "Laatst gebruikt",
|
||||
"page.api_keys.table.token": "Token",
|
||||
"page.api_keys.table.token": "API-token",
|
||||
"page.api_keys.title": "API-sleutels",
|
||||
"page.categories.entries": "Artikelen",
|
||||
"page.categories.feed_count": [
|
||||
@@ -462,7 +464,7 @@
|
||||
"page.edit_category.title": "Bewerk categorie: %s",
|
||||
"page.edit_feed.etag_header": "ETAG header:",
|
||||
"page.edit_feed.last_check": "Laatste controle:",
|
||||
"page.edit_feed.last_modified_header": "LastModified header:",
|
||||
"page.edit_feed.last_modified_header": "LastModified-header:",
|
||||
"page.edit_feed.last_parsing_error": "Laatste analysefout",
|
||||
"page.edit_feed.no_header": "Geen",
|
||||
"page.edit_feed.title": "Bewerk feed: %s",
|
||||
@@ -476,14 +478,14 @@
|
||||
"page.feeds.next_check": "Volgende controle:",
|
||||
"page.feeds.read_counter": "Aantal gelezen artikelen",
|
||||
"page.feeds.title": "Feeds",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "Terug naar boven",
|
||||
"page.history.title": "Geschiedenis",
|
||||
"page.import.title": "Importeren",
|
||||
"page.integration.bookmarklet": "Bookmarklet",
|
||||
"page.integration.bookmarklet.help": "Gebruik deze link als bookmark in je browser om je direct te abonneren op een website.",
|
||||
"page.integration.bookmarklet.instructions": "Sleep deze link naar je bookmarks.",
|
||||
"page.integration.bookmarklet.name": "Toevoegen aan Miniflux",
|
||||
"page.integration.miniflux_api": "Miniflux API",
|
||||
"page.integration.miniflux_api": "Miniflux-API",
|
||||
"page.integration.miniflux_api_endpoint": "API-URL",
|
||||
"page.integration.miniflux_api_password": "Wachtwoord",
|
||||
"page.integration.miniflux_api_password_value": "Wachtwoord van jouw account",
|
||||
@@ -629,4 +631,4 @@
|
||||
"time_elapsed.yesterday": "gisteren",
|
||||
"tooltip.keyboard_shortcuts": "Sneltoets: %s",
|
||||
"tooltip.logged_user": "Ingelogd als %s"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +269,7 @@
|
||||
"form.integration.linkwarden_activate": "Zapisuj wpisy w Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Klucz API do Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Podstawowy adres URL Linkwarden",
|
||||
"form.integration.linkwarden_collection_id": "Identyfikator kolekcji Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Przesyłaj nowe wpisy do Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Identyfikator pokoju Matrix",
|
||||
"form.integration.matrix_bot_password": "Hasło do Matrix",
|
||||
@@ -309,6 +310,7 @@
|
||||
"form.integration.readeck_endpoint": "Adres URL Readeck",
|
||||
"form.integration.readeck_labels": "Etykiety Readeck",
|
||||
"form.integration.readeck_only_url": "Wysyłaj tylko adres URL (zamiast pełnej treści)",
|
||||
"form.integration.readeck_push_activate": "Automatycznie przesyłaj nowe wpisy do Readeck",
|
||||
"form.integration.readwise_activate": "Zapisuj wpisy w czytniku Readwise",
|
||||
"form.integration.readwise_api_key": "Token dostępu do czytnika Readwise",
|
||||
"form.integration.readwise_api_key_link": "Zdobądź token dostępu Readwise",
|
||||
@@ -647,4 +649,4 @@
|
||||
"time_elapsed.yesterday": "wczoraj",
|
||||
"tooltip.keyboard_shortcuts": "Skróty klawiszowe: %s",
|
||||
"tooltip.logged_user": "Zalogowany jako %s"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,12 +216,12 @@
|
||||
"form.integration.apprise_activate": "Enviar itens para o Apprise",
|
||||
"form.integration.apprise_services_url": "Lista de URLs de serviços Apprise separadas por vírgula",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_activate": "Salvar itens no Betula",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.integration.discord_activate": "Push entries to Discord",
|
||||
"form.integration.cubox_activate": "Salvar itens no Cubox",
|
||||
"form.integration.cubox_api_link": "Link da API do Cubox",
|
||||
"form.integration.discord_activate": "Enviar itens para o Discord",
|
||||
"form.integration.discord_webhook_link": "Discord Webhook link",
|
||||
"form.integration.espial_activate": "Salvar itens no Espial",
|
||||
"form.integration.espial_api_key": "Chave de API do Espial",
|
||||
@@ -241,7 +241,7 @@
|
||||
"form.integration.karakeep_activate": "Salvar itens no Karakeep",
|
||||
"form.integration.karakeep_api_key": "Chave de API do Karakeep",
|
||||
"form.integration.karakeep_url": "Endpoint de API do Karakeep",
|
||||
"form.integration.karakeep_tags": "Karakeep Tags",
|
||||
"form.integration.karakeep_tags": "Etiquetas do Karakeep",
|
||||
"form.integration.linkace_activate": "Salvar itens no LinkAce",
|
||||
"form.integration.linkace_api_key": "Chave de API do LinkAce",
|
||||
"form.integration.linkace_check_disabled": "Desativar verificação de link",
|
||||
@@ -252,11 +252,11 @@
|
||||
"form.integration.linkding_api_key": "Chave de API do Linkding",
|
||||
"form.integration.linkding_bookmark": "Salvar marcador como não lido",
|
||||
"form.integration.linkding_endpoint": "Endpoint de API do Linkding",
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkding_tags": "Etiquetas do Linkding",
|
||||
"form.integration.linktaco_activate": "Salvar itens no LinkTaco",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token_hint": "Obtenha seu token de acesso pessoal em",
|
||||
"form.integration.linktaco_org_slug": "Organization Slug",
|
||||
"form.integration.linktaco_org_slug": "Slug da organização",
|
||||
"form.integration.linktaco_tags": "Tags (máx 10, separadas por vírgula)",
|
||||
"form.integration.linktaco_tags_hint": "Máximo 10 tags, separadas por vírgula",
|
||||
"form.integration.linktaco_visibility": "Visibilidade",
|
||||
@@ -266,6 +266,7 @@
|
||||
"form.integration.linkwarden_activate": "Salvar itens no Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Chave de API do Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "URL base do Linkwarden",
|
||||
"form.integration.linkwarden_collection_id": "ID da coleção do Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Transferir novos artigos para o Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Identificação da sala Matrix",
|
||||
"form.integration.matrix_bot_password": "Palavra-passe para utilizador da Matrix",
|
||||
@@ -279,9 +280,9 @@
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (opcional)",
|
||||
"form.integration.ntfy_internal_links": "Usar links internos ao clicar (opcional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (opcional)",
|
||||
"form.integration.ntfy_topic": "Ntfy topic (default if not set in feed)",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_topic": "Tópico do Ntfy (padrão se não definido na fonte)",
|
||||
"form.integration.ntfy_url": "URL do Ntfy (opcional, padrão ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Usuário do Ntfy (opcional)",
|
||||
"form.integration.nunux_keeper_activate": "Salvar itens no Nunux Keeper",
|
||||
"form.integration.nunux_keeper_api_key": "Chave de API do Nunux Keeper",
|
||||
"form.integration.nunux_keeper_endpoint": "Endpoint de API do Nunux Keeper",
|
||||
@@ -304,8 +305,9 @@
|
||||
"form.integration.readeck_activate": "Salvar itens no Readeck",
|
||||
"form.integration.readeck_api_key": "Chave de API do Readeck",
|
||||
"form.integration.readeck_endpoint": "Endpoint de API do Readeck",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_labels": "Etiquetas do Readeck",
|
||||
"form.integration.readeck_only_url": "Enviar apenas URL (em vez de conteúdo completo)",
|
||||
"form.integration.readeck_push_activate": "Enviar automaticamente novos itens para o Readeck",
|
||||
"form.integration.readwise_activate": "Salvar itens no Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Token de acesso do Readwise Reader",
|
||||
"form.integration.readwise_api_key_link": "Obtenha seu token de acesso do Readwise",
|
||||
@@ -319,12 +321,12 @@
|
||||
"form.integration.shiori_endpoint": "Endpoint da API do Shiori",
|
||||
"form.integration.shiori_password": "Senha do Shiori",
|
||||
"form.integration.shiori_username": "Nome de usuário do Shiori",
|
||||
"form.integration.slack_activate": "Slack entries to Discord",
|
||||
"form.integration.slack_webhook_link": "Slack Webhook link",
|
||||
"form.integration.slack_activate": "Enviar itens para o Slack",
|
||||
"form.integration.slack_webhook_link": "Link do Webhook do Slack",
|
||||
"form.integration.telegram_bot_activate": "Envie novos artigos para o chat do Telegram",
|
||||
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||
"form.integration.telegram_bot_disable_notification": "Disable notification",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
|
||||
"form.integration.telegram_bot_disable_buttons": "Desativar botões",
|
||||
"form.integration.telegram_bot_disable_notification": "Desativar notificação",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Desativar pré-visualização de página",
|
||||
"form.integration.telegram_bot_token": "Token de bot",
|
||||
"form.integration.telegram_chat_id": "ID de bate-papo",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
@@ -335,10 +337,10 @@
|
||||
"form.integration.wallabag_only_url": "Enviar apenas URL (em vez de conteúdo completo)",
|
||||
"form.integration.wallabag_password": "Senha do Wallabag",
|
||||
"form.integration.wallabag_username": "Nome de usuário do Wallabag",
|
||||
"form.integration.wallabag_tags": "Wallabag Tags",
|
||||
"form.integration.webhook_activate": "Enable Webhooks",
|
||||
"form.integration.webhook_secret": "Webhooks Secret",
|
||||
"form.integration.webhook_url": "Default Webhook URL",
|
||||
"form.integration.wallabag_tags": "Etiquetas do Wallabag",
|
||||
"form.integration.webhook_activate": "Ativar Webhooks",
|
||||
"form.integration.webhook_secret": "Segredo dos Webhooks",
|
||||
"form.integration.webhook_url": "URL padrão do Webhook",
|
||||
"form.prefs.fieldset.application_settings": "Configurações do aplicativo",
|
||||
"form.prefs.fieldset.authentication_settings": "Configurações de autenticação",
|
||||
"form.prefs.fieldset.global_feed_settings": "Configurações globais de fontes",
|
||||
@@ -425,8 +427,8 @@
|
||||
"page.about.author": "Autor:",
|
||||
"page.about.build_date": "Compilado em:",
|
||||
"page.about.credits": "Créditos",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.db_usage": "Tamanho do banco de dados:",
|
||||
"page.about.git_commit": "Commit do Git:",
|
||||
"page.about.global_config_options": "opções de configuração global",
|
||||
"page.about.go_version": "Go versão:",
|
||||
"page.about.license": "Licença:",
|
||||
@@ -476,7 +478,7 @@
|
||||
"page.feeds.next_check": "Próxima verificação:",
|
||||
"page.feeds.read_counter": "Número de itens lidos",
|
||||
"page.feeds.title": "Fontes",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "Voltar ao topo",
|
||||
"page.history.title": "Histórico",
|
||||
"page.import.title": "Importar",
|
||||
"page.integration.bookmarklet": "Bookmarklet",
|
||||
@@ -629,4 +631,4 @@
|
||||
"time_elapsed.yesterday": "ontem",
|
||||
"tooltip.keyboard_shortcuts": "Atalho do teclado: %s",
|
||||
"tooltip.logged_user": "Autenticado como %s"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@
|
||||
"form.integration.linktaco_activate": "Salvează înregistrările în LinkTaco",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token_hint": "Obțineți jetonul de acces personal la",
|
||||
"form.integration.linktaco_org_slug": "Organization Slug",
|
||||
"form.integration.linktaco_org_slug": "Slug organizației",
|
||||
"form.integration.linktaco_tags": "Tag-uri (maxim 10, separate prin virgule)",
|
||||
"form.integration.linktaco_tags_hint": "Maxim 10 tag-uri, separate prin virgule",
|
||||
"form.integration.linktaco_visibility": "Vizibilitate",
|
||||
@@ -269,6 +269,7 @@
|
||||
"form.integration.linkwarden_activate": "Salvează intrările în Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Cheie API Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "URL-ul de bază Linkwarden",
|
||||
"form.integration.linkwarden_collection_id": "Linkwarden Collection ID",
|
||||
"form.integration.matrix_bot_activate": "Împinge intrările noi pe Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID-ul Camerei Matrix",
|
||||
"form.integration.matrix_bot_password": "Parola utilizatorului Matrix",
|
||||
@@ -309,11 +310,12 @@
|
||||
"form.integration.readeck_endpoint": "URL Readeck",
|
||||
"form.integration.readeck_labels": "Etichete Readeck",
|
||||
"form.integration.readeck_only_url": "Trimite numai URL (în loc de tot conținutul)",
|
||||
"form.integration.readeck_push_activate": "Trimite automat noile înregistrări în Readeck",
|
||||
"form.integration.readwise_activate": "Salvare înregistrări în Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Token Acces Readwise Reader",
|
||||
"form.integration.readwise_api_key_link": "Obțineți Token-ul de Acess pe Readwise",
|
||||
"form.integration.rssbridge_activate": "Verifică RSS-Bridge la adăugarea de abonamente",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_token": "Token de autentificare RSS-Bridge",
|
||||
"form.integration.rssbridge_url": "URL server RSS-Bridge",
|
||||
"form.integration.shaarli_activate": "Salvează articolele în Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Secret API Shaarli",
|
||||
@@ -335,7 +337,7 @@
|
||||
"form.integration.wallabag_client_id": "ID Client Wallabag",
|
||||
"form.integration.wallabag_client_secret": "Secret Client Wallabag",
|
||||
"form.integration.wallabag_endpoint": "URL Wallabag",
|
||||
"form.integration.wallabag_tags": "Wallabag Tags",
|
||||
"form.integration.wallabag_tags": "Etichete Wallabag",
|
||||
"form.integration.wallabag_only_url": "Trimite numai URL-ul (fără conținut complet)",
|
||||
"form.integration.wallabag_password": "Parolă Wallabag",
|
||||
"form.integration.wallabag_username": "Utilizator Wallabag",
|
||||
@@ -482,7 +484,7 @@
|
||||
"page.feeds.next_check": "Următoarea verificare:",
|
||||
"page.feeds.read_counter": "Numărul de intrări citite",
|
||||
"page.feeds.title": "Fluxuri",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "Înapoi sus",
|
||||
"page.history.title": "Istoric",
|
||||
"page.import.title": "Import",
|
||||
"page.integration.bookmarklet": "Marcaje",
|
||||
@@ -647,4 +649,4 @@
|
||||
"time_elapsed.yesterday": "ieri",
|
||||
"tooltip.keyboard_shortcuts": "Scurtături Tastatură: %s",
|
||||
"tooltip.logged_user": "Atentificat ca %s"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
"form.feed.label.webhook_url": "Переопределить URL вебхука",
|
||||
"form.import.label.file": "OPML файл",
|
||||
"form.import.label.url": "Ссылка",
|
||||
"form.integration.archiveorg_activate": "TОтправить статьи в archive.org",
|
||||
"form.integration.archiveorg_activate": "Отправить статьи в archive.org",
|
||||
"form.integration.apprise_activate": "Отправить статьи в Apprise",
|
||||
"form.integration.apprise_services_url": "Список ссылок сервисов Apprise, разделенный запятой",
|
||||
"form.integration.apprise_url": "Ссылка на Apprise API",
|
||||
@@ -259,7 +259,7 @@
|
||||
"form.integration.linktaco_activate": "Сохранять статьи в LinkTaco",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token_hint": "Получить ваш персональный токен доступа на",
|
||||
"form.integration.linktaco_org_slug": "Organization Slug",
|
||||
"form.integration.linktaco_org_slug": "Слаг организации",
|
||||
"form.integration.linktaco_tags": "Теги (макс. 10, через запятую)",
|
||||
"form.integration.linktaco_tags_hint": "Максимум 10 тегов, через запятую",
|
||||
"form.integration.linktaco_visibility": "Видимость",
|
||||
@@ -269,6 +269,7 @@
|
||||
"form.integration.linkwarden_activate": "Сохранять статьи в Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "API-ключ Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Базовый URL-адрес Linkwarden",
|
||||
"form.integration.linkwarden_collection_id": "Linkwarden Collection ID",
|
||||
"form.integration.matrix_bot_activate": "Отправлять статьи в Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID комнаты Matrix",
|
||||
"form.integration.matrix_bot_password": "Пароль пользователя Matrix",
|
||||
@@ -309,6 +310,7 @@
|
||||
"form.integration.readeck_endpoint": "Конечная точка Readeck API",
|
||||
"form.integration.readeck_labels": "Теги Readeck",
|
||||
"form.integration.readeck_only_url": "Отправлять только ссылку (без содержимого)",
|
||||
"form.integration.readeck_push_activate": "Автоматически отправлять новые статьи в Readeck",
|
||||
"form.integration.readwise_activate": "Сохранить статьи в Readwise",
|
||||
"form.integration.readwise_api_key": "Токен доступа в Readwise",
|
||||
"form.integration.readwise_api_key_link": "Получить токен доступа Readwise",
|
||||
@@ -335,7 +337,7 @@
|
||||
"form.integration.wallabag_client_id": "Номер клиента Wallabag",
|
||||
"form.integration.wallabag_client_secret": "Секретный код клиента Wallabag",
|
||||
"form.integration.wallabag_endpoint": "URL-адрес базы Валлабаг",
|
||||
"form.integration.wallabag_tags": "Wallabag Tags",
|
||||
"form.integration.wallabag_tags": "Теги Wallabag",
|
||||
"form.integration.wallabag_only_url": "Отправлять только ссылку (без содержимого)",
|
||||
"form.integration.wallabag_password": "Пароль Wallabag",
|
||||
"form.integration.wallabag_username": "Имя пользователя Wallabag",
|
||||
@@ -482,7 +484,7 @@
|
||||
"page.feeds.next_check": "Следующее обновление:",
|
||||
"page.feeds.read_counter": "Количество прочитанных статей",
|
||||
"page.feeds.title": "Подписки",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "Вернуться наверх",
|
||||
"page.history.title": "История",
|
||||
"page.import.title": "Импорт",
|
||||
"page.integration.bookmarklet": "Букмарклет",
|
||||
@@ -647,4 +649,4 @@
|
||||
"time_elapsed.yesterday": "вчера",
|
||||
"tooltip.keyboard_shortcuts": "Сочетания клавиш: %s",
|
||||
"tooltip.logged_user": "Авторизован как %s"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@
|
||||
"form.integration.linktaco_activate": "Makaleleri LinkTaco'ya kaydet",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token_hint": "Kişisel erişim tokenınızı edinin",
|
||||
"form.integration.linktaco_org_slug": "Organization Slug",
|
||||
"form.integration.linktaco_org_slug": "Organizasyon kısaltması",
|
||||
"form.integration.linktaco_tags": "Etiketler (maks 10, virgülle ayrılmış)",
|
||||
"form.integration.linktaco_tags_hint": "Maksimum 10 etiket, virgülle ayrılmış",
|
||||
"form.integration.linktaco_visibility": "Görünürlük",
|
||||
@@ -266,6 +266,7 @@
|
||||
"form.integration.linkwarden_activate": "Makaleleri Linkwarden'e kaydet",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API Anahtarı",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden Temel URL'si",
|
||||
"form.integration.linkwarden_collection_id": "Linkwarden Collection ID",
|
||||
"form.integration.matrix_bot_activate": "Yeni makaleleri Matrix'e aktarın",
|
||||
"form.integration.matrix_bot_chat_id": "Matrix odasının kimliği",
|
||||
"form.integration.matrix_bot_password": "Matrix kullanıcısı için parola",
|
||||
@@ -274,14 +275,14 @@
|
||||
"form.integration.notion_activate": "Makaleleri Notion'a kaydet",
|
||||
"form.integration.notion_page_id": "Notion Sayfa ID'si",
|
||||
"form.integration.notion_token": "Notion Secret Token",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.integration.ntfy_activate": "Makaleleri ntfy'ye gönder",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (isteğe bağlı)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy ikon URL'si (isteğe bağlı)",
|
||||
"form.integration.ntfy_internal_links": "Tıklamada dahili bağlantıları kullan (isteğe bağlı)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_topic": "Ntfy topic (default if not set in feed)",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy parolası (isteğe bağlı)",
|
||||
"form.integration.ntfy_topic": "Ntfy konusu (beslemede yoksa varsayılan kullanılır)",
|
||||
"form.integration.ntfy_url": "Ntfy URL'si (isteğe bağlı, varsayılan ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Ntfy kullanıcı adı (isteğe bağlı)",
|
||||
"form.integration.nunux_keeper_activate": "Makaleleri Nunux Keeper'a kaydet",
|
||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper API anahtarı",
|
||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API Uç Noktası",
|
||||
@@ -292,11 +293,11 @@
|
||||
"form.integration.pinboard_bookmark": "Yer imini okunmadı olarak işaretle",
|
||||
"form.integration.pinboard_tags": "Pinboard Etiketleri",
|
||||
"form.integration.pinboard_token": "Pinboard API Token",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
"form.integration.pushover_token": "Pushover application API token",
|
||||
"form.integration.pushover_user": "Pushover user key",
|
||||
"form.integration.pushover_activate": "Makaleleri Pushover'a gönder",
|
||||
"form.integration.pushover_device": "Pushover cihazı (isteğe bağlı)",
|
||||
"form.integration.pushover_prefix": "Pushover URL öneki (isteğe bağlı)",
|
||||
"form.integration.pushover_token": "Pushover uygulama API anahtarı",
|
||||
"form.integration.pushover_user": "Pushover kullanıcı anahtarı",
|
||||
"form.integration.raindrop_activate": "Makaleleri Raindrop'a kaydet",
|
||||
"form.integration.raindrop_collection_id": "Koleksiyon ID",
|
||||
"form.integration.raindrop_tags": "Etiketler (virgülle ayrılmış)",
|
||||
@@ -306,11 +307,12 @@
|
||||
"form.integration.readeck_endpoint": "Readeck API Uç Noktası",
|
||||
"form.integration.readeck_labels": "Readeck Etiketleri",
|
||||
"form.integration.readeck_only_url": "Yalnızca URL gönder (tam makale yerine)",
|
||||
"form.integration.readeck_push_activate": "Yeni makaleleri otomatik olarak Readeck'e gönder",
|
||||
"form.integration.readwise_activate": "Makaleleri Readwise Reader'a kaydet",
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Readwise Access Token'ınızı alın",
|
||||
"form.integration.rssbridge_activate": "Abonelik eklerken RSS-Bridge'i kontrol edin",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge kimlik doğrulama jetonu",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Makaleleri Shaarli'ye kaydet",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
@@ -335,7 +337,7 @@
|
||||
"form.integration.wallabag_only_url": "Yalnızca URL gönder (tam makale yerine)",
|
||||
"form.integration.wallabag_password": "Wallabag Parolası",
|
||||
"form.integration.wallabag_username": "Wallabag Kullanıcı Adı",
|
||||
"form.integration.wallabag_tags": "Wallabag Tags",
|
||||
"form.integration.wallabag_tags": "Wallabag etiketleri",
|
||||
"form.integration.webhook_activate": "Webhook'u etkinleştir",
|
||||
"form.integration.webhook_secret": "Webhooks Secret",
|
||||
"form.integration.webhook_url": "Default Webhook URL",
|
||||
@@ -425,7 +427,7 @@
|
||||
"page.about.author": "Yazar:",
|
||||
"page.about.build_date": "Oluşturulma Tarihi:",
|
||||
"page.about.credits": "Katkıda Bulunanlar",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.db_usage": "Veritabanı boyutu:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.global_config_options": "Global yapılandırma seçenekleri",
|
||||
"page.about.go_version": "Go sürümü:",
|
||||
@@ -476,7 +478,7 @@
|
||||
"page.feeds.next_check": "Sonraki kontrol:",
|
||||
"page.feeds.read_counter": "Okunmuş makalelerin sayısı",
|
||||
"page.feeds.title": "Beslemeler",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "Başa dön",
|
||||
"page.history.title": "Geçmiş",
|
||||
"page.import.title": "İçeri Aktar",
|
||||
"page.integration.bookmarklet": "Bookmarklet",
|
||||
@@ -629,4 +631,4 @@
|
||||
"time_elapsed.yesterday": "dün",
|
||||
"tooltip.keyboard_shortcuts": "Klavye Kısayolu: %s",
|
||||
"tooltip.logged_user": "%s olarak giriş yapıldı"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,8 +155,8 @@
|
||||
"error.unable_to_create_api_key": "Не вдається створити такий ключ API",
|
||||
"error.unable_to_create_category": "Не вдається сворити категорію.",
|
||||
"error.unable_to_create_user": "Не вдається створити користувача.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.unable_to_detect_rssbridge": "Не вдалося виявити стрічку за допомогою RSS-Bridge: %v.",
|
||||
"error.unable_to_parse_feed": "Не вдалося розібрати цю стрічку: %v.",
|
||||
"error.unable_to_update_category": "Не вдається відредагувати категорію.",
|
||||
"error.unable_to_update_feed": "Не вдається оновити стрічку.",
|
||||
"error.unable_to_update_user": "Не вдається оновити користувача.",
|
||||
@@ -198,7 +198,7 @@
|
||||
"form.feed.label.ntfy_min_priority": "Мінімальний пріоритет ntfy",
|
||||
"form.feed.label.ntfy_priority": "Пріоритет ntfy",
|
||||
"form.feed.label.ntfy_topic": "Тема ntfy (необов’язково)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.proxy_url": "URL-адреса проксі",
|
||||
"form.feed.label.pushover_activate": "Надсилати записи у pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Стандартний пріоритет Pushover",
|
||||
"form.feed.label.pushover_high_priority": "Високий пріоритет Pushover",
|
||||
@@ -218,17 +218,17 @@
|
||||
"form.integration.archiveorg_activate": "Надсилати записи у archive.org",
|
||||
"form.integration.apprise_activate": "Надсилати записи у Apprise",
|
||||
"form.integration.apprise_services_url": "Список URL сервісів Apprise, розділених комами",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.apprise_url": "URL API Apprise",
|
||||
"form.integration.betula_activate": "Зберігати записи до Betula",
|
||||
"form.integration.betula_token": "Токен Betula",
|
||||
"form.integration.betula_url": "URL сервера Betula",
|
||||
"form.integration.cubox_activate": "Зберігати статті до Cubox",
|
||||
"form.integration.cubox_api_link": "Посилання на Cubox API",
|
||||
"form.integration.discord_activate": "Push entries to Discord",
|
||||
"form.integration.discord_webhook_link": "Discord Webhook link",
|
||||
"form.integration.discord_activate": "Надсилати записи до Discord",
|
||||
"form.integration.discord_webhook_link": "Посилання на вебхук Discord",
|
||||
"form.integration.espial_activate": "Зберігати статті до Espial",
|
||||
"form.integration.espial_api_key": "Ключ API Espial",
|
||||
"form.integration.espial_endpoint": "Espial API Endpoint",
|
||||
"form.integration.espial_endpoint": "Кінцева точка API Espial",
|
||||
"form.integration.espial_tags": "Теги для Espial",
|
||||
"form.integration.fever_activate": "Увімкнути API Fever",
|
||||
"form.integration.fever_endpoint": "Адреса доступу API Fever:",
|
||||
@@ -243,19 +243,19 @@
|
||||
"form.integration.instapaper_username": "Ім’я користувача Instapaper",
|
||||
"form.integration.karakeep_activate": "Зберігати статті до Karakeep",
|
||||
"form.integration.karakeep_api_key": "Ключ API Karakeep",
|
||||
"form.integration.karakeep_url": "Karakeep API Endpoint",
|
||||
"form.integration.karakeep_tags": "Karakeep Tags",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_is_private": "Mark link as private",
|
||||
"form.integration.linkace_tags": "LinkAce Tags",
|
||||
"form.integration.karakeep_url": "Кінцева точка API Karakeep",
|
||||
"form.integration.karakeep_tags": "Теги Karakeep",
|
||||
"form.integration.linkace_activate": "Зберігати статті до LinkAce",
|
||||
"form.integration.linkace_api_key": "Ключ API LinkAce",
|
||||
"form.integration.linkace_check_disabled": "Вимкнути перевірку посилань",
|
||||
"form.integration.linkace_endpoint": "Кінцева точка API LinkAce",
|
||||
"form.integration.linkace_is_private": "Відмічати посилання як приватне",
|
||||
"form.integration.linkace_tags": "Теги LinkAce",
|
||||
"form.integration.linkding_activate": "Зберігати статті до Linkding",
|
||||
"form.integration.linkding_api_key": "Ключ API Linkding",
|
||||
"form.integration.linkding_bookmark": "Відмічати закладку як непрочитану",
|
||||
"form.integration.linkding_endpoint": "Linkding API Endpoint",
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkding_tags": "Теги Linkding",
|
||||
"form.integration.linktaco_activate": "Зберігати статті в LinkTaco",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token_hint": "Отримайте ваш персональний токен доступу на",
|
||||
@@ -269,6 +269,7 @@
|
||||
"form.integration.linkwarden_activate": "Зберігати статті до Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Ключ API Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Базова URL-адреса Linkwarden",
|
||||
"form.integration.linkwarden_collection_id": "Linkwarden Collection ID",
|
||||
"form.integration.matrix_bot_activate": "Перенесення нових статей в Матрицю",
|
||||
"form.integration.matrix_bot_chat_id": "Ідентифікатор кімнати Матриці",
|
||||
"form.integration.matrix_bot_password": "Пароль для користувача Matrix",
|
||||
@@ -309,6 +310,7 @@
|
||||
"form.integration.readeck_endpoint": "Readeck URL",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "Надіслати лише URL (замість повного вмісту)",
|
||||
"form.integration.readeck_push_activate": "Automatically push new entries to Readeck",
|
||||
"form.integration.readwise_activate": "Save entries to Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
@@ -325,27 +327,27 @@
|
||||
"form.integration.slack_activate": "Slack entries to Discord",
|
||||
"form.integration.slack_webhook_link": "Slack Webhook link",
|
||||
"form.integration.telegram_bot_activate": "Відправляти нові статті до чату Telegram",
|
||||
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||
"form.integration.telegram_bot_disable_notification": "Disable notification",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
|
||||
"form.integration.telegram_bot_disable_buttons": "Вимкнути кнопки",
|
||||
"form.integration.telegram_bot_disable_notification": "Вимкнути сповіщення",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Вимкнути попередній перегляд вебсторінок",
|
||||
"form.integration.telegram_bot_token": "Токен боту",
|
||||
"form.integration.telegram_chat_id": "ID чату",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
"form.integration.telegram_topic_id": "ID теми",
|
||||
"form.integration.wallabag_activate": "Зберігати статті до Wallabag",
|
||||
"form.integration.wallabag_client_id": "Wallabag Client ID",
|
||||
"form.integration.wallabag_client_secret": "Wallabag Client Secret",
|
||||
"form.integration.wallabag_client_id": "ID клієнта Wallabag",
|
||||
"form.integration.wallabag_client_secret": "Секрет клієнта Wallabag",
|
||||
"form.integration.wallabag_endpoint": "Базова URL-адреса Wallabag",
|
||||
"form.integration.wallabag_tags": "Wallabag Tags",
|
||||
"form.integration.wallabag_tags": "Теги Wallabag",
|
||||
"form.integration.wallabag_only_url": "Надіслати лише URL (замість повного вмісту)",
|
||||
"form.integration.wallabag_password": "Пароль Wallabag",
|
||||
"form.integration.wallabag_username": "Ім’я користувача Wallabag",
|
||||
"form.integration.webhook_activate": "Enable Webhooks",
|
||||
"form.integration.webhook_secret": "Webhooks Secret",
|
||||
"form.integration.webhook_url": "Default Webhook URL",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.integration.webhook_activate": "Увімкнути вебхуки",
|
||||
"form.integration.webhook_secret": "Секрет вебхуків",
|
||||
"form.integration.webhook_url": "URL вебхука за замовчуванням",
|
||||
"form.prefs.fieldset.application_settings": "Налаштування застосунку",
|
||||
"form.prefs.fieldset.authentication_settings": "Налаштування автентифікації",
|
||||
"form.prefs.fieldset.global_feed_settings": "Глобальні налаштування стрічок",
|
||||
"form.prefs.fieldset.reader_settings": "Налаштування читача",
|
||||
"form.prefs.help.external_font_hosts": "Список дозволених зовнішніх хостів шрифтів, розділених пробілами. Наприклад: 'fonts.gstatic.com fonts.googleapis.com'.",
|
||||
"form.prefs.label.always_open_external_links": "Читати статті, відкриваючи зовнішні посилання",
|
||||
"form.prefs.label.categories_sorting_order": "Сортування за категоріями",
|
||||
@@ -363,10 +365,10 @@
|
||||
"form.prefs.label.gesture_nav": "Жест для переходу між записами",
|
||||
"form.prefs.label.keyboard_shortcuts": "Увімкнути комбінації клавиш",
|
||||
"form.prefs.label.language": "Мова",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Позначати записи як прочитані вручну",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Позначати прочитаним лише після відтворення аудіо/відео на 90%%",
|
||||
"form.prefs.label.mark_read_on_view": "Автоматично позначати записи як прочитані під час перегляду",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Позначати прочитаним під час перегляду. Для аудіо/відео — на 90%% відтворення",
|
||||
"form.prefs.label.media_playback_rate": "Швидкість відтворення аудіо/відео",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Відкривати зовнішні посилання у новій вкладці (додає target=\"_blank\" до посилань)",
|
||||
"form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів",
|
||||
@@ -428,8 +430,8 @@
|
||||
"page.about.author": "Автор:",
|
||||
"page.about.build_date": "Дата побудови:",
|
||||
"page.about.credits": "Титри",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.db_usage": "Розмір бази даних:",
|
||||
"page.about.git_commit": "Git-коміт:",
|
||||
"page.about.global_config_options": "Параметри глобальної конфігурації",
|
||||
"page.about.go_version": "Версія Go:",
|
||||
"page.about.license": "Ліцензія:",
|
||||
@@ -437,7 +439,7 @@
|
||||
"page.about.title": "Про додадок",
|
||||
"page.about.version": "Версія:",
|
||||
"page.add_feed.choose_feed": "Обрати підписку",
|
||||
"page.add_feed.label.url": "URL",
|
||||
"page.add_feed.label.url": "URL-адреса",
|
||||
"page.add_feed.legend.advanced_options": "Розширені опції",
|
||||
"page.add_feed.no_category": "Немає категорії. Ви маєте додати принаймні одну категорію.",
|
||||
"page.add_feed.submit": "Знайти підписку",
|
||||
@@ -459,9 +461,9 @@
|
||||
"page.categories.no_feed": "Немає стрічки.",
|
||||
"page.categories.title": "Категорії",
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories",
|
||||
"%d categories"
|
||||
"%d категорія",
|
||||
"%d категорії",
|
||||
"%d категорій"
|
||||
],
|
||||
"page.category_label": "Категорія: %s",
|
||||
"page.edit_category.title": "Редагування категорії: %s",
|
||||
@@ -482,14 +484,14 @@
|
||||
"page.feeds.next_check": "Наступна перевірка:",
|
||||
"page.feeds.read_counter": "Кількість прочитаних записів",
|
||||
"page.feeds.title": "Стрічки",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "Повернутися нагору",
|
||||
"page.history.title": "Історія",
|
||||
"page.import.title": "Імпорт",
|
||||
"page.integration.bookmarklet": "Букмарклет",
|
||||
"page.integration.bookmarklet.help": "Це спеціальне посилання дозволяє підписатися на веб-сайт безпосередньо за допомогою закладки у вашому веб-браузері.",
|
||||
"page.integration.bookmarklet.instructions": "Перетягніть це посилання до своїх закладок.",
|
||||
"page.integration.bookmarklet.name": "Додати до Miniflux",
|
||||
"page.integration.miniflux_api": "Miniflux API",
|
||||
"page.integration.miniflux_api": "API Miniflux",
|
||||
"page.integration.miniflux_api_endpoint": "Адреса доступу API",
|
||||
"page.integration.miniflux_api_password": "Пароль",
|
||||
"page.integration.miniflux_api_password_value": "Пароль до вашого облікового запису",
|
||||
@@ -528,7 +530,7 @@
|
||||
"page.keyboard_shortcuts.subtitle.sections": "Навігація по розділах",
|
||||
"page.keyboard_shortcuts.title": "Комбінації клавиш",
|
||||
"page.keyboard_shortcuts.toggle_star_status": "Переключити статус закладки",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Перемкнути відкриття/закриття вкладень запису",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "Переключити статус читання, перейти до наступного",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Переключити статус читання, перейти до попереднього",
|
||||
"page.login.google_signin": "Увійти через Google",
|
||||
@@ -536,7 +538,7 @@
|
||||
"page.login.title": "Вхід",
|
||||
"page.login.webauthn_login": "Увійти за допомогою пароля",
|
||||
"page.login.webauthn_login.error": "Неможливо ввійти за допомогою ключа доступу",
|
||||
"page.login.webauthn_login.help": "Please enter your username if you're using a security key. This is not required if you are using a Passkey (discoverable credentials).",
|
||||
"page.login.webauthn_login.help": "Якщо використовуєте ключ безпеки, введіть ім'я користувача. Для паролю-паскі це не потрібно.",
|
||||
"page.new_api_key.title": "Створити ключ API",
|
||||
"page.new_category.title": "Нова категорія",
|
||||
"page.new_user.title": "Новий користувач",
|
||||
@@ -544,56 +546,56 @@
|
||||
"page.offline.refresh_page": "Спробуйте оновити сторінку",
|
||||
"page.offline.title": "Автономний режим",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries",
|
||||
"%d read entries"
|
||||
"%d прочитаний запис",
|
||||
"%d прочитаних записів",
|
||||
"%d прочитаних записів"
|
||||
],
|
||||
"page.search.title": "Результати пошуку",
|
||||
"page.sessions.table.actions": "Дії",
|
||||
"page.sessions.table.current_session": "Поточний сеанс",
|
||||
"page.sessions.table.date": "Дата",
|
||||
"page.sessions.table.ip": "IP адреса",
|
||||
"page.sessions.table.user_agent": "User Agent",
|
||||
"page.sessions.table.user_agent": "Агент користувача (User Agent)",
|
||||
"page.sessions.title": "Сеанси",
|
||||
"page.settings.link_google_account": "Підключити мій обліковий запис Google",
|
||||
"page.settings.link_oidc_account": "Підключити мій обліковий запис %s",
|
||||
"page.settings.title": "Налаштування ",
|
||||
"page.settings.unlink_google_account": "Відключити мій обліковий запис Google",
|
||||
"page.settings.unlink_oidc_account": "Відключити мій обліковий запис %s",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.added_on": "Added On",
|
||||
"page.settings.webauthn.actions": "Дії",
|
||||
"page.settings.webauthn.added_on": "Додано",
|
||||
"page.settings.webauthn.delete": [
|
||||
"Видалити %d ключ доступу",
|
||||
"Видаліть %d ключа доступу",
|
||||
"Видаліть %d ключа доступу"
|
||||
],
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.last_seen_on": "Востаннє використано",
|
||||
"page.settings.webauthn.passkey_name": "Назва паскі",
|
||||
"page.settings.webauthn.passkeys": "Паскі",
|
||||
"page.settings.webauthn.register": "Зареєструвати пароль",
|
||||
"page.settings.webauthn.register.error": "Не вдалося зареєструвати ключ доступу",
|
||||
"page.shared_entries.title": "Спільні записи",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries",
|
||||
"%d shared entries"
|
||||
"%d спільний запис",
|
||||
"%d спільні записи",
|
||||
"%d спільних записів"
|
||||
],
|
||||
"page.starred.title": "З зірочкою",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries",
|
||||
"%d starred entries"
|
||||
"%d запис із зіркою",
|
||||
"%d записи із зіркою",
|
||||
"%d записів із зіркою"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total",
|
||||
"%d entries in total"
|
||||
"Усього %d запис",
|
||||
"Усього %d записи",
|
||||
"Усього %d записів"
|
||||
],
|
||||
"page.unread.title": "Непрочитане",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries",
|
||||
"%d unread entries"
|
||||
"%d непрочитаний запис",
|
||||
"%d непрочитані записи",
|
||||
"%d непрочитаних записів"
|
||||
],
|
||||
"page.users.actions": "Дії",
|
||||
"page.users.admin.no": "Ні",
|
||||
@@ -603,7 +605,7 @@
|
||||
"page.users.never_logged": "Ніколи",
|
||||
"page.users.title": "Користувачі",
|
||||
"page.users.username": "Ім’я користувача",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"page.webauthn_rename.title": "Перейменувати паскі",
|
||||
"pagination.first": "Перша",
|
||||
"pagination.last": "Остання",
|
||||
"pagination.next": "Наступна",
|
||||
@@ -611,7 +613,7 @@
|
||||
"search.label": "Пошук",
|
||||
"search.placeholder": "Шукати...",
|
||||
"search.submit": "Знайти",
|
||||
"skip_to_content": "Skip to content",
|
||||
"skip_to_content": "Перейти до вмісту",
|
||||
"time_elapsed.days": [
|
||||
"%d день тому",
|
||||
"%d дні тому",
|
||||
@@ -647,4 +649,4 @@
|
||||
"time_elapsed.yesterday": "вчора",
|
||||
"tooltip.keyboard_shortcuts": "Комбінація клавіш: %s",
|
||||
"tooltip.logged_user": "Здійснено вхід як %s"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
"form.integration.linktaco_activate": "保存条目到 LinkTaco",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token_hint": "在此获取您的个人访问令牌",
|
||||
"form.integration.linktaco_org_slug": "Organization Slug",
|
||||
"form.integration.linktaco_org_slug": "组织代称",
|
||||
"form.integration.linktaco_tags": "标签(最多10个,逗号分隔)",
|
||||
"form.integration.linktaco_tags_hint": "最多10个标签,逗号分隔",
|
||||
"form.integration.linktaco_visibility": "可见性",
|
||||
@@ -263,6 +263,7 @@
|
||||
"form.integration.linkwarden_activate": "保存条目到 Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API 密钥",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden 基本 URL",
|
||||
"form.integration.linkwarden_collection_id": "Linkwarden 集合 ID",
|
||||
"form.integration.matrix_bot_activate": "推送新条目到 Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Matrix 房间 ID",
|
||||
"form.integration.matrix_bot_password": "Matrix 用户密码",
|
||||
@@ -303,6 +304,7 @@
|
||||
"form.integration.readeck_endpoint": "Readeck API 端点",
|
||||
"form.integration.readeck_labels": "Readeck 标签",
|
||||
"form.integration.readeck_only_url": "仅发送 URL(而非完整内容)",
|
||||
"form.integration.readeck_push_activate": "自动将新条目推送到 Readeck",
|
||||
"form.integration.readwise_activate": "保存条目到 Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader 访问令牌",
|
||||
"form.integration.readwise_api_key_link": "获取你的 Readwise 访问令牌",
|
||||
@@ -332,7 +334,7 @@
|
||||
"form.integration.wallabag_only_url": "仅发送 URL(而非完整内容)",
|
||||
"form.integration.wallabag_password": "Wallabag 密码",
|
||||
"form.integration.wallabag_username": "Wallabag 用户名",
|
||||
"form.integration.wallabag_tags": "Wallabag Tags",
|
||||
"form.integration.wallabag_tags": "Wallabag 标签",
|
||||
"form.integration.webhook_activate": "启用 Webhooks",
|
||||
"form.integration.webhook_secret": "Webhooks 密钥",
|
||||
"form.integration.webhook_url": "默认 Webhook URL",
|
||||
@@ -470,7 +472,7 @@
|
||||
"page.feeds.next_check": "下次检查:",
|
||||
"page.feeds.read_counter": "已读条目数",
|
||||
"page.feeds.title": "订阅源",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "返回顶部",
|
||||
"page.history.title": "历史记录",
|
||||
"page.import.title": "导入",
|
||||
"page.integration.bookmarklet": "书签小应用",
|
||||
|
||||
@@ -193,13 +193,13 @@
|
||||
"form.feed.label.ntfy_priority": "Ntfy 優先順序",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (選填)",
|
||||
"form.feed.label.proxy_url": "代理URL",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover消息優先級",
|
||||
"form.feed.label.pushover_activate": "推送文章到 Pushover",
|
||||
"form.feed.label.pushover_default_priority": "Pushover 預設優先順序",
|
||||
"form.feed.label.pushover_high_priority": "Pushover 高優先順序",
|
||||
"form.feed.label.pushover_low_priority": "Pushover 低優先順序",
|
||||
"form.feed.label.pushover_max_priority": "Pushover 最高優先順序",
|
||||
"form.feed.label.pushover_min_priority": "Pushover 最低優先順序",
|
||||
"form.feed.label.pushover_priority": "Pushover 訊息優先順序",
|
||||
"form.feed.label.rewrite_rules": "內容重寫規則",
|
||||
"form.feed.label.scraper_rules": "抓取規則",
|
||||
"form.feed.label.site_url": "網站網址",
|
||||
@@ -253,7 +253,7 @@
|
||||
"form.integration.linktaco_activate": "儲存文章到 LinkTaco",
|
||||
"form.integration.linktaco_api_token": "LinkTaco API Token",
|
||||
"form.integration.linktaco_api_token_hint": "在此取得您的個人存取權杖",
|
||||
"form.integration.linktaco_org_slug": "Organization Slug",
|
||||
"form.integration.linktaco_org_slug": "組織代稱",
|
||||
"form.integration.linktaco_tags": "標籤(最多10個,逗號分隔)",
|
||||
"form.integration.linktaco_tags_hint": "最多10個標籤,逗號分隔",
|
||||
"form.integration.linktaco_visibility": "可見性",
|
||||
@@ -263,6 +263,7 @@
|
||||
"form.integration.linkwarden_activate": "儲存文章到 Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API 金鑰",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden 基本 URL",
|
||||
"form.integration.linkwarden_collection_id": "Linkwarden Collection ID",
|
||||
"form.integration.matrix_bot_activate": "推送文章到 Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Matrix 房間 ID",
|
||||
"form.integration.matrix_bot_password": "Matrix 密碼",
|
||||
@@ -289,11 +290,11 @@
|
||||
"form.integration.pinboard_bookmark": "標記為未讀",
|
||||
"form.integration.pinboard_tags": "Pinboard 標籤",
|
||||
"form.integration.pinboard_token": "Pinboard API Token",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
"form.integration.pushover_token": "Pushover application API token",
|
||||
"form.integration.pushover_user": "Pushover user key",
|
||||
"form.integration.pushover_activate": "推送文章到 Pushover",
|
||||
"form.integration.pushover_device": "Pushover 裝置(選填)",
|
||||
"form.integration.pushover_prefix": "Pushover URL 前綴(選填)",
|
||||
"form.integration.pushover_token": "Pushover 應用程式 API 金鑰",
|
||||
"form.integration.pushover_user": "Pushover 使用者金鑰",
|
||||
"form.integration.raindrop_activate": "儲存文章到 Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "標籤 (以逗號分隔)",
|
||||
@@ -301,13 +302,14 @@
|
||||
"form.integration.readeck_activate": "儲存文章到 Readeck",
|
||||
"form.integration.readeck_api_key": "Readeck API 金鑰",
|
||||
"form.integration.readeck_endpoint": "Readeck API 端點",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_labels": "Readeck 標籤",
|
||||
"form.integration.readeck_only_url": "僅傳送網址(而不是完整內容)",
|
||||
"form.integration.readeck_push_activate": "自動將新文章推送到 Readeck",
|
||||
"form.integration.readwise_activate": "儲存文章到 Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader 存取金鑰",
|
||||
"form.integration.readwise_api_key_link": "取得您的 Readwise 存取金鑰",
|
||||
"form.integration.rssbridge_activate": "新增訂閱時檢查 RSS-Bridge",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge 驗證權杖",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge 伺服器的網址",
|
||||
"form.integration.shaarli_activate": "儲存文章到 Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API 金鑰",
|
||||
@@ -341,7 +343,7 @@
|
||||
"form.prefs.fieldset.global_feed_settings": "全域 Feed 設定",
|
||||
"form.prefs.fieldset.reader_settings": "閱讀器設定",
|
||||
"form.prefs.help.external_font_hosts": "以空白分隔允許的外部字型來源。例如:「fonts.gstatic.com fonts.googleapis.com」。",
|
||||
"form.prefs.label.always_open_external_links": "Read articles by opening external links",
|
||||
"form.prefs.label.always_open_external_links": "開啟外部連結閱讀文章",
|
||||
"form.prefs.label.categories_sorting_order": "分類排序",
|
||||
"form.prefs.label.cjk_reading_speed": "中文、韓文和日文的閱讀速度(每分鐘字元數)",
|
||||
"form.prefs.label.custom_css": "自訂 CSS",
|
||||
@@ -422,8 +424,8 @@
|
||||
"page.about.author": "作者:",
|
||||
"page.about.build_date": "建構日期:",
|
||||
"page.about.credits": "版權",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.db_usage": "資料庫大小:",
|
||||
"page.about.git_commit": "Git 提交:",
|
||||
"page.about.global_config_options": "全域設定選項",
|
||||
"page.about.go_version": "Go 版本:",
|
||||
"page.about.license": "授權:",
|
||||
@@ -470,7 +472,7 @@
|
||||
"page.feeds.next_check": "下次檢查時間:",
|
||||
"page.feeds.read_counter": "已讀文章數",
|
||||
"page.feeds.title": "Feeds",
|
||||
"page.footer.elevator": "Back to top",
|
||||
"page.footer.elevator": "返回頂部",
|
||||
"page.history.title": "歷史",
|
||||
"page.import.title": "匯入",
|
||||
"page.integration.bookmarklet": "書籤小工具",
|
||||
@@ -611,4 +613,4 @@
|
||||
"time_elapsed.yesterday": "昨天",
|
||||
"tooltip.keyboard_shortcuts": "快捷鍵:%s",
|
||||
"tooltip.logged_user": "目前登入 %s"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ type Integration struct {
|
||||
LinkwardenEnabled bool
|
||||
LinkwardenURL string
|
||||
LinkwardenAPIKey string
|
||||
LinkwardenCollectionID *int64
|
||||
MatrixBotEnabled bool
|
||||
MatrixBotUser string
|
||||
MatrixBotPassword string
|
||||
@@ -77,6 +78,7 @@ type Integration struct {
|
||||
AppriseURL string
|
||||
AppriseServicesURL string
|
||||
ReadeckEnabled bool
|
||||
ReadeckPushEnabled bool
|
||||
ReadeckURL string
|
||||
ReadeckAPIKey string
|
||||
ReadeckLabels string
|
||||
|
||||
@@ -156,6 +156,10 @@ func (f *iconFinder) downloadIcon(iconURL string) (*model.Icon, error) {
|
||||
slog.String("icon_url", iconURL),
|
||||
)
|
||||
|
||||
if err := ensureRemoteIconURLAllowed(iconURL, config.Opts.IconFetchAllowPrivateNetworks()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseHandler := fetcher.NewResponseHandler(f.requestBuilder.ExecuteRequest(iconURL))
|
||||
defer responseHandler.Close()
|
||||
|
||||
@@ -330,3 +334,39 @@ func parseImageDataURL(value string) (*model.Icon, error) {
|
||||
MimeType: mediaType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ensureRemoteIconURLAllowed(iconURL string, allowPrivateNetworks bool) error {
|
||||
parsedURL, err := url.Parse(iconURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("icon: invalid icon URL %q: %w", iconURL, err)
|
||||
}
|
||||
|
||||
if !parsedURL.IsAbs() {
|
||||
return fmt.Errorf("icon: icon URL %q must be absolute", iconURL)
|
||||
}
|
||||
|
||||
scheme := strings.ToLower(parsedURL.Scheme)
|
||||
if scheme != "http" && scheme != "https" {
|
||||
return fmt.Errorf("icon: unsupported icon URL scheme %q", parsedURL.Scheme)
|
||||
}
|
||||
|
||||
hostname := parsedURL.Hostname()
|
||||
if hostname == "" {
|
||||
return fmt.Errorf("icon: icon URL %q has no hostname", iconURL)
|
||||
}
|
||||
|
||||
if allowPrivateNetworks {
|
||||
return nil
|
||||
}
|
||||
|
||||
isPrivate, err := urllib.ResolvesToPrivateIP(hostname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("icon: unable to resolve icon hostname %q: %w", hostname, err)
|
||||
}
|
||||
|
||||
if isPrivate {
|
||||
return fmt.Errorf("icon: refusing to download icon from private network host %q", hostname)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -400,6 +400,24 @@ func TestResizeIconWebp(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureRemoteIconURLAllowedRejectsPrivateNetworks(t *testing.T) {
|
||||
if err := ensureRemoteIconURLAllowed("http://192.168.0.1/favicon.ico", false); err == nil {
|
||||
t.Fatal("Expected private network hosts to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureRemoteIconURLAllowedAllowsPublicNetworks(t *testing.T) {
|
||||
if err := ensureRemoteIconURLAllowed("https://1.1.1.1/favicon.ico", false); err != nil {
|
||||
t.Fatalf("Expected public network hosts to be allowed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureRemoteIconURLAllowedAllowsPrivateWhenEnabled(t *testing.T) {
|
||||
if err := ensureRemoteIconURLAllowed("http://10.0.0.5/icon.png", true); err != nil {
|
||||
t.Fatalf("Expected private network hosts to be allowed when explicitly enabled: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResizeInvalidImage(t *testing.T) {
|
||||
icon := model.Icon{
|
||||
Content: []byte("invalid data"),
|
||||
|
||||
@@ -313,13 +313,14 @@ func addYoutubeVideoFromId(entryContent string) string {
|
||||
if matches == nil {
|
||||
return entryContent
|
||||
}
|
||||
videoPlayerHTML := ""
|
||||
var videoPlayerHTML strings.Builder
|
||||
for _, match := range matches {
|
||||
if len(match) == 2 {
|
||||
videoPlayerHTML += buildVideoPlayerIframe(config.Opts.YouTubeEmbedUrlOverride()+match[1]) + "<br>"
|
||||
videoPlayerHTML.WriteString(buildVideoPlayerIframe(config.Opts.YouTubeEmbedUrlOverride() + match[1]))
|
||||
videoPlayerHTML.WriteString("<br>")
|
||||
}
|
||||
}
|
||||
return videoPlayerHTML + entryContent
|
||||
return videoPlayerHTML.String() + entryContent
|
||||
}
|
||||
|
||||
func addInvidiousVideo(entryURL, entryContent string) string {
|
||||
|
||||
@@ -1257,14 +1257,14 @@ func TestStripImageQueryParams(t *testing.T) {
|
||||
<p>Article content with images having query parameters:</p>
|
||||
<img src="https://example.org/images/image1.jpg?width=200&height=113&q=80&blur=90" alt="Image with params">
|
||||
<img src="https://example.org/images/image2.jpg?width=800&height=600&q=85" alt="Another image with params">
|
||||
|
||||
|
||||
<p>More images with various query parameters:</p>
|
||||
<img src="https://example.org/image123.jpg?blur=50&size=small&format=webp" alt="Complex query params">
|
||||
<img src="https://example.org/image123.jpg?size=large&quality=95&cache=123" alt="Different params">
|
||||
|
||||
|
||||
<p>Image without query parameters:</p>
|
||||
<img src="https://example.org/single-image.jpg" alt="Clean image">
|
||||
|
||||
|
||||
<p>Images with various other params:</p>
|
||||
<img src="https://example.org/normal1.jpg?width=300&format=jpg" alt="Normal 1">
|
||||
<img src="https://example.org/normal1.jpg?width=600&quality=high" alt="Normal 2">
|
||||
@@ -1278,14 +1278,14 @@ func TestStripImageQueryParams(t *testing.T) {
|
||||
<p>Article content with images having query parameters:</p>
|
||||
<img src="https://example.org/images/image1.jpg" alt="Image with params"/>
|
||||
<img src="https://example.org/images/image2.jpg?width=800&height=600&q=85" alt="Another image with params"/>
|
||||
|
||||
|
||||
<p>More images with various query parameters:</p>
|
||||
<img src="https://example.org/image123.jpg" alt="Complex query params"/>
|
||||
<img src="https://example.org/image123.jpg?size=large&quality=95&cache=123" alt="Different params"/>
|
||||
|
||||
|
||||
<p>Image without query parameters:</p>
|
||||
<img src="https://example.org/single-image.jpg" alt="Clean image"/>
|
||||
|
||||
|
||||
<p>Images with various other params:</p>
|
||||
<img src="https://example.org/normal1.jpg?width=300&format=jpg" alt="Normal 1"/>
|
||||
<img src="https://example.org/normal1.jpg?width=600&quality=high" alt="Normal 2"/>
|
||||
@@ -1327,17 +1327,17 @@ func TestStripImageQueryParamsEdgeCases(t *testing.T) {
|
||||
Title: `Edge Cases`,
|
||||
Content: `
|
||||
<p>Edge cases for image query parameter stripping:</p>
|
||||
|
||||
|
||||
<!-- Various query parameters -->
|
||||
<img src="https://example.org/image1.jpg?blur=80&width=300" alt="Multiple params">
|
||||
|
||||
|
||||
<!-- Complex query parameters -->
|
||||
<img src="https://example.org/image2.jpg?BLUR=60&format=webp&cache=123" alt="Complex params">
|
||||
<img src="https://example.org/image3.jpg?quality=high&version=2" alt="Other params">
|
||||
|
||||
|
||||
<!-- Query params in middle of string -->
|
||||
<img src="https://example.org/image4.jpg?size=large&blur=30&format=webp&quality=90" alt="Middle params">
|
||||
|
||||
|
||||
<!-- Image without query params -->
|
||||
<img src="https://example.org/clean.jpg" alt="Clean image">
|
||||
`,
|
||||
@@ -1347,17 +1347,17 @@ func TestStripImageQueryParamsEdgeCases(t *testing.T) {
|
||||
URL: "https://example.org/article",
|
||||
Title: `Edge Cases`,
|
||||
Content: `<p>Edge cases for image query parameter stripping:</p>
|
||||
|
||||
|
||||
<!-- Various query parameters -->
|
||||
<img src="https://example.org/image1.jpg" alt="Multiple params"/>
|
||||
|
||||
|
||||
<!-- Complex query parameters -->
|
||||
<img src="https://example.org/image2.jpg?BLUR=60&format=webp&cache=123" alt="Complex params"/>
|
||||
<img src="https://example.org/image3.jpg?quality=high&version=2" alt="Other params"/>
|
||||
|
||||
|
||||
<!-- Query params in middle of string -->
|
||||
<img src="https://example.org/image4.jpg" alt="Middle params"/>
|
||||
|
||||
|
||||
<!-- Image without query params -->
|
||||
<img src="https://example.org/clean.jpg" alt="Clean image"/>
|
||||
`,
|
||||
@@ -1375,7 +1375,7 @@ func TestStripImageQueryParamsSimple(t *testing.T) {
|
||||
Title: `Simple Test`,
|
||||
Content: `
|
||||
<p>Testing query parameter stripping:</p>
|
||||
|
||||
|
||||
<!-- Images with various query parameters -->
|
||||
<img src="https://example.org/test1.jpg?blur=0&width=300" alt="With blur zero">
|
||||
<img src="https://example.org/test2.jpg?blur=50&width=300&format=webp" alt="With blur fifty">
|
||||
@@ -1388,7 +1388,7 @@ func TestStripImageQueryParamsSimple(t *testing.T) {
|
||||
URL: "https://example.org/article",
|
||||
Title: `Simple Test`,
|
||||
Content: `<p>Testing query parameter stripping:</p>
|
||||
|
||||
|
||||
<!-- Images with various query parameters -->
|
||||
<img src="https://example.org/test1.jpg?blur=0&width=300" alt="With blur zero"/>
|
||||
<img src="https://example.org/test2.jpg" alt="With blur fifty"/>
|
||||
|
||||
@@ -53,6 +53,21 @@ func TestGetRefererForURL(t *testing.T) {
|
||||
url: "https://example.com/image.jpg",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Exact hostname match",
|
||||
url: "https://appinn.com/some/path",
|
||||
expected: "https://appinn.com",
|
||||
},
|
||||
{
|
||||
name: "Only hostnames starting with a dot should match as suffix",
|
||||
url: "https://fake-appinn.com/some/path",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Subdomain match with suffix",
|
||||
url: "https://sub.moyu.im/image.png",
|
||||
expected: "https://i.jandan.net",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -45,6 +45,7 @@ var (
|
||||
"h5": {"id"},
|
||||
"h6": {"id"},
|
||||
"hr": {},
|
||||
"i": {},
|
||||
"iframe": {"width", "height", "frameborder", "src", "allowfullscreen"},
|
||||
"img": {"alt", "title", "src", "srcset", "sizes", "width", "height", "fetchpriority", "decoding"},
|
||||
"ins": {},
|
||||
@@ -60,6 +61,7 @@ var (
|
||||
"rtc": {},
|
||||
"ruby": {},
|
||||
"s": {},
|
||||
"small": {},
|
||||
"samp": {},
|
||||
"source": {"src", "type", "srcset", "sizes", "media"},
|
||||
"strong": {},
|
||||
@@ -309,6 +311,7 @@ func sanitizeAttributes(parsedBaseUrl *url.URL, tagName string, attributes []htm
|
||||
attrNames := make([]string, 0, len(attributes))
|
||||
var err error
|
||||
var isAnchorLink bool
|
||||
var isYouTubeEmbed bool
|
||||
|
||||
for _, attribute := range attributes {
|
||||
if !isValidAttribute(tagName, attribute.Key) {
|
||||
@@ -355,10 +358,16 @@ func sanitizeAttributes(parsedBaseUrl *url.URL, tagName string, attributes []htm
|
||||
if isExternalResourceAttribute(attribute.Key) {
|
||||
switch {
|
||||
case tagName == "iframe":
|
||||
if !isValidIframeSource(attribute.Val) {
|
||||
iframeSourceDomain, trustedIframeDomain := findAllowedIframeSourceDomain(attribute.Val)
|
||||
if !trustedIframeDomain {
|
||||
continue
|
||||
}
|
||||
|
||||
value = rewriteIframeURL(attribute.Val)
|
||||
|
||||
if iframeSourceDomain == "youtube.com" || iframeSourceDomain == "youtube-nocookie.com" {
|
||||
isYouTubeEmbed = true
|
||||
}
|
||||
case tagName == "img" && attribute.Key == "src" && isValidDataAttribute(attribute.Val):
|
||||
value = attribute.Val
|
||||
case tagName == "a" && attribute.Key == "href" && strings.HasPrefix(attribute.Val, "#"):
|
||||
@@ -387,7 +396,7 @@ func sanitizeAttributes(parsedBaseUrl *url.URL, tagName string, attributes []htm
|
||||
}
|
||||
|
||||
if !isAnchorLink {
|
||||
extraAttrNames, extraHTMLAttributes := getExtraAttributes(tagName, sanitizerOptions)
|
||||
extraAttrNames, extraHTMLAttributes := getExtraAttributes(tagName, isYouTubeEmbed, sanitizerOptions)
|
||||
if len(extraAttrNames) > 0 {
|
||||
attrNames = append(attrNames, extraAttrNames...)
|
||||
htmlAttrs = append(htmlAttrs, extraHTMLAttributes...)
|
||||
@@ -397,7 +406,7 @@ func sanitizeAttributes(parsedBaseUrl *url.URL, tagName string, attributes []htm
|
||||
return attrNames, strings.Join(htmlAttrs, " ")
|
||||
}
|
||||
|
||||
func getExtraAttributes(tagName string, sanitizerOptions *SanitizerOptions) ([]string, []string) {
|
||||
func getExtraAttributes(tagName string, isYouTubeEmbed bool, sanitizerOptions *SanitizerOptions) ([]string, []string) {
|
||||
switch tagName {
|
||||
case "a":
|
||||
attributeNames := []string{"rel", "referrerpolicy"}
|
||||
@@ -410,7 +419,20 @@ func getExtraAttributes(tagName string, sanitizerOptions *SanitizerOptions) ([]s
|
||||
case "video", "audio":
|
||||
return []string{"controls"}, []string{"controls"}
|
||||
case "iframe":
|
||||
return []string{"sandbox", "loading"}, []string{`sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"`, `loading="lazy"`}
|
||||
extraAttrNames := []string{}
|
||||
extraHTMLAttributes := []string{}
|
||||
|
||||
// Note: the referrerpolicy seems to be required to avoid YouTube error 153 video player configuration error
|
||||
// See https://developers.google.com/youtube/terms/required-minimum-functionality#embedded-player-api-client-identity
|
||||
if isYouTubeEmbed {
|
||||
extraAttrNames = append(extraAttrNames, "referrerpolicy")
|
||||
extraHTMLAttributes = append(extraHTMLAttributes, `referrerpolicy="strict-origin-when-cross-origin"`)
|
||||
}
|
||||
|
||||
extraAttrNames = append(extraAttrNames, "sandbox", "loading")
|
||||
extraHTMLAttributes = append(extraHTMLAttributes, `sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"`, `loading="lazy"`)
|
||||
|
||||
return extraAttrNames, extraHTMLAttributes
|
||||
case "img":
|
||||
return []string{"loading"}, []string{`loading="lazy"`}
|
||||
default:
|
||||
@@ -496,22 +518,22 @@ func isBlockedResource(absoluteURL string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func isValidIframeSource(iframeSourceURL string) bool {
|
||||
func findAllowedIframeSourceDomain(iframeSourceURL string) (string, bool) {
|
||||
iframeSourceDomain := urllib.DomainWithoutWWW(iframeSourceURL)
|
||||
|
||||
if _, ok := iframeAllowList[iframeSourceDomain]; ok {
|
||||
return true
|
||||
return iframeSourceDomain, true
|
||||
}
|
||||
|
||||
if ytDomain := config.Opts.YouTubeEmbedDomain(); ytDomain != "" && iframeSourceDomain == strings.TrimPrefix(ytDomain, "www.") {
|
||||
return true
|
||||
return iframeSourceDomain, true
|
||||
}
|
||||
|
||||
if invidiousInstance := config.Opts.InvidiousInstance(); invidiousInstance != "" && iframeSourceDomain == strings.TrimPrefix(invidiousInstance, "www.") {
|
||||
return true
|
||||
return iframeSourceDomain, true
|
||||
}
|
||||
|
||||
return false
|
||||
return "", false
|
||||
}
|
||||
|
||||
func rewriteIframeURL(link string) string {
|
||||
|
||||
@@ -449,7 +449,7 @@ func TestIFrameWithChildElements(t *testing.T) {
|
||||
config.Opts = config.NewConfigOptions()
|
||||
|
||||
input := `<iframe src="https://www.youtube.com/"><p>test</p></iframe>`
|
||||
expected := `<iframe src="https://www.youtube.com/" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube.com/" referrerpolicy="strict-origin-when-cross-origin" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
output := sanitizeHTMLWithDefaultOptions("http://example.com/", input)
|
||||
|
||||
if expected != output {
|
||||
@@ -457,6 +457,18 @@ func TestIFrameWithChildElements(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIFrameWithReferrerPolicy(t *testing.T) {
|
||||
config.Opts = config.NewConfigOptions()
|
||||
|
||||
input := `<iframe src="https://www.youtube.com/embed/test123" referrerpolicy="strict-origin-when-cross-origin"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123" referrerpolicy="strict-origin-when-cross-origin" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
output := sanitizeHTMLWithDefaultOptions("http://example.com/", input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Wrong output: %q != %q`, expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkWithTarget(t *testing.T) {
|
||||
input := `<p>This link is <a href="http://example.org/index.html">an anchor</a></p>`
|
||||
expected := `<p>This link is <a href="http://example.org/index.html" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">an anchor</a></p>`
|
||||
@@ -796,8 +808,16 @@ func TestEspaceAttributes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReplaceYoutubeURL(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
var err error
|
||||
config.Opts, err = config.NewConfigParser().ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
input := `<iframe src="http://www.youtube.com/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent" referrerpolicy="strict-origin-when-cross-origin" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
output := sanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||
|
||||
if expected != output {
|
||||
@@ -806,8 +826,16 @@ func TestReplaceYoutubeURL(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReplaceSecureYoutubeURL(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
var err error
|
||||
config.Opts, err = config.NewConfigParser().ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
input := `<iframe src="https://www.youtube.com/embed/test123"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123" referrerpolicy="strict-origin-when-cross-origin" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
output := sanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||
|
||||
if expected != output {
|
||||
@@ -816,8 +844,16 @@ func TestReplaceSecureYoutubeURL(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReplaceSecureYoutubeURLWithParameters(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
var err error
|
||||
config.Opts, err = config.NewConfigParser().ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
input := `<iframe src="https://www.youtube.com/embed/test123?rel=0&controls=0"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&controls=0" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&controls=0" referrerpolicy="strict-origin-when-cross-origin" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
output := sanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||
|
||||
if expected != output {
|
||||
@@ -826,8 +862,16 @@ func TestReplaceSecureYoutubeURLWithParameters(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReplaceYoutubeURLAlreadyReplaced(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
var err error
|
||||
config.Opts, err = config.NewConfigParser().ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
input := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&controls=0" sandbox="allow-scripts allow-same-origin"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&controls=0" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&controls=0" referrerpolicy="strict-origin-when-cross-origin" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
output := sanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||
|
||||
if expected != output {
|
||||
@@ -836,8 +880,16 @@ func TestReplaceYoutubeURLAlreadyReplaced(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReplaceProtocolRelativeYoutubeURL(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
var err error
|
||||
config.Opts, err = config.NewConfigParser().ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
input := `<iframe src="//www.youtube.com/embed/Bf2W84jrGqs" width="560" height="314" allowfullscreen="allowfullscreen"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/Bf2W84jrGqs" width="560" height="314" allowfullscreen="allowfullscreen" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/Bf2W84jrGqs" width="560" height="314" allowfullscreen="allowfullscreen" referrerpolicy="strict-origin-when-cross-origin" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
output := sanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||
|
||||
if expected != output {
|
||||
@@ -856,7 +908,7 @@ func TestReplaceYoutubeURLWithCustomURL(t *testing.T) {
|
||||
}
|
||||
|
||||
input := `<iframe src="https://www.youtube.com/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent"></iframe>`
|
||||
expected := `<iframe src="https://invidious.custom/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
expected := `<iframe src="https://invidious.custom/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent" referrerpolicy="strict-origin-when-cross-origin" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
output := sanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||
|
||||
if expected != output {
|
||||
|
||||
@@ -69,7 +69,11 @@ func (f *subscriptionFinder) FindSubscriptions(websiteURL, rssBridgeURL string,
|
||||
return Subscriptions{NewSubscription(responseHandler.EffectiveURL(), responseHandler.EffectiveURL(), feedFormat)}, nil
|
||||
}
|
||||
|
||||
// Step 2) Check if the website URL is a YouTube channel.
|
||||
// Step 2) Find the canonical URL of the website.
|
||||
slog.Debug("Try to find the canonical URL of the website", slog.String("website_url", websiteURL))
|
||||
websiteURL = f.findCanonicalURL(websiteURL, responseHandler.ContentType(), bytes.NewReader(responseBody))
|
||||
|
||||
// Step 3) Check if the website URL is a YouTube channel.
|
||||
slog.Debug("Try to detect feeds for a YouTube page", slog.String("website_url", websiteURL))
|
||||
if subscriptions, localizedError := f.findSubscriptionsFromYouTube(websiteURL); localizedError != nil {
|
||||
return nil, localizedError
|
||||
@@ -78,7 +82,7 @@ func (f *subscriptionFinder) FindSubscriptions(websiteURL, rssBridgeURL string,
|
||||
return subscriptions, nil
|
||||
}
|
||||
|
||||
// Step 3) Parse web page to find feeds from HTML meta tags.
|
||||
// Step 4) Parse web page to find feeds from HTML meta tags.
|
||||
slog.Debug("Try to detect feeds from HTML meta tags",
|
||||
slog.String("website_url", websiteURL),
|
||||
slog.String("content_type", responseHandler.ContentType()),
|
||||
@@ -90,7 +94,7 @@ func (f *subscriptionFinder) FindSubscriptions(websiteURL, rssBridgeURL string,
|
||||
return subscriptions, nil
|
||||
}
|
||||
|
||||
// Step 4) Check if the website URL can use RSS-Bridge.
|
||||
// Step 5) Check if the website URL can use RSS-Bridge.
|
||||
if rssBridgeURL != "" {
|
||||
slog.Debug("Try to detect feeds with RSS-Bridge", slog.String("website_url", websiteURL))
|
||||
if subscriptions, localizedError := f.findSubscriptionsFromRSSBridge(websiteURL, rssBridgeURL, rssBridgeToken); localizedError != nil {
|
||||
@@ -101,7 +105,7 @@ func (f *subscriptionFinder) FindSubscriptions(websiteURL, rssBridgeURL string,
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5) Check if the website has a known feed URL.
|
||||
// Step 6) Check if the website has a known feed URL.
|
||||
slog.Debug("Try to detect feeds from well-known URLs", slog.String("website_url", websiteURL))
|
||||
if subscriptions, localizedError := f.findSubscriptionsFromWellKnownURLs(websiteURL); localizedError != nil {
|
||||
return nil, localizedError
|
||||
@@ -274,6 +278,15 @@ func (f *subscriptionFinder) findSubscriptionsFromRSSBridge(websiteURL, rssBridg
|
||||
}
|
||||
|
||||
func (f *subscriptionFinder) findSubscriptionsFromYouTube(websiteURL string) (Subscriptions, *locale.LocalizedErrorWrapper) {
|
||||
playlistPrefixes := []struct {
|
||||
prefix string
|
||||
title string
|
||||
}{
|
||||
{"UULF", "Videos"},
|
||||
{"UUSH", "Short videos"},
|
||||
{"UULV", "Live streams"},
|
||||
}
|
||||
|
||||
decodedURL, err := url.Parse(websiteURL)
|
||||
if err != nil {
|
||||
return nil, locale.NewLocalizedErrorWrapper(err, "error.invalid_site_url", err)
|
||||
@@ -283,9 +296,19 @@ func (f *subscriptionFinder) findSubscriptionsFromYouTube(websiteURL string) (Su
|
||||
slog.Debug("YouTube feed discovery skipped: not a YouTube domain", slog.String("website_url", websiteURL))
|
||||
return nil, nil
|
||||
}
|
||||
if _, channelID, found := strings.Cut(decodedURL.Path, "channel/"); found {
|
||||
feedURL := "https://www.youtube.com/feeds/videos.xml?channel_id=" + channelID
|
||||
return Subscriptions{NewSubscription(decodedURL.String(), feedURL, parser.FormatAtom)}, nil
|
||||
|
||||
if _, baseID, found := strings.Cut(decodedURL.Path, "channel/UC"); found {
|
||||
var subscriptions Subscriptions
|
||||
|
||||
channelFeedURL := "https://www.youtube.com/feeds/videos.xml?channel_id=UC" + baseID
|
||||
subscriptions = append(subscriptions, NewSubscription("Channel", channelFeedURL, parser.FormatAtom))
|
||||
|
||||
for _, playlist := range playlistPrefixes {
|
||||
playlistFeedURL := "https://www.youtube.com/feeds/videos.xml?playlist_id=" + playlist.prefix + baseID
|
||||
subscriptions = append(subscriptions, NewSubscription(playlist.title, playlistFeedURL, parser.FormatAtom))
|
||||
}
|
||||
|
||||
return subscriptions, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(decodedURL.Path, "/watch") || strings.HasPrefix(decodedURL.Path, "/playlist") {
|
||||
@@ -297,3 +320,37 @@ func (f *subscriptionFinder) findSubscriptionsFromYouTube(websiteURL string) (Su
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// findCanonicalURL extracts the canonical URL from the HTML <link rel="canonical"> tag.
|
||||
// Returns the canonical URL if found, otherwise returns the effective URL.
|
||||
func (f *subscriptionFinder) findCanonicalURL(effectiveURL, contentType string, body io.Reader) string {
|
||||
htmlDocumentReader, err := encoding.NewCharsetReader(body, contentType)
|
||||
if err != nil {
|
||||
return effectiveURL
|
||||
}
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(htmlDocumentReader)
|
||||
if err != nil {
|
||||
return effectiveURL
|
||||
}
|
||||
|
||||
baseURL := effectiveURL
|
||||
if hrefValue, exists := doc.FindMatcher(goquery.Single("head base")).Attr("href"); exists {
|
||||
hrefValue = strings.TrimSpace(hrefValue)
|
||||
if urllib.IsAbsoluteURL(hrefValue) {
|
||||
baseURL = hrefValue
|
||||
}
|
||||
}
|
||||
|
||||
canonicalHref, exists := doc.Find("link[rel='canonical' i]").First().Attr("href")
|
||||
if !exists || strings.TrimSpace(canonicalHref) == "" {
|
||||
return effectiveURL
|
||||
}
|
||||
|
||||
canonicalURL, err := urllib.AbsoluteURL(baseURL, strings.TrimSpace(canonicalHref))
|
||||
if err != nil {
|
||||
return effectiveURL
|
||||
}
|
||||
|
||||
return canonicalURL
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
func TestFindYoutubeFeed(t *testing.T) {
|
||||
type testResult struct {
|
||||
websiteURL string
|
||||
feedURL string
|
||||
feedURLs []string
|
||||
discoveryError bool
|
||||
}
|
||||
|
||||
@@ -19,57 +19,62 @@ func TestFindYoutubeFeed(t *testing.T) {
|
||||
// Video URL
|
||||
{
|
||||
websiteURL: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
feedURL: "",
|
||||
feedURLs: []string{},
|
||||
},
|
||||
// Video URL with position argument
|
||||
{
|
||||
websiteURL: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=1",
|
||||
feedURL: "",
|
||||
feedURLs: []string{},
|
||||
},
|
||||
// Video URL with position argument
|
||||
{
|
||||
websiteURL: "https://www.youtube.com/watch?t=1&v=dQw4w9WgXcQ",
|
||||
feedURL: "",
|
||||
feedURLs: []string{},
|
||||
},
|
||||
// Channel URL
|
||||
{
|
||||
websiteURL: "https://www.youtube.com/channel/UC-Qj80avWItNRjkZ41rzHyw",
|
||||
feedURL: "https://www.youtube.com/feeds/videos.xml?channel_id=UC-Qj80avWItNRjkZ41rzHyw",
|
||||
feedURLs: []string{
|
||||
"https://www.youtube.com/feeds/videos.xml?channel_id=UC-Qj80avWItNRjkZ41rzHyw",
|
||||
"https://www.youtube.com/feeds/videos.xml?playlist_id=UULF-Qj80avWItNRjkZ41rzHyw",
|
||||
"https://www.youtube.com/feeds/videos.xml?playlist_id=UUSH-Qj80avWItNRjkZ41rzHyw",
|
||||
"https://www.youtube.com/feeds/videos.xml?playlist_id=UULV-Qj80avWItNRjkZ41rzHyw",
|
||||
},
|
||||
},
|
||||
// Channel URL with name
|
||||
{
|
||||
websiteURL: "https://www.youtube.com/@ABCDEFG",
|
||||
feedURL: "",
|
||||
feedURLs: []string{},
|
||||
},
|
||||
// Playlist URL
|
||||
{
|
||||
websiteURL: "https://www.youtube.com/playlist?list=PLOOwEPgFWm_NHcQd9aCi5JXWASHO_n5uR",
|
||||
feedURL: "https://www.youtube.com/feeds/videos.xml?playlist_id=PLOOwEPgFWm_NHcQd9aCi5JXWASHO_n5uR",
|
||||
feedURLs: []string{"https://www.youtube.com/feeds/videos.xml?playlist_id=PLOOwEPgFWm_NHcQd9aCi5JXWASHO_n5uR"},
|
||||
},
|
||||
// Playlist URL with video ID
|
||||
{
|
||||
websiteURL: "https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLOOwEPgFWm_N42HlCLhqyJ0ZBWr5K1QDM",
|
||||
feedURL: "https://www.youtube.com/feeds/videos.xml?playlist_id=PLOOwEPgFWm_N42HlCLhqyJ0ZBWr5K1QDM",
|
||||
feedURLs: []string{"https://www.youtube.com/feeds/videos.xml?playlist_id=PLOOwEPgFWm_N42HlCLhqyJ0ZBWr5K1QDM"},
|
||||
},
|
||||
// Playlist URL with video ID and index argument
|
||||
{
|
||||
websiteURL: "https://www.youtube.com/watch?v=6IutBmRJNLk&list=PLOOwEPgFWm_NHcQd9aCi5JXWASHO_n5uR&index=4",
|
||||
feedURL: "https://www.youtube.com/feeds/videos.xml?playlist_id=PLOOwEPgFWm_NHcQd9aCi5JXWASHO_n5uR",
|
||||
feedURLs: []string{"https://www.youtube.com/feeds/videos.xml?playlist_id=PLOOwEPgFWm_NHcQd9aCi5JXWASHO_n5uR"},
|
||||
},
|
||||
// Empty playlist ID parameter
|
||||
{
|
||||
websiteURL: "https://www.youtube.com/playlist?list=",
|
||||
feedURL: "",
|
||||
feedURLs: []string{},
|
||||
},
|
||||
// Non-Youtube URL
|
||||
{
|
||||
websiteURL: "https://www.example.com/channel/UC-Qj80avWItNRjkZ41rzHyw",
|
||||
feedURL: "",
|
||||
feedURLs: []string{},
|
||||
},
|
||||
// Invalid URL
|
||||
{
|
||||
websiteURL: "https://example|org/",
|
||||
feedURL: "",
|
||||
feedURLs: []string{},
|
||||
discoveryError: true,
|
||||
},
|
||||
}
|
||||
@@ -82,7 +87,7 @@ func TestFindYoutubeFeed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if scenario.feedURL == "" {
|
||||
if len(scenario.feedURLs) == 0 {
|
||||
if len(subscriptions) > 0 {
|
||||
t.Fatalf(`Parsing an invalid URL should not return any subscription: %q -> %v`, scenario.websiteURL, subscriptions)
|
||||
}
|
||||
@@ -91,12 +96,14 @@ func TestFindYoutubeFeed(t *testing.T) {
|
||||
t.Fatalf(`Parsing a correctly formatted YouTube playlist or channel page should not return any error: %v`, localizedError)
|
||||
}
|
||||
|
||||
if len(subscriptions) != 1 {
|
||||
t.Fatalf(`Incorrect number of subscriptions returned`)
|
||||
if len(subscriptions) != len(scenario.feedURLs) {
|
||||
t.Fatalf(`Incorrect number of subscriptions returned, expected %d, got %d`, len(scenario.feedURLs), len(subscriptions))
|
||||
}
|
||||
|
||||
if subscriptions[0].URL != scenario.feedURL {
|
||||
t.Errorf(`Unexpected feed, got %s, instead of %s`, subscriptions[0].URL, scenario.feedURL)
|
||||
for i := range scenario.feedURLs {
|
||||
if subscriptions[i].URL != scenario.feedURLs[i] {
|
||||
t.Errorf(`Unexpected feed, got %s, instead of %s`, subscriptions[i].URL, scenario.feedURLs[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,3 +404,36 @@ func TestParseWebPageWithNoHref(t *testing.T) {
|
||||
t.Fatal(`Incorrect number of subscriptions returned`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindCanonicalURL(t *testing.T) {
|
||||
htmlPage := `
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="canonical" href="https://example.org/canonical-page">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
canonicalURL := NewSubscriptionFinder(nil).findCanonicalURL("https://example.org/page", "text/html", strings.NewReader(htmlPage))
|
||||
if canonicalURL != "https://example.org/canonical-page" {
|
||||
t.Errorf(`Unexpected canonical URL, got %q, expected %q`, canonicalURL, "https://example.org/canonical-page")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindCanonicalURLNotFound(t *testing.T) {
|
||||
htmlPage := `
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
canonicalURL := NewSubscriptionFinder(nil).findCanonicalURL("https://example.org/page", "text/html", strings.NewReader(htmlPage))
|
||||
if canonicalURL != "https://example.org/page" {
|
||||
t.Errorf(`Expected effective URL when canonical not found, got %q`, canonicalURL)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,16 @@ var trackingParams = map[string]bool{
|
||||
"fb_source": true,
|
||||
"fb_comment_id": true,
|
||||
|
||||
// Humble Bundles
|
||||
"hmb_campaign": true,
|
||||
"hmb_medium": true,
|
||||
"hmb_source": true,
|
||||
|
||||
// Likely Google as well
|
||||
"itm_campaign": true,
|
||||
"itm_medium": true,
|
||||
"itm_source": true,
|
||||
|
||||
// Google Click Identifiers
|
||||
"gclid": true,
|
||||
"dclid": true,
|
||||
@@ -40,6 +50,9 @@ var trackingParams = map[string]bool{
|
||||
"campaign_term": true,
|
||||
"campaign_content": true,
|
||||
|
||||
// Google
|
||||
"srsltid": true,
|
||||
|
||||
// Yandex Click Identifiers
|
||||
"yclid": true,
|
||||
"ysclid": true,
|
||||
@@ -53,6 +66,7 @@ var trackingParams = map[string]bool{
|
||||
// Mailchimp Click Identifiers
|
||||
"mc_cid": true,
|
||||
"mc_eid": true,
|
||||
"mc_tc": true,
|
||||
|
||||
// Wicked Reports click tracking
|
||||
"wickedid": true,
|
||||
@@ -87,6 +101,9 @@ var trackingParams = map[string]bool{
|
||||
// Branch.io
|
||||
"_branch_match_id": true,
|
||||
"_branch_referrer": true,
|
||||
|
||||
// Readwise
|
||||
"__readwiseLocation": true,
|
||||
}
|
||||
|
||||
// Outbound tracking parameters are appending the website's url to outbound links.
|
||||
@@ -95,6 +112,20 @@ var trackingParamsOutbound = map[string]bool{
|
||||
"ref": true,
|
||||
}
|
||||
|
||||
var trackingParamsPrefixes = []string{
|
||||
"utm_", // https://en.wikipedia.org/wiki/UTM_parameters
|
||||
"mtm_", // https://matomo.org/faq/reports/common-campaign-tracking-use-cases-and-examples/
|
||||
}
|
||||
|
||||
func isTrackingParam(param string) bool {
|
||||
for _, prefix := range trackingParamsPrefixes {
|
||||
if strings.HasPrefix(param, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return trackingParams[param]
|
||||
}
|
||||
|
||||
func RemoveTrackingParameters(parsedFeedURL, parsedSiteURL, parsedInputUrl *url.URL) (string, error) {
|
||||
if parsedFeedURL == nil || parsedSiteURL == nil || parsedInputUrl == nil {
|
||||
return "", errors.New("urlcleaner: one of the URLs is nil")
|
||||
@@ -106,7 +137,7 @@ func RemoveTrackingParameters(parsedFeedURL, parsedSiteURL, parsedInputUrl *url.
|
||||
// Remove tracking parameters
|
||||
for param := range queryParams {
|
||||
lowerParam := strings.ToLower(param)
|
||||
if trackingParams[lowerParam] || strings.HasPrefix(lowerParam, "utm_") {
|
||||
if isTrackingParam(lowerParam) {
|
||||
queryParams.Del(param)
|
||||
hasTrackers = true
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ func TestRemoveTrackingParams(t *testing.T) {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
baseUrl string
|
||||
feedUrl string
|
||||
baseURL string
|
||||
feedURL string
|
||||
strictComparison bool
|
||||
}{
|
||||
{
|
||||
@@ -67,62 +67,69 @@ func TestRemoveTrackingParams(t *testing.T) {
|
||||
{
|
||||
name: "ref parameter for another url",
|
||||
input: "https://example.com/page?ref=test.com",
|
||||
baseUrl: "https://example.com/page",
|
||||
baseURL: "https://example.com/page",
|
||||
expected: "https://example.com/page?ref=test.com",
|
||||
},
|
||||
{
|
||||
name: "ref parameter for feed url",
|
||||
input: "https://example.com/page?ref=feed.com",
|
||||
baseUrl: "https://example.com/page",
|
||||
baseURL: "https://example.com/page",
|
||||
expected: "https://example.com/page",
|
||||
feedUrl: "http://feed.com",
|
||||
feedURL: "http://feed.com",
|
||||
},
|
||||
{
|
||||
name: "ref parameter for site url",
|
||||
input: "https://example.com/page?ref=example.com",
|
||||
baseUrl: "https://example.com/page",
|
||||
baseURL: "https://example.com/page",
|
||||
expected: "https://example.com/page",
|
||||
},
|
||||
{
|
||||
name: "ref parameter for base url",
|
||||
input: "https://example.com/page?ref=example.com",
|
||||
expected: "https://example.com/page",
|
||||
baseUrl: "https://example.com",
|
||||
feedUrl: "https://feedburned.com/example",
|
||||
baseURL: "https://example.com",
|
||||
feedURL: "https://feedburned.com/example",
|
||||
},
|
||||
{
|
||||
name: "ref parameter for base url on subdomain",
|
||||
input: "https://blog.exploits.club/some-path?ref=blog.exploits.club",
|
||||
expected: "https://blog.exploits.club/some-path",
|
||||
baseUrl: "https://blog.exploits.club/some-path",
|
||||
feedUrl: "https://feedburned.com/exploit.club",
|
||||
baseURL: "https://blog.exploits.club/some-path",
|
||||
feedURL: "https://feedburned.com/exploit.club",
|
||||
},
|
||||
{
|
||||
name: "Non-standard URL parameter with no tracker",
|
||||
input: "https://example.com/foo.jpg?crop/1420x708/format/webp",
|
||||
expected: "https://example.com/foo.jpg?crop/1420x708/format/webp",
|
||||
baseUrl: "https://example.com/page",
|
||||
baseURL: "https://example.com/page",
|
||||
strictComparison: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid URL",
|
||||
input: "https://example|org/",
|
||||
baseUrl: "https://example.com/page",
|
||||
baseURL: "https://example.com/page",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Non-HTTP URL",
|
||||
input: "mailto:user@example.org",
|
||||
expected: "mailto:user@example.org",
|
||||
baseUrl: "https://example.com/page",
|
||||
baseURL: "https://example.com/page",
|
||||
strictComparison: true,
|
||||
},
|
||||
{
|
||||
name: "Matomo tracking URL",
|
||||
input: "https://example.com/?mtm_campaign=2020_august_promo&mtm_source=newsletter&mtm_medium=email&mtm_content=primary-cta",
|
||||
expected: "https://example.com/",
|
||||
baseURL: "https://example.com",
|
||||
strictComparison: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parsedBaseUrl, _ := url.Parse(tt.baseUrl)
|
||||
parsedFeedUrl, _ := url.Parse(tt.feedUrl)
|
||||
parsedBaseUrl, _ := url.Parse(tt.baseURL)
|
||||
parsedFeedUrl, _ := url.Parse(tt.feedURL)
|
||||
parsedInputUrl, _ := url.Parse(tt.input)
|
||||
result, err := RemoveTrackingParameters(parsedBaseUrl, parsedFeedUrl, parsedInputUrl)
|
||||
if tt.expected == "" {
|
||||
|
||||
@@ -236,6 +236,58 @@ func (s *Storage) entryExists(tx *sql.Tx, entry *model.Entry) (bool, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Storage) getEntryIDByHash(tx *sql.Tx, feedID int64, entryHash string) (int64, error) {
|
||||
var entryID int64
|
||||
|
||||
err := tx.QueryRow(
|
||||
`SELECT id FROM entries WHERE feed_id=$1 AND hash=$2 LIMIT 1`,
|
||||
feedID,
|
||||
entryHash,
|
||||
).Scan(&entryID)
|
||||
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf(`store: unable to fetch entry ID: %v`, err)
|
||||
}
|
||||
|
||||
return entryID, nil
|
||||
}
|
||||
|
||||
// InsertEntryForFeed inserts a single entry into a feed, optionally updating if it already exists.
|
||||
// Returns true if a new entry was created, false if an existing one was reused.
|
||||
func (s *Storage) InsertEntryForFeed(userID, feedID int64, entry *model.Entry) (bool, error) {
|
||||
entry.UserID = userID
|
||||
entry.FeedID = feedID
|
||||
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("store: unable to start transaction: %v", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
exists, err := s.entryExists(tx, entry)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if exists {
|
||||
entryID, err := s.getEntryIDByHash(tx, entry.FeedID, entry.Hash)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
entry.ID = entryID
|
||||
} else {
|
||||
if err := s.createEntry(tx, entry); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return !exists, nil
|
||||
}
|
||||
|
||||
func (s *Storage) IsNewEntry(feedID int64, entryHash string) bool {
|
||||
var result bool
|
||||
s.db.QueryRow(`SELECT true FROM entries WHERE feed_id=$1 AND hash=$2 LIMIT 1`, feedID, entryHash).Scan(&result)
|
||||
|
||||
@@ -164,6 +164,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
|
||||
linkwarden_enabled,
|
||||
linkwarden_url,
|
||||
linkwarden_api_key,
|
||||
linkwarden_collection_id,
|
||||
matrix_bot_enabled,
|
||||
matrix_bot_user,
|
||||
matrix_bot_password,
|
||||
@@ -177,6 +178,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
|
||||
readeck_api_key,
|
||||
readeck_labels,
|
||||
readeck_only_url,
|
||||
readeck_push_enabled,
|
||||
shiori_enabled,
|
||||
shiori_url,
|
||||
shiori_username,
|
||||
@@ -291,6 +293,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
|
||||
&integration.LinkwardenEnabled,
|
||||
&integration.LinkwardenURL,
|
||||
&integration.LinkwardenAPIKey,
|
||||
&integration.LinkwardenCollectionID,
|
||||
&integration.MatrixBotEnabled,
|
||||
&integration.MatrixBotUser,
|
||||
&integration.MatrixBotPassword,
|
||||
@@ -304,6 +307,7 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
|
||||
&integration.ReadeckAPIKey,
|
||||
&integration.ReadeckLabels,
|
||||
&integration.ReadeckOnlyURL,
|
||||
&integration.ReadeckPushEnabled,
|
||||
&integration.ShioriEnabled,
|
||||
&integration.ShioriURL,
|
||||
&integration.ShioriUsername,
|
||||
@@ -491,9 +495,11 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
|
||||
linktaco_org_slug=$116,
|
||||
linktaco_tags=$117,
|
||||
linktaco_visibility=$118,
|
||||
archiveorg_enabled=$119
|
||||
archiveorg_enabled=$119,
|
||||
linkwarden_collection_id=$120,
|
||||
readeck_push_enabled=$121
|
||||
WHERE
|
||||
user_id=$120
|
||||
user_id=$122
|
||||
`
|
||||
_, err := s.db.Exec(
|
||||
query,
|
||||
@@ -616,6 +622,8 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
|
||||
integration.LinktacoTags,
|
||||
integration.LinktacoVisibility,
|
||||
integration.ArchiveorgEnabled,
|
||||
integration.LinkwardenCollectionID,
|
||||
integration.ReadeckPushEnabled,
|
||||
integration.UserID,
|
||||
)
|
||||
|
||||
|
||||
+12
-11
@@ -53,36 +53,37 @@ func (s *Storage) createAppSession(session *model.Session) (*model.Session, erro
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// UpdateAppSessionField updates only one session field.
|
||||
func (s *Storage) UpdateAppSessionField(sessionID, field string, value any) error {
|
||||
// SetAppSessionTextField sets a text field in the session data.
|
||||
func (s *Storage) SetAppSessionTextField(sessionID, field string, value any) error {
|
||||
query := `
|
||||
UPDATE
|
||||
sessions
|
||||
SET
|
||||
data = jsonb_set(data, '{%s}', to_jsonb($1::text), true)
|
||||
data = jsonb_set(data, ARRAY[$2::text], to_jsonb($1::text), true)
|
||||
WHERE
|
||||
id=$2
|
||||
id=$3
|
||||
`
|
||||
_, err := s.db.Exec(fmt.Sprintf(query, field), value, sessionID)
|
||||
_, err := s.db.Exec(query, value, field, sessionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`store: unable to update session field: %v`, err)
|
||||
return fmt.Errorf(`store: unable to update session text field %q: %v`, field, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) UpdateAppSessionObjectField(sessionID, field string, value any) error {
|
||||
// SetAppSessionJSONField sets a JSON field in the session data.
|
||||
func (s *Storage) SetAppSessionJSONField(sessionID, field string, value any) error {
|
||||
query := `
|
||||
UPDATE
|
||||
sessions
|
||||
SET
|
||||
data = jsonb_set(data, '{%s}', $1, true)
|
||||
data = jsonb_set(data, ARRAY[$2::text], $1, true)
|
||||
WHERE
|
||||
id=$2
|
||||
id=$3
|
||||
`
|
||||
_, err := s.db.Exec(fmt.Sprintf(query, field), value, sessionID)
|
||||
_, err := s.db.Exec(query, value, field, sessionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`store: unable to update session field: %v`, err)
|
||||
return fmt.Errorf(`store: unable to update session JSON field %q: %v`, field, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -472,7 +472,8 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
|
||||
// AnotherUserWithFieldExists returns true if a user has the value set for the given field.
|
||||
func (s *Storage) AnotherUserWithFieldExists(userID int64, field, value string) bool {
|
||||
var result bool
|
||||
s.db.QueryRow(fmt.Sprintf(`SELECT true FROM users WHERE id <> $1 AND %s=$2 LIMIT 1`, pq.QuoteIdentifier(field)), userID, value).Scan(&result)
|
||||
query := `SELECT true FROM users WHERE id <> $1 AND ` + pq.QuoteIdentifier(field) + `=$2 LIMIT 1`
|
||||
s.db.QueryRow(query, userID, value).Scan(&result)
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ func (f *funcMap) Map() template.FuncMap {
|
||||
"truncate": truncate,
|
||||
"isEmail": isEmail,
|
||||
"baseURL": config.Opts.BaseURL,
|
||||
"apiEnabled": config.Opts.HasAPI,
|
||||
"rootURL": config.Opts.RootURL,
|
||||
"disableLocalAuth": config.Opts.DisableLocalAuth,
|
||||
"oidcProviderName": config.Opts.OAuth2OIDCProviderName,
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
<li>
|
||||
<a href="{{ route "integrations" }}">{{ icon "third-party-services" }}{{ t "menu.integrations" }}</a>
|
||||
</li>
|
||||
{{ if apiEnabled }}
|
||||
<li>
|
||||
<a href="{{ route "apiKeys" }}">{{ icon "api" }}{{ t "menu.api_keys" }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
<li>
|
||||
<a href="{{ route "sessions" }}">{{ icon "sessions" }}{{ t "menu.sessions" }}</a>
|
||||
</li>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<div class="panel">
|
||||
<h3>{{ t "page.about.credits" }}</h3>
|
||||
<ul>
|
||||
<li><strong>{{ t "page.about.author" }}</strong> Frédéric Guillot</li>
|
||||
<li><strong>{{ t "page.about.author" }}</strong> Frédéric Guillot and <a href="https://github.com/miniflux/v2/graphs/contributors">contributors</a></li>
|
||||
<li><strong>{{ t "page.about.license" }}</strong> <a href="https://www.apache.org/licenses/LICENSE-2.0">Apache 2.0</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -97,6 +97,8 @@
|
||||
<a
|
||||
{{ if $.showOnlyUnreadEntries }}
|
||||
href="{{ route "unreadCategoryEntry" "categoryID" .Feed.Category.ID "entryID" .ID }}"
|
||||
{{ else if $.showOnlyStarredEntries }}
|
||||
href="{{ route "starredCategoryEntry" "categoryID" .Feed.Category.ID "entryID" .ID }}"
|
||||
{{ else }}
|
||||
href="{{ route "categoryEntry" "categoryID" .Feed.Category.ID "entryID" .ID }}"
|
||||
{{ end }}
|
||||
|
||||
@@ -284,6 +284,9 @@
|
||||
<label for="form-linkwarden-api-key">{{ t "form.integration.linkwarden_api_key" }}</label>
|
||||
<input type="text" name="linkwarden_api_key" id="form-linkwarden-api-key" value="{{ .form.LinkwardenAPIKey }}" spellcheck="false">
|
||||
|
||||
<label for="form-linkwarden-collection-id">{{ t "form.integration.linkwarden_collection_id" }}</label>
|
||||
<input type="number" name="linkwarden_collection_id" id="form-linkwarden-collection-id" {{ if .form.LinkwardenCollectionID }}value="{{ .form.LinkwardenCollectionID }}"{{ end }} spellcheck="false">
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
|
||||
</div>
|
||||
@@ -506,6 +509,10 @@
|
||||
<input type="checkbox" name="readeck_enabled" value="1" {{ if .form.ReadeckEnabled }}checked{{ end }}> {{ t "form.integration.readeck_activate" }}
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="readeck_push_enabled" value="1" {{ if .form.ReadeckPushEnabled }}checked{{ end }}> {{ t "form.integration.readeck_push_activate" }}
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="readeck_only_url" value="1" {{ if .form.ReadeckOnlyURL }}checked{{ end }}> {{ t "form.integration.readeck_only_url" }}
|
||||
</label>
|
||||
|
||||
@@ -71,6 +71,7 @@ type IntegrationForm struct {
|
||||
LinkwardenEnabled bool
|
||||
LinkwardenURL string
|
||||
LinkwardenAPIKey string
|
||||
LinkwardenCollectionID *int64
|
||||
MatrixBotEnabled bool
|
||||
MatrixBotUser string
|
||||
MatrixBotPassword string
|
||||
@@ -80,6 +81,7 @@ type IntegrationForm struct {
|
||||
AppriseURL string
|
||||
AppriseServicesURL string
|
||||
ReadeckEnabled bool
|
||||
ReadeckPushEnabled bool
|
||||
ReadeckURL string
|
||||
ReadeckAPIKey string
|
||||
ReadeckLabels string
|
||||
@@ -192,6 +194,7 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
|
||||
integration.LinkwardenEnabled = i.LinkwardenEnabled
|
||||
integration.LinkwardenURL = i.LinkwardenURL
|
||||
integration.LinkwardenAPIKey = i.LinkwardenAPIKey
|
||||
integration.LinkwardenCollectionID = i.LinkwardenCollectionID
|
||||
integration.MatrixBotEnabled = i.MatrixBotEnabled
|
||||
integration.MatrixBotUser = i.MatrixBotUser
|
||||
integration.MatrixBotPassword = i.MatrixBotPassword
|
||||
@@ -201,6 +204,7 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
|
||||
integration.AppriseServicesURL = i.AppriseServicesURL
|
||||
integration.AppriseURL = i.AppriseURL
|
||||
integration.ReadeckEnabled = i.ReadeckEnabled
|
||||
integration.ReadeckPushEnabled = i.ReadeckPushEnabled
|
||||
integration.ReadeckURL = i.ReadeckURL
|
||||
integration.ReadeckAPIKey = i.ReadeckAPIKey
|
||||
integration.ReadeckLabels = i.ReadeckLabels
|
||||
@@ -315,6 +319,7 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
|
||||
LinkwardenEnabled: r.FormValue("linkwarden_enabled") == "1",
|
||||
LinkwardenURL: r.FormValue("linkwarden_url"),
|
||||
LinkwardenAPIKey: r.FormValue("linkwarden_api_key"),
|
||||
LinkwardenCollectionID: optionalInt64Field(r.FormValue("linkwarden_collection_id")),
|
||||
MatrixBotEnabled: r.FormValue("matrix_bot_enabled") == "1",
|
||||
MatrixBotUser: r.FormValue("matrix_bot_user"),
|
||||
MatrixBotPassword: r.FormValue("matrix_bot_password"),
|
||||
@@ -324,6 +329,7 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
|
||||
AppriseURL: r.FormValue("apprise_url"),
|
||||
AppriseServicesURL: r.FormValue("apprise_services_url"),
|
||||
ReadeckEnabled: r.FormValue("readeck_enabled") == "1",
|
||||
ReadeckPushEnabled: r.FormValue("readeck_push_enabled") == "1",
|
||||
ReadeckURL: r.FormValue("readeck_url"),
|
||||
ReadeckAPIKey: r.FormValue("readeck_api_key"),
|
||||
ReadeckLabels: r.FormValue("readeck_labels"),
|
||||
|
||||
@@ -84,6 +84,7 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
|
||||
LinkwardenEnabled: integration.LinkwardenEnabled,
|
||||
LinkwardenURL: integration.LinkwardenURL,
|
||||
LinkwardenAPIKey: integration.LinkwardenAPIKey,
|
||||
LinkwardenCollectionID: integration.LinkwardenCollectionID,
|
||||
MatrixBotEnabled: integration.MatrixBotEnabled,
|
||||
MatrixBotUser: integration.MatrixBotUser,
|
||||
MatrixBotPassword: integration.MatrixBotPassword,
|
||||
@@ -93,6 +94,7 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
|
||||
AppriseURL: integration.AppriseURL,
|
||||
AppriseServicesURL: integration.AppriseServicesURL,
|
||||
ReadeckEnabled: integration.ReadeckEnabled,
|
||||
ReadeckPushEnabled: integration.ReadeckPushEnabled,
|
||||
ReadeckURL: integration.ReadeckURL,
|
||||
ReadeckAPIKey: integration.ReadeckAPIKey,
|
||||
ReadeckLabels: integration.ReadeckLabels,
|
||||
|
||||
@@ -6,7 +6,6 @@ package ui // import "miniflux.app/v2/internal/ui"
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/cookie"
|
||||
@@ -17,6 +16,7 @@ import (
|
||||
"miniflux.app/v2/internal/ui/form"
|
||||
"miniflux.app/v2/internal/ui/session"
|
||||
"miniflux.app/v2/internal/ui/view"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
)
|
||||
|
||||
func (h *handler) checkLogin(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -96,11 +96,9 @@ func (h *handler) checkLogin(w http.ResponseWriter, r *http.Request) {
|
||||
config.Opts.BasePath(),
|
||||
))
|
||||
|
||||
if redirectURL != "" {
|
||||
if parsedURL, err := url.Parse(redirectURL); err == nil && !parsedURL.IsAbs() {
|
||||
html.Redirect(w, r, redirectURL)
|
||||
return
|
||||
}
|
||||
if redirectURL != "" && urllib.IsRelativePath(redirectURL) {
|
||||
html.Redirect(w, r, redirectURL)
|
||||
return
|
||||
}
|
||||
|
||||
html.Redirect(w, r, route.Path(h.router, user.DefaultHomePage))
|
||||
|
||||
@@ -206,6 +206,19 @@ func (m *middleware) handleAuthProxy(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
remoteIP := request.FindRemoteIP(r)
|
||||
trustedNetworks := config.Opts.TrustedReverseProxyNetworks()
|
||||
if !request.IsTrustedIP(remoteIP, trustedNetworks) {
|
||||
slog.Warn("[AuthProxy] Rejecting authentication request from untrusted proxy",
|
||||
slog.String("remote_ip", remoteIP),
|
||||
slog.String("client_ip", request.ClientIP(r)),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.Any("trusted_networks", trustedNetworks),
|
||||
)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
username := r.Header.Get(config.Opts.AuthProxyHeader())
|
||||
if username == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
@@ -215,6 +228,7 @@ func (m *middleware) handleAuthProxy(next http.Handler) http.Handler {
|
||||
clientIP := request.ClientIP(r)
|
||||
slog.Debug("[AuthProxy] Received authenticated requested",
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("remote_ip", remoteIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.String("username", username),
|
||||
)
|
||||
@@ -230,6 +244,7 @@ func (m *middleware) handleAuthProxy(next http.Handler) http.Handler {
|
||||
slog.Debug("[AuthProxy] User doesn't exist and user creation is not allowed",
|
||||
slog.Bool("authentication_failed", true),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("remote_ip", remoteIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.String("username", username),
|
||||
)
|
||||
@@ -252,6 +267,7 @@ func (m *middleware) handleAuthProxy(next http.Handler) http.Handler {
|
||||
slog.Info("[AuthProxy] User authenticated successfully",
|
||||
slog.Bool("authentication_successful", true),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("remote_ip", remoteIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.Int64("user_id", user.ID),
|
||||
slog.String("username", user.Username),
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"miniflux.app/v2/internal/http/response/html"
|
||||
"miniflux.app/v2/internal/reader/fetcher"
|
||||
"miniflux.app/v2/internal/reader/rewrite"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
)
|
||||
|
||||
func (h *handler) mediaProxy(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -81,6 +82,24 @@ func (h *handler) mediaProxy(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
mediaURL := string(decodedURL)
|
||||
|
||||
if !config.Opts.MediaProxyAllowPrivateNetworks() {
|
||||
if isPrivate, err := urllib.ResolvesToPrivateIP(parsedMediaURL.Hostname()); err != nil {
|
||||
slog.Warn("MediaProxy: Unable to resolve hostname",
|
||||
slog.String("media_url", mediaURL),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
html.Forbidden(w, r)
|
||||
return
|
||||
} else if isPrivate {
|
||||
slog.Warn("MediaProxy: Refusing to access private IP address",
|
||||
slog.String("media_url", mediaURL),
|
||||
)
|
||||
html.Forbidden(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
slog.Debug("MediaProxy: Fetching remote resource",
|
||||
slog.String("media_url", mediaURL),
|
||||
)
|
||||
|
||||
@@ -22,53 +22,54 @@ func New(store *storage.Storage, sessionID string) *Session {
|
||||
}
|
||||
|
||||
func (s *Session) SetLastForceRefresh() {
|
||||
s.store.UpdateAppSessionField(s.sessionID, "last_force_refresh", time.Now().UTC().Unix())
|
||||
s.store.SetAppSessionTextField(s.sessionID, "last_force_refresh", time.Now().UTC().Unix())
|
||||
}
|
||||
|
||||
func (s *Session) SetOAuth2State(state string) {
|
||||
s.store.UpdateAppSessionField(s.sessionID, "oauth2_state", state)
|
||||
s.store.SetAppSessionTextField(s.sessionID, "oauth2_state", state)
|
||||
}
|
||||
|
||||
func (s *Session) SetOAuth2CodeVerifier(codeVerfier string) {
|
||||
s.store.UpdateAppSessionField(s.sessionID, "oauth2_code_verifier", codeVerfier)
|
||||
s.store.SetAppSessionTextField(s.sessionID, "oauth2_code_verifier", codeVerfier)
|
||||
}
|
||||
|
||||
// NewFlashMessage creates a new flash message.
|
||||
func (s *Session) NewFlashMessage(message string) {
|
||||
s.store.UpdateAppSessionField(s.sessionID, "flash_message", message)
|
||||
s.store.SetAppSessionTextField(s.sessionID, "flash_message", message)
|
||||
}
|
||||
|
||||
// FlashMessage returns the current flash message if any.
|
||||
func (s *Session) FlashMessage(message string) string {
|
||||
if message != "" {
|
||||
s.store.UpdateAppSessionField(s.sessionID, "flash_message", "")
|
||||
s.store.SetAppSessionTextField(s.sessionID, "flash_message", "")
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
// NewFlashErrorMessage creates a new flash error message.
|
||||
func (s *Session) NewFlashErrorMessage(message string) {
|
||||
s.store.UpdateAppSessionField(s.sessionID, "flash_error_message", message)
|
||||
s.store.SetAppSessionTextField(s.sessionID, "flash_error_message", message)
|
||||
}
|
||||
|
||||
// FlashErrorMessage returns the last flash error message if any.
|
||||
func (s *Session) FlashErrorMessage(message string) string {
|
||||
if message != "" {
|
||||
s.store.UpdateAppSessionField(s.sessionID, "flash_error_message", "")
|
||||
s.store.SetAppSessionTextField(s.sessionID, "flash_error_message", "")
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
// SetLanguage updates the language field in session.
|
||||
func (s *Session) SetLanguage(language string) {
|
||||
s.store.UpdateAppSessionField(s.sessionID, "language", language)
|
||||
s.store.SetAppSessionTextField(s.sessionID, "language", language)
|
||||
}
|
||||
|
||||
// SetTheme updates the theme field in session.
|
||||
func (s *Session) SetTheme(theme string) {
|
||||
s.store.UpdateAppSessionField(s.sessionID, "theme", theme)
|
||||
s.store.SetAppSessionTextField(s.sessionID, "theme", theme)
|
||||
}
|
||||
|
||||
// SetWebAuthnSessionData sets the WebAuthn session data.
|
||||
func (s *Session) SetWebAuthnSessionData(sessionData *model.WebAuthnSession) {
|
||||
s.store.UpdateAppSessionObjectField(s.sessionID, "webauthn_session_data", sessionData)
|
||||
s.store.SetAppSessionJSONField(s.sessionID, "webauthn_session_data", sessionData)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ui // import "miniflux.app/v2/internal/ui"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/html"
|
||||
"miniflux.app/v2/internal/http/route"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
"miniflux.app/v2/internal/ui/session"
|
||||
"miniflux.app/v2/internal/ui/view"
|
||||
)
|
||||
|
||||
func (h *handler) showStarredCategoryEntryPage(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := h.store.UserByID(request.UserID(r))
|
||||
if err != nil {
|
||||
html.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
categoryID := request.RouteInt64Param(r, "categoryID")
|
||||
entryID := request.RouteInt64Param(r, "entryID")
|
||||
|
||||
builder := h.store.NewEntryQueryBuilder(user.ID)
|
||||
builder.WithCategoryID(categoryID)
|
||||
builder.WithEntryID(entryID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
|
||||
entry, err := builder.GetEntry()
|
||||
if err != nil {
|
||||
html.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
html.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if entry.ShouldMarkAsReadOnView(user) {
|
||||
err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
|
||||
if err != nil {
|
||||
html.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
entry.Status = model.EntryStatusRead
|
||||
}
|
||||
|
||||
if user.AlwaysOpenExternalLinks {
|
||||
html.Redirect(w, r, entry.URL)
|
||||
return
|
||||
}
|
||||
|
||||
entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, user.EntryOrder, user.EntryDirection)
|
||||
entryPaginationBuilder.WithCategoryID(categoryID)
|
||||
entryPaginationBuilder.WithStarred()
|
||||
|
||||
prevEntry, nextEntry, err := entryPaginationBuilder.Entries()
|
||||
if err != nil {
|
||||
html.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
nextEntryRoute := ""
|
||||
if nextEntry != nil {
|
||||
nextEntryRoute = route.Path(h.router, "starredCategoryEntry", "categoryID", categoryID, "entryID", nextEntry.ID)
|
||||
}
|
||||
|
||||
prevEntryRoute := ""
|
||||
if prevEntry != nil {
|
||||
prevEntryRoute = route.Path(h.router, "starredCategoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID)
|
||||
}
|
||||
|
||||
sess := session.New(h.store, request.SessionID(r))
|
||||
view := view.New(h.tpl, r, sess)
|
||||
view.Set("entry", entry)
|
||||
view.Set("prevEntry", prevEntry)
|
||||
view.Set("nextEntry", nextEntry)
|
||||
view.Set("nextEntryRoute", nextEntryRoute)
|
||||
view.Set("prevEntryRoute", prevEntryRoute)
|
||||
view.Set("menu", "categories")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
|
||||
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
|
||||
view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
|
||||
|
||||
html.OK(w, r, view.Render("entry"))
|
||||
}
|
||||
|
||||
@@ -82,6 +82,18 @@ a:hover {
|
||||
translate: -50% 0;
|
||||
}
|
||||
|
||||
/* Smoother pages transition */
|
||||
@media not all and (prefers-reduced-motion: reduce) {
|
||||
@view-transition {
|
||||
navigation: auto;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@view-transition {
|
||||
navigation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Header and main menu */
|
||||
.header {
|
||||
margin-top: 10px;
|
||||
|
||||
@@ -648,10 +648,7 @@ function toggleEntryStatus(element, toasting) {
|
||||
showToastNotification(newStatus, currentStatus === "read" ? buttonElement.dataset.toastUnread : buttonElement.dataset.toastRead);
|
||||
}
|
||||
|
||||
if (element.classList.contains("item-status-" + currentStatus)) {
|
||||
element.classList.remove("item-status-" + currentStatus);
|
||||
element.classList.add("item-status-" + newStatus);
|
||||
}
|
||||
element.classList.replace("item-status-" + currentStatus, "item-status-" + newStatus);
|
||||
|
||||
if (isListView() && getVisibleEntries().length === 0) {
|
||||
window.location.reload();
|
||||
@@ -821,10 +818,7 @@ function openOriginalLinkFromListView() {
|
||||
goToListItem(1);
|
||||
|
||||
// Mark as read if currently unread
|
||||
if (currentItem.classList.contains("item-status-unread")) {
|
||||
currentItem.classList.remove("item-status-unread");
|
||||
currentItem.classList.add("item-status-read");
|
||||
|
||||
if (currentItem.classList.replace("item-status-unread", "item-status-read")) {
|
||||
const entryID = parseInt(currentItem.dataset.id, 10);
|
||||
updateEntriesStatus([entryID], "read");
|
||||
}
|
||||
@@ -897,7 +891,7 @@ function updateUnreadCounterValue(delta) {
|
||||
if (window.location.href.endsWith('/unread')) {
|
||||
const oldValue = parseInt(document.title.split('(')[1], 10);
|
||||
const newValue = oldValue + delta;
|
||||
document.title = document.title.replace(/(.*?)\(\d+\)(.*?)/, `$1(${newValue})$2`);
|
||||
document.title = document.title.replace(/\(\d+\)/, `(${newValue})`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-4
@@ -79,6 +79,7 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
|
||||
// Category pages.
|
||||
uiRouter.HandleFunc("/category/{categoryID}/entry/{entryID}", handler.showCategoryEntryPage).Name("categoryEntry").Methods(http.MethodGet)
|
||||
uiRouter.HandleFunc("/unread/category/{categoryID}/entry/{entryID}", handler.showUnreadCategoryEntryPage).Name("unreadCategoryEntry").Methods(http.MethodGet)
|
||||
uiRouter.HandleFunc("/starred/category/{categoryID}/entry/{entryID}", handler.showStarredCategoryEntryPage).Name("starredCategoryEntry").Methods(http.MethodGet)
|
||||
uiRouter.HandleFunc("/categories", handler.showCategoryListPage).Name("categories").Methods(http.MethodGet)
|
||||
uiRouter.HandleFunc("/category/create", handler.showCreateCategoryPage).Name("createCategory").Methods(http.MethodGet)
|
||||
uiRouter.HandleFunc("/category/save", handler.saveCategory).Name("saveCategory").Methods(http.MethodPost)
|
||||
@@ -132,10 +133,12 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
|
||||
uiRouter.HandleFunc("/sessions/{sessionID}/remove", handler.removeSession).Name("removeSession").Methods(http.MethodPost)
|
||||
|
||||
// API Keys pages.
|
||||
uiRouter.HandleFunc("/keys", handler.showAPIKeysPage).Name("apiKeys").Methods(http.MethodGet)
|
||||
uiRouter.HandleFunc("/keys/{keyID}/delete", handler.deleteAPIKey).Name("deleteAPIKey").Methods(http.MethodPost)
|
||||
uiRouter.HandleFunc("/keys/create", handler.showCreateAPIKeyPage).Name("createAPIKey").Methods(http.MethodGet)
|
||||
uiRouter.HandleFunc("/keys/save", handler.saveAPIKey).Name("saveAPIKey").Methods(http.MethodPost)
|
||||
if config.Opts.HasAPI() {
|
||||
uiRouter.HandleFunc("/keys", handler.showAPIKeysPage).Name("apiKeys").Methods(http.MethodGet)
|
||||
uiRouter.HandleFunc("/keys/{keyID}/delete", handler.deleteAPIKey).Name("deleteAPIKey").Methods(http.MethodPost)
|
||||
uiRouter.HandleFunc("/keys/create", handler.showCreateAPIKeyPage).Name("createAPIKey").Methods(http.MethodGet)
|
||||
uiRouter.HandleFunc("/keys/save", handler.saveAPIKey).Name("saveAPIKey").Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
// OPML pages.
|
||||
uiRouter.HandleFunc("/export", handler.exportFeeds).Name("export").Methods(http.MethodGet)
|
||||
|
||||
@@ -6,10 +6,27 @@ package urllib // import "miniflux.app/v2/internal/urllib"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsRelativePath returns true if the link is a relative path.
|
||||
func IsRelativePath(link string) bool {
|
||||
if link == "" {
|
||||
return false
|
||||
}
|
||||
if parsedURL, err := url.Parse(link); err == nil {
|
||||
// Only allow relative paths (not scheme-relative URLs like //example.org)
|
||||
// and ensure the URL doesn't have a host component
|
||||
if !parsedURL.IsAbs() && parsedURL.Host == "" && parsedURL.Scheme == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsAbsoluteURL returns true if the link is absolute.
|
||||
func IsAbsoluteURL(link string) bool {
|
||||
u, err := url.Parse(link)
|
||||
@@ -123,3 +140,33 @@ func JoinBaseURLAndPath(baseURL, path string) (string, error) {
|
||||
|
||||
return finalURL, nil
|
||||
}
|
||||
|
||||
// ResolvesToPrivateIP resolves a hostname and returns true if
|
||||
// ANY resolved IP address is non-public.
|
||||
func ResolvesToPrivateIP(host string) (bool, error) {
|
||||
ips, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if slices.ContainsFunc(ips, isNonPublicIP) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// isNonPublicIP returns true if the given IP is private, loopback,
|
||||
// link-local, multicast, or unspecified.
|
||||
func isNonPublicIP(ip net.IP) bool {
|
||||
if ip == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return ip.IsPrivate() ||
|
||||
ip.IsLoopback() ||
|
||||
ip.IsLinkLocalUnicast() ||
|
||||
ip.IsLinkLocalMulticast() ||
|
||||
ip.IsMulticast() ||
|
||||
ip.IsUnspecified()
|
||||
}
|
||||
|
||||
+107
-1
@@ -3,7 +3,44 @@
|
||||
|
||||
package urllib // import "miniflux.app/v2/internal/urllib"
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsRelativePath(t *testing.T) {
|
||||
scenarios := map[string]bool{
|
||||
// Valid relative paths
|
||||
"path/to/file.ext": true,
|
||||
"./path/to/file.ext": true,
|
||||
"../path/to/file.ext": true,
|
||||
"file.ext": true,
|
||||
"./file.ext": true,
|
||||
"../file.ext": true,
|
||||
"/absolute/path": true,
|
||||
"path?query=value": true,
|
||||
"path#fragment": true,
|
||||
"path?query#fragment": true,
|
||||
|
||||
// Not relative paths
|
||||
"https://example.org/file.ext": false,
|
||||
"http://example.org/file.ext": false,
|
||||
"//example.org/file.ext": false,
|
||||
"//example.org": false,
|
||||
"ftp://example.org/file.ext": false,
|
||||
"mailto:user@example.org": false,
|
||||
"magnet:?xt=urn:btih:example": false,
|
||||
"": false,
|
||||
"magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C": false,
|
||||
}
|
||||
|
||||
for input, expected := range scenarios {
|
||||
actual := IsRelativePath(input)
|
||||
if actual != expected {
|
||||
t.Errorf(`Unexpected result for IsRelativePath, got %v instead of %v for %q`, actual, expected, input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAbsoluteURL(t *testing.T) {
|
||||
scenarios := map[string]bool{
|
||||
@@ -120,3 +157,72 @@ func TestJoinBaseURLAndPath(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNonPublicIP(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
ipString string
|
||||
want bool
|
||||
}{
|
||||
{"nil", "", true},
|
||||
{"private IPv4", "192.168.1.10", true},
|
||||
{"loopback IPv4", "127.0.0.1", true},
|
||||
{"link-local IPv4", "169.254.42.1", true},
|
||||
{"multicast IPv4", "224.0.0.1", true},
|
||||
{"unspecified IPv6", "::", true},
|
||||
{"loopback IPv6", "::1", true},
|
||||
{"multicast IPv6", "ff02::1", true},
|
||||
{"public IPv4", "93.184.216.34", false},
|
||||
{"public IPv6", "2001:4860:4860::8888", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var ip net.IP
|
||||
if tc.ipString != "" {
|
||||
ip = net.ParseIP(tc.ipString)
|
||||
if ip == nil {
|
||||
t.Fatalf("unable to parse %q", tc.ipString)
|
||||
}
|
||||
}
|
||||
|
||||
if got := isNonPublicIP(ip); got != tc.want {
|
||||
t.Fatalf("unexpected result for %s: got %v want %v", tc.name, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvesToPrivateIP(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
host string
|
||||
want bool
|
||||
}{
|
||||
{"localhost", "localhost", true},
|
||||
{"example.org", "example.org", false},
|
||||
{"loopback IPv4 literal", "127.0.0.1", true},
|
||||
{"loopback IPv6 literal", "::1", true},
|
||||
{"private IPv4 literal", "192.168.1.1", true},
|
||||
{"public IPv4 literal", "93.184.216.34", false},
|
||||
{"public IPv6 literal", "2001:4860:4860::8888", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := ResolvesToPrivateIP(tc.host)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error for %s: %v", tc.host, err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Fatalf("unexpected result for %s: got %v want %v", tc.name, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvesToPrivateIPError(t *testing.T) {
|
||||
if _, err := ResolvesToPrivateIP(""); err == nil {
|
||||
t.Fatalf("expected an error for empty host")
|
||||
}
|
||||
}
|
||||
|
||||
+59
-28
@@ -1,20 +1,22 @@
|
||||
.\" Manpage for miniflux.
|
||||
.TH "MINIFLUX" "1" "September 29, 2025" "\ \&" "\ \&"
|
||||
.TH "MINIFLUX" "1" "January 5, 2026" "\ \&" "\ \&"
|
||||
|
||||
.SH NAME
|
||||
miniflux \- Minimalist and opinionated feed reader
|
||||
|
||||
.SH SYNOPSIS
|
||||
\fBminiflux\fR [-vic] [-config-dump] [-config-file] [-create-admin] [-debug]
|
||||
[-flush-sessions] [-healthcheck] [-info] [-migrate] [-refresh-feeds]
|
||||
[-reset-feed-errors] [-reset-feed-next-check-at] [-reset-password]
|
||||
[-run-cleanup-tasks] [-version]
|
||||
\fBminiflux\fR [\fBoptions\fR]
|
||||
|
||||
.SH DESCRIPTION
|
||||
\fBminiflux\fR is a minimalist and opinionated feed reader.
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
.B \-h, \-help
|
||||
.RS 4
|
||||
Show usage information and exit\&.
|
||||
.RE
|
||||
.PP
|
||||
.B \-config-dump
|
||||
.RS 4
|
||||
Print parsed configuration values. This will include sensitive information like passwords\&.
|
||||
@@ -44,7 +46,10 @@ Set log level to debug\&.
|
||||
.RS 4
|
||||
Export user feeds (provide the username as argument)\&.
|
||||
.br
|
||||
Example: "miniflux -export-user-feeds someone > feeds.xml"\&.
|
||||
Example:
|
||||
.EX
|
||||
miniflux -export-user-feeds someone > feeds.xml
|
||||
.EE
|
||||
.RE
|
||||
.PP
|
||||
.B \-flush-sessions
|
||||
@@ -56,7 +61,7 @@ Flush all sessions (disconnect users)\&.
|
||||
.RS 4
|
||||
Perform a health check on the given endpoint\&.
|
||||
.br
|
||||
The value "auto" try to guess the health check endpoint\&.
|
||||
The value "auto" tries to guess the health check endpoint\&.
|
||||
.RE
|
||||
.PP
|
||||
.B \-i
|
||||
@@ -96,7 +101,7 @@ Reset user password\&.
|
||||
.PP
|
||||
.B \-run-cleanup-tasks
|
||||
.RS 4
|
||||
Run cleanup tasks (delete old sessions and archives old entries)\&.
|
||||
Run cleanup tasks (delete old sessions and archive old entries)\&.
|
||||
.RE
|
||||
.PP
|
||||
.B \-v
|
||||
@@ -110,7 +115,7 @@ Show application version\&.
|
||||
.RE
|
||||
|
||||
.SH CONFIGURATION FILE
|
||||
The configuration file is a text file that follow these rules:
|
||||
The configuration file is a text file that follows these rules:
|
||||
.LP
|
||||
- Miniflux expects each line to be in KEY=VALUE format.
|
||||
.br
|
||||
@@ -120,35 +125,41 @@ The configuration file is a text file that follow these rules:
|
||||
.br
|
||||
- There is no variable interpolation.
|
||||
.PP
|
||||
Keys are the same as the environment variables described below.
|
||||
Keys are the same as the environment variables described below\&.
|
||||
.br
|
||||
Environment variables override the values defined in the config file.
|
||||
Environment variables override the values defined in the config file\&.
|
||||
|
||||
.SH ENVIRONMENT
|
||||
.PP
|
||||
Boolean options accept the following values (case-insensitive): 1/0, yes/no, true/false, on/off\&.
|
||||
.br
|
||||
For variables ending in \fB_FILE\fR, the value is a path to a file that contains the corresponding secret value\&.
|
||||
.TP
|
||||
.B ADMIN_PASSWORD
|
||||
Admin user password, used only if $CREATE_ADMIN is enabled\&.
|
||||
Admin user password, used only if \fBCREATE_ADMIN\fR is enabled\&.
|
||||
.br
|
||||
Default is empty\&.
|
||||
.TP
|
||||
.B ADMIN_PASSWORD_FILE
|
||||
Path to a secret key exposed as a file, it should contain $ADMIN_PASSWORD value\&.
|
||||
Path to a secret key exposed as a file, it should contain the \fBADMIN_PASSWORD\fR value\&.
|
||||
.br
|
||||
Default is empty\&.
|
||||
.TP
|
||||
.B ADMIN_USERNAME
|
||||
Admin user login, used only if $CREATE_ADMIN is enabled\&.
|
||||
Admin user login, used only if \fBCREATE_ADMIN\fR is enabled\&.
|
||||
.br
|
||||
Default is empty\&.
|
||||
.TP
|
||||
.B ADMIN_USERNAME_FILE
|
||||
Path to a secret key exposed as a file, it should contain $ADMIN_USERNAME value\&.
|
||||
Path to a secret key exposed as a file, it should contain the \fBADMIN_USERNAME\fR value\&.
|
||||
.br
|
||||
Default is empty\&.
|
||||
.TP
|
||||
.B AUTH_PROXY_HEADER
|
||||
Proxy authentication HTTP header\&.
|
||||
.br
|
||||
The option \fBTRUSTED_REVERSE_PROXY_NETWORKS\fR must be configured to allow the proxy to authenticate users\&.
|
||||
.br
|
||||
Default is empty.
|
||||
.TP
|
||||
.B AUTH_PROXY_USER_CREATION
|
||||
@@ -159,7 +170,7 @@ Disabled by default\&.
|
||||
.B BASE_URL
|
||||
Base URL to generate HTML links and base path for cookies\&.
|
||||
.br
|
||||
Default is http://localhost/\&.
|
||||
Default is http://localhost\&.
|
||||
.TP
|
||||
.B BATCH_SIZE
|
||||
Number of feeds to send to the queue for each interval\&.
|
||||
@@ -226,15 +237,20 @@ Minimum number of database connections\&.
|
||||
Default is 1\&.
|
||||
.TP
|
||||
.B DATABASE_URL
|
||||
Postgresql connection parameters\&.
|
||||
PostgreSQL connection parameters\&.
|
||||
.br
|
||||
Default is "user=postgres password=postgres dbname=miniflux2 sslmode=disable"\&.
|
||||
.TP
|
||||
.B DATABASE_URL_FILE
|
||||
Path to a secret key exposed as a file, it should contain $DATABASE_URL value\&.
|
||||
Path to a secret key exposed as a file, it should contain the \fBDATABASE_URL\fR value\&.
|
||||
.br
|
||||
Default is empty\&.
|
||||
.TP
|
||||
.B DISABLE_API
|
||||
Disable miniflux's API\&.
|
||||
.br
|
||||
Default is false (The API is enabled)\&.
|
||||
.TP
|
||||
.B DISABLE_HSTS
|
||||
Disable HTTP Strict Transport Security header if \fBHTTPS\fR is set\&.
|
||||
.br
|
||||
@@ -283,9 +299,9 @@ use it as a reading time\&.
|
||||
Disabled by default\&.
|
||||
.TP
|
||||
.B FILTER_ENTRY_MAX_AGE_DAYS
|
||||
Number of days after which new entries should be retained\&.
|
||||
Ignore new entries older than the given number of days\&.
|
||||
.br
|
||||
Set 7 to fetch only entries 7 days old\&.
|
||||
Set to 7 to fetch only entries from the last 7 days\&.
|
||||
.br
|
||||
Default is 0 (disabled)\&.
|
||||
.TP
|
||||
@@ -310,7 +326,7 @@ Proxy URL to use when the "Fetch via proxy" feed option is enabled\&.
|
||||
Default is empty\&.
|
||||
.TP
|
||||
.B HTTP_CLIENT_TIMEOUT
|
||||
Time limit in seconds before the HTTP client cancel the request\&.
|
||||
Time limit in seconds before the HTTP client cancels the request\&.
|
||||
.br
|
||||
Default is 20 seconds\&.
|
||||
.TP
|
||||
@@ -322,7 +338,7 @@ When empty, Miniflux uses a default User-Agent that includes the Miniflux versio
|
||||
Default is empty.
|
||||
.TP
|
||||
.B HTTP_SERVER_TIMEOUT
|
||||
Time limit in seconds before the HTTP client cancel the request\&.
|
||||
Read, write, and idle timeout in seconds for the HTTP server\&.
|
||||
.br
|
||||
Default is 300 seconds\&.
|
||||
.TP
|
||||
@@ -331,6 +347,11 @@ Forces cookies to use secure flag and send HSTS header\&.
|
||||
.br
|
||||
Default is disabled\&.
|
||||
.TP
|
||||
.B ICON_FETCH_ALLOW_PRIVATE_NETWORKS
|
||||
Set to 1 to allow downloading favicons that resolve to private or loopback networks\&.
|
||||
.br
|
||||
Disabled by default, private networks are refused\&.
|
||||
.TP
|
||||
.B INVIDIOUS_INSTANCE
|
||||
Set a custom invidious instance to use\&.
|
||||
.br
|
||||
@@ -384,10 +405,15 @@ Sets an external server to proxy media through\&.
|
||||
Default is empty, Miniflux does the proxying\&.
|
||||
.TP
|
||||
.B MEDIA_PROXY_HTTP_CLIENT_TIMEOUT
|
||||
Time limit in seconds before the media proxy HTTP client cancel the request\&.
|
||||
Time limit in seconds before the media proxy HTTP client cancels the request\&.
|
||||
.br
|
||||
Default is 120 seconds\&.
|
||||
.TP
|
||||
.B MEDIA_PROXY_ALLOW_PRIVATE_NETWORKS
|
||||
Set to 1 to allow proxying media that resolves to private or loopback networks\&.
|
||||
.br
|
||||
Disabled by default, private networks are refused\&.
|
||||
.TP
|
||||
.B MEDIA_PROXY_RESOURCE_TYPES
|
||||
A comma-separated list of media types to proxify. Supported values are: image, audio, video\&.
|
||||
.br
|
||||
@@ -399,7 +425,7 @@ Possible values: http-only, all, or none\&.
|
||||
Default is http-only\&.
|
||||
.TP
|
||||
.B MEDIA_PROXY_PRIVATE_KEY
|
||||
Set a custom custom private key used to sign proxified media URLs\&.
|
||||
Set a custom private key used to sign proxified media URLs\&.
|
||||
.br
|
||||
By default, a secret key is randomly generated during startup\&.
|
||||
.TP
|
||||
@@ -444,7 +470,7 @@ OAuth2 client ID\&.
|
||||
Default is empty\&.
|
||||
.TP
|
||||
.B OAUTH2_CLIENT_ID_FILE
|
||||
Path to a secret key exposed as a file, it should contain $OAUTH2_CLIENT_ID value\&.
|
||||
Path to a secret key exposed as a file, it should contain the \fBOAUTH2_CLIENT_ID\fR value\&.
|
||||
.br
|
||||
Default is empty\&.
|
||||
.TP
|
||||
@@ -454,7 +480,7 @@ OAuth2 client secret\&.
|
||||
Default is empty\&.
|
||||
.TP
|
||||
.B OAUTH2_CLIENT_SECRET_FILE
|
||||
Path to a secret key exposed as a file, it should contain $OAUTH2_CLIENT_SECRET value\&.
|
||||
Path to a secret key exposed as a file, it should contain the \fBOAUTH2_CLIENT_SECRET\fR value\&.
|
||||
.br
|
||||
Default is empty\&.
|
||||
.TP
|
||||
@@ -522,7 +548,7 @@ Regardless of the scheduler used, the total number of polled feeds will not exce
|
||||
Default is "round_robin"\&.
|
||||
.TP
|
||||
.B PORT
|
||||
Override LISTEN_ADDR to 0.0.0.0:$PORT\&.
|
||||
Override \fBLISTEN_ADDR\fR to \fB0.0.0.0:$PORT\fR\&.
|
||||
.br
|
||||
Default is empty\&.
|
||||
.TP
|
||||
@@ -556,6 +582,11 @@ Minimum interval in minutes for the round robin scheduler\&.
|
||||
.br
|
||||
Default is 60 minutes\&.
|
||||
.TP
|
||||
.B TRUSTED_REVERSE_PROXY_NETWORKS
|
||||
List of networks (CIDR notation) allowed to use the proxy authentication header, \fBX-Forwarded-For\fR, \fBX-Forwarded-Proto\fR, and \fBX-Real-Ip\fR headers\&.
|
||||
.br
|
||||
Default is empty\&.
|
||||
.TP
|
||||
.B WATCHDOG
|
||||
Enable or disable Systemd watchdog\&.
|
||||
.br
|
||||
@@ -584,7 +615,7 @@ YouTube URL which will be used for embeds\&.
|
||||
Default is https://www.youtube-nocookie.com/embed/\&.
|
||||
.SH AUTHORS
|
||||
.P
|
||||
Miniflux is written and maintained by Fr\['e]d\['e]ric Guillot\&.
|
||||
Miniflux is developed and maintained by Fr\['e]d\['e]ric Guillot with contributions from the Miniflux community\&.
|
||||
|
||||
.SH "COPYRIGHT"
|
||||
.P
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
FROM docker.io/library/golang:alpine3.22 AS build
|
||||
FROM docker.io/library/golang:alpine3.23 AS build
|
||||
RUN apk add --no-cache build-base git make
|
||||
ADD . /go/src/app
|
||||
WORKDIR /go/src/app
|
||||
RUN make miniflux
|
||||
|
||||
FROM docker.io/library/alpine:3.22
|
||||
FROM docker.io/library/alpine:3.23
|
||||
|
||||
LABEL org.opencontainers.image.title=Miniflux
|
||||
LABEL org.opencontainers.image.description="Miniflux is a minimalist and opinionated feed reader"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM docker.io/library/golang:bookworm AS build
|
||||
FROM docker.io/library/golang:trixie AS build
|
||||
ADD . /go/src/app
|
||||
WORKDIR /go/src/app
|
||||
RUN make miniflux
|
||||
|
||||
FROM gcr.io/distroless/base-debian12:nonroot
|
||||
FROM gcr.io/distroless/base-debian13:nonroot
|
||||
|
||||
LABEL org.opencontainers.image.title=Miniflux
|
||||
LABEL org.opencontainers.image.description="Miniflux is a minimalist and opinionated feed reader"
|
||||
|
||||
Reference in New Issue
Block a user