Compare commits
360 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da951164d5 | |||
| a0988f6c16 | |||
| fddc861e41 | |||
| 9288001f09 | |||
| da261a5fc7 | |||
| edeb5f0366 | |||
| 65ff328804 | |||
| 7759ea1b43 | |||
| 7c5d6cf35f | |||
| 39cc1887ea | |||
| b0a3b4d5d9 | |||
| 469f23968e | |||
| 051bdecabd | |||
| fe10d4302d | |||
| b9dfd5bf6d | |||
| 51030ef1a8 | |||
| 2bcc4b8399 | |||
| ea4d0a4f72 | |||
| 5c5ad19c43 | |||
| 191f3a7ad7 | |||
| 366928b35d | |||
| 3b654fefa7 | |||
| 0adbcc3a04 | |||
| 7fdb450446 | |||
| 86285f5a05 | |||
| 8a4b4e459e | |||
| 879446bdb2 | |||
| ef633dc427 | |||
| 86c0cc61ba | |||
| ffe3ed4b9a | |||
| 5c4df786de | |||
| ee8c6621e9 | |||
| f748513df6 | |||
| e1050e21b5 | |||
| e555e442fb | |||
| 600dea6ce5 | |||
| e07203ad46 | |||
| f16735fd6d | |||
| 562a7b79a5 | |||
| cb610230d9 | |||
| 628b2b388d | |||
| faa70f3019 | |||
| cfe410f202 | |||
| c2ac2bfb83 | |||
| c326d5574b | |||
| f1c8c060c0 | |||
| 6944fb1e50 | |||
| 7d21298fab | |||
| 95201fc5cf | |||
| 5e335995e1 | |||
| 70f126fc5a | |||
| 38cdc4d3df | |||
| 4ab1cdd2e9 | |||
| f3e48505df | |||
| 60c75ab3b6 | |||
| 349f040921 | |||
| 74376cd33c | |||
| 2a4d2985c4 | |||
| 907941394b | |||
| 88ea0ade3e | |||
| fcf9fde118 | |||
| 5c38688783 | |||
| e0850fc648 | |||
| 8708a109b3 | |||
| 0fe787bb93 | |||
| 2dffcfeadc | |||
| 4bbc12e3b2 | |||
| 3e0e8dda2b | |||
| eb4bca6eb7 | |||
| 810b351772 | |||
| 89ff33ddd0 | |||
| f3a5a3ee14 | |||
| e98e16e45a | |||
| eb057d0415 | |||
| fa51c3ead7 | |||
| 56d7e4d5e9 | |||
| cc94ab704a | |||
| 9b8eabf036 | |||
| a8ac3dec47 | |||
| 6feee555ba | |||
| ee926e73cb | |||
| ade412f453 | |||
| 6fb7e84ce1 | |||
| 770cc1dbb3 | |||
| 59dac15bdf | |||
| da6aa36758 | |||
| 2a22fe6b75 | |||
| bcbf9f4025 | |||
| 569529d73b | |||
| 31cb06026d | |||
| d048d59d39 | |||
| 4f55361f5f | |||
| 37309adbc0 | |||
| 92f3dc26e4 | |||
| f6dc952551 | |||
| 29387f2d60 | |||
| c0f6e32a99 | |||
| 11cafec863 | |||
| 8cfe77a3cd | |||
| 8d4d092cd7 | |||
| 968355f9b9 | |||
| 3ca52c7f7f | |||
| 2e856a6bf0 | |||
| 36c25e7689 | |||
| cb97d4a1a8 | |||
| 79ea9e28b5 | |||
| f847c3e754 | |||
| 4ca19d123a | |||
| 90ef864edd | |||
| 01133c586f | |||
| b683756d8e | |||
| 3dfc70cee6 | |||
| 91b4a7d35f | |||
| 2fbe2df086 | |||
| 964698f363 | |||
| e34af65ae9 | |||
| e99a675912 | |||
| d96ad4ddef | |||
| 4272932402 | |||
| a60996e666 | |||
| a09ddbbaf4 | |||
| 92db691344 | |||
| a334c8e691 | |||
| bf1c851093 | |||
| c787bb5b48 | |||
| 1a81866bb9 | |||
| c4278821cb | |||
| 4498ba10e8 | |||
| a46e702536 | |||
| f0e8323f19 | |||
| a0106c9ffc | |||
| f98d5de484 | |||
| ee5e18ea9f | |||
| 3ef2522c62 | |||
| 839c4ad044 | |||
| 9f3a8e7f1b | |||
| e54825bf02 | |||
| 07f6d397d4 | |||
| f33e76eb8c | |||
| 84e97826d8 | |||
| 839fc3843a | |||
| 0bab8fac8e | |||
| 0cf1a40276 | |||
| 91479bc0ee | |||
| 251821289c | |||
| cac0bc682f | |||
| a733c14c61 | |||
| b68b05c64c | |||
| 5ce3f24838 | |||
| 48ddc02ba8 | |||
| fe9f1bba16 | |||
| 740fa4a5d2 | |||
| 8a38f54ef5 | |||
| ae432bc9c6 | |||
| 96f7e8bae0 | |||
| 1f35ed1675 | |||
| d6deac1810 | |||
| b692768730 | |||
| 2178580a75 | |||
| b52f61cc77 | |||
| 3388f8e376 | |||
| 83ceb20c1c | |||
| c06850ca34 | |||
| d856c02fbb | |||
| a33b1adf13 | |||
| a631bd527d | |||
| ca62b0b36b | |||
| 7d6a4243c1 | |||
| d056aa1f73 | |||
| 018e24404e | |||
| 4d3ee0d15d | |||
| 797450986b | |||
| 93bc9ce24d | |||
| 9233568da3 | |||
| fb075b60b5 | |||
| 2c4c845cd2 | |||
| 2caabbe939 | |||
| 771f9d2b5f | |||
| 647c66e70a | |||
| b205b5aad0 | |||
| 4ab0d9422d | |||
| 38b80d96ea | |||
| 35edd8ea92 | |||
| f0cb041885 | |||
| fdd1b3f18e | |||
| 6e870cdccc | |||
| 194f517be8 | |||
| 11fd1c935e | |||
| 47e1111908 | |||
| c5b812eb7b | |||
| 53be550e8a | |||
| d0d693a6ef | |||
| 1b8c45d162 | |||
| 19ce519836 | |||
| 3e0d5de7a3 | |||
| 0336774e8c | |||
| 756dd449cc | |||
| a0b4665080 | |||
| 6592c1ad6b | |||
| f109e3207c | |||
| b54fe66809 | |||
| 93c9d43497 | |||
| e3b3c40c28 | |||
| 068790fc19 | |||
| 41d99c517f | |||
| 3db3f9884f | |||
| ad1d349a0c | |||
| 7ee4a731af | |||
| 3c822a45ac | |||
| c2311e316c | |||
| ed20771194 | |||
| beb8c80787 | |||
| fc4bdf3ab0 | |||
| 6bc819e198 | |||
| 08640b27d5 | |||
| 4be993e055 | |||
| 9df12177eb | |||
| a78d1c79da | |||
| 1ea3953271 | |||
| fe8b7a907e | |||
| a15cdb1655 | |||
| fa9697b972 | |||
| 8e28e41b02 | |||
| e2ee74428a | |||
| 863a5b3648 | |||
| 91f5522ce0 | |||
| 8212f16aa2 | |||
| b1e73fafdf | |||
| f6404290ba | |||
| c29ca0e313 | |||
| 02a074ed26 | |||
| 00dabc1d3c | |||
| b68ada396a | |||
| e299e821a6 | |||
| 0f17dfc7d6 | |||
| 7c80d6b86d | |||
| f6f63b5282 | |||
| 309fdbb9fc | |||
| e2d862f2f6 | |||
| 4834e934f2 | |||
| dd4fb660c1 | |||
| 2ba893bc79 | |||
| 7a307f8e74 | |||
| 7310e13499 | |||
| bf6d286735 | |||
| ca919c2ff8 | |||
| 5948786b15 | |||
| f4746a7306 | |||
| 648b9a8f6f | |||
| 66b8483791 | |||
| e0ee28c013 | |||
| d862d86f90 | |||
| d25c032171 | |||
| 8429c6b0ab | |||
| 6bc4b35e38 | |||
| ee3486af66 | |||
| 45d486b919 | |||
| 688b73b7ae | |||
| 6d97f8b458 | |||
| f8e50947f2 | |||
| 9a637ce95e | |||
| d3a85b049b | |||
| 5bcb37901c | |||
| 9c8a7dfffe | |||
| 74e4032ffc | |||
| fd1fee852c | |||
| c51a3270da | |||
| 45fa641d26 | |||
| fd8f25916b | |||
| 826e4d654f | |||
| d9d17f0d69 | |||
| eaaeb68474 | |||
| 382885f144 | |||
| 0f7b047b0a | |||
| a074773e6c | |||
| 3d0126be0b | |||
| eda2e2f3f5 | |||
| 111e3f2106 | |||
| c1ec77a42c | |||
| 3339d9d3d7 | |||
| 8d80e9103f | |||
| d55b410800 | |||
| 9fe99ce7fa | |||
| b8df6c31a0 | |||
| abdd5876a1 | |||
| 1b5edfc00a | |||
| 347740dce1 | |||
| ab85d4d678 | |||
| 31ac62f410 | |||
| 97765b93a9 | |||
| f858ad5f26 | |||
| e6524f925f | |||
| c493f8921e | |||
| b2ce98da87 | |||
| 4db138d4b8 | |||
| f12d5131b0 | |||
| 1f5c8ce353 | |||
| 645a817685 | |||
| f4f8342245 | |||
| 543a690bfd | |||
| c4e5dad549 | |||
| fa12c23d79 | |||
| 4fe902a5d2 | |||
| 61af08a721 | |||
| b04550e2f2 | |||
| 5e5cb056c5 | |||
| 48fa64f8ec | |||
| f274394f0e | |||
| 9a4a942cc4 | |||
| 6b3b8e8c9b | |||
| 5a7d6f8997 | |||
| b4ed17fbac | |||
| 57476f4d59 | |||
| 7660910232 | |||
| b054506e3a | |||
| c961c6db7d | |||
| 0f126d4d11 | |||
| b94756bbf0 | |||
| db6ae707ef | |||
| 97feec8ebf | |||
| bce21a9f91 | |||
| 06e256e5ef | |||
| 040938ff6d | |||
| 21da7f77f5 | |||
| c2d2f31438 | |||
| 5b2558bf92 | |||
| ecd59009fb | |||
| 4a943b722d | |||
| 9d1b1e19d4 | |||
| 7a8061fc72 | |||
| bca84bac8b | |||
| 66e0eb1bd6 | |||
| 26d189917e | |||
| ccd3955bf4 | |||
| 8a2cc3a344 | |||
| 647fa025f8 | |||
| 1955350318 | |||
| 04916a57d2 | |||
| 0adac5c6f7 | |||
| 54b5be5e7d | |||
| eae4cb1417 | |||
| 420a3d4d95 | |||
| b48ad6dbfb | |||
| 2be5051b19 | |||
| c544dadd55 | |||
| 1da65d97d8 | |||
| c595c80356 | |||
| 20e5fbcd7a | |||
| ac77154907 | |||
| 97ace53bc9 | |||
| feb962f98a | |||
| 8602089a1e | |||
| 4b0648f3d7 | |||
| 856b96cbf8 | |||
| facf38955c | |||
| cfdb890eae | |||
| 2f8d3a7958 | |||
| 59311deb57 | |||
| d2541a173a | |||
| b618c11b80 |
@@ -1,7 +1,7 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
image: mcr.microsoft.com/devcontainers/go
|
||||
image: mcr.microsoft.com/devcontainers/go:1.23
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
command: sleep infinity
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
ports:
|
||||
- 5432:5432
|
||||
apprise:
|
||||
image: caronc/apprise:latest
|
||||
image: caronc/apprise:1.0
|
||||
restart: unless-stopped
|
||||
hostname: apprise
|
||||
volumes:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature request
|
||||
labels: wishlist
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
Do you follow the guidelines?
|
||||
|
||||
- [ ] I have tested my changes
|
||||
- [ ] There is no breaking changes
|
||||
- [ ] I really tested my changes and there is no regression
|
||||
- [ ] Ideally, my commit messages follow the [Conventional Commits specification](https://www.conventionalcommits.org/)
|
||||
- [ ] I read this document: https://miniflux.app/faq.html#pull-request
|
||||
|
||||
@@ -12,10 +12,13 @@ jobs:
|
||||
- name: Set up Golang
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22"
|
||||
go-version: "1.23.x"
|
||||
check-latest: true
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Compile binaries
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
run: make build
|
||||
- name: Upload binaries
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -29,6 +29,10 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23.x"
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: Debian Packages
|
||||
permissions: read-all
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
@@ -28,8 +29,34 @@ jobs:
|
||||
run: make debian-packages
|
||||
- name: List generated files
|
||||
run: ls -l *.deb
|
||||
build-packages-manually:
|
||||
if: github.event_name != 'pull_request' && github.event_name != 'push'
|
||||
name: Build Packages Manually
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
id: buildx
|
||||
with:
|
||||
install: true
|
||||
- name: Available Docker Platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
- name: Build Debian Packages
|
||||
run: make debian-packages
|
||||
- name: Upload package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: packages
|
||||
path: "*.deb"
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
publish-packages:
|
||||
if: ${{ ! github.event.pull_request }}
|
||||
if: github.event_name == 'push'
|
||||
name: Publish Packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -8,35 +8,8 @@ on:
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
test-docker-images:
|
||||
if: github.event.pull_request
|
||||
name: Test Images
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Build Alpine image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./packaging/docker/alpine/Dockerfile
|
||||
push: false
|
||||
tags: ${{ github.repository_owner }}/miniflux:alpine-dev
|
||||
- name: Test Alpine Docker image
|
||||
run: docker run --rm ${{ github.repository_owner }}/miniflux:alpine-dev miniflux -i
|
||||
- name: Build Distroless image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./packaging/docker/distroless/Dockerfile
|
||||
push: false
|
||||
tags: ${{ github.repository_owner }}/miniflux:distroless-dev
|
||||
- name: Test Distroless Docker image
|
||||
run: docker run --rm ${{ github.repository_owner }}/miniflux:distroless-dev miniflux -i
|
||||
|
||||
publish-docker-images:
|
||||
if: ${{ ! github.event.pull_request }}
|
||||
name: Publish Images
|
||||
docker-images:
|
||||
name: Docker Images
|
||||
permissions:
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
@@ -46,33 +19,33 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate Alpine Docker tag
|
||||
id: docker_alpine_tag
|
||||
run: |
|
||||
DOCKER_IMAGE=${{ github.repository_owner }}/miniflux
|
||||
DOCKER_VERSION=dev
|
||||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
DOCKER_VERSION=nightly
|
||||
TAGS="docker.io/${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},quay.io/${DOCKER_IMAGE}:${DOCKER_VERSION}"
|
||||
elif [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
DOCKER_VERSION=${GITHUB_REF#refs/tags/}
|
||||
TAGS="docker.io/${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},quay.io/${DOCKER_IMAGE}:${DOCKER_VERSION},docker.io/${DOCKER_IMAGE}:latest,ghcr.io/${DOCKER_IMAGE}:latest,quay.io/${DOCKER_IMAGE}:latest"
|
||||
fi
|
||||
echo ::set-output name=tags::${TAGS}
|
||||
- name: Generate Alpine Docker tags
|
||||
id: docker_alpine_tags
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
docker.io/${{ github.repository_owner }}/miniflux
|
||||
ghcr.io/${{ github.repository_owner }}/miniflux
|
||||
quay.io/${{ github.repository_owner }}/miniflux
|
||||
tags: |
|
||||
type=ref,event=pr
|
||||
type=schedule,pattern=nightly
|
||||
type=semver,pattern={{raw}}
|
||||
|
||||
- name: Generate Distroless Docker tag
|
||||
id: docker_distroless_tag
|
||||
run: |
|
||||
DOCKER_IMAGE=${{ github.repository_owner }}/miniflux
|
||||
DOCKER_VERSION=dev-distroless
|
||||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
DOCKER_VERSION=nightly-distroless
|
||||
TAGS="docker.io/${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},quay.io/${DOCKER_IMAGE}:${DOCKER_VERSION}"
|
||||
elif [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
DOCKER_VERSION=${GITHUB_REF#refs/tags/}-distroless
|
||||
TAGS="docker.io/${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},quay.io/${DOCKER_IMAGE}:${DOCKER_VERSION},docker.io/${DOCKER_IMAGE}:latest-distroless,ghcr.io/${DOCKER_IMAGE}:latest-distroless,quay.io/${DOCKER_IMAGE}:latest-distroless"
|
||||
fi
|
||||
echo ::set-output name=tags::${TAGS}
|
||||
- name: Generate Distroless Docker tags
|
||||
id: docker_distroless_tags
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
docker.io/${{ github.repository_owner }}/miniflux
|
||||
ghcr.io/${{ github.repository_owner }}/miniflux
|
||||
quay.io/${{ github.repository_owner }}/miniflux
|
||||
tags: |
|
||||
type=ref,event=pr
|
||||
type=schedule,pattern=nightly
|
||||
type=semver,pattern={{raw}}
|
||||
flavor: |
|
||||
suffix=-distroless,onlatest=true
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -81,12 +54,14 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: ${{ github.event_name != 'pull_request' && vars.PUBLISH_DOCKER_IMAGES == 'true' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ github.event_name != 'pull_request' && vars.PUBLISH_DOCKER_IMAGES == 'true' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -94,6 +69,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Quay Container Registry
|
||||
if: ${{ github.event_name != 'pull_request' && vars.PUBLISH_DOCKER_IMAGES == 'true' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: quay.io
|
||||
@@ -101,19 +77,21 @@ jobs:
|
||||
password: ${{ secrets.QUAY_TOKEN }}
|
||||
|
||||
- name: Build and Push Alpine images
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ vars.PUBLISH_DOCKER_IMAGES == 'true' }}
|
||||
with:
|
||||
context: .
|
||||
file: ./packaging/docker/alpine/Dockerfile
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.docker_alpine_tag.outputs.tags }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_alpine_tags.outputs.tags }}
|
||||
|
||||
- name: Build and Push Distroless images
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ vars.PUBLISH_DOCKER_IMAGES == 'true' }}
|
||||
with:
|
||||
context: .
|
||||
file: ./packaging/docker/distroless/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.docker_distroless_tag.outputs.tags }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_distroless_tags.outputs.tags }}
|
||||
|
||||
@@ -13,20 +13,31 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install jshint
|
||||
- name: Install linters
|
||||
run: |
|
||||
sudo npm install -g jshint@2.13.3
|
||||
sudo npm install -g jshint@2.13.6 eslint@8.57.0
|
||||
- name: Run jshint
|
||||
run: jshint ui/static/js/*.js
|
||||
run: jshint internal/ui/static/js/*.js
|
||||
- name: Run ESLint
|
||||
run: eslint internal/ui/static/js/*.js
|
||||
|
||||
golangci:
|
||||
name: Golang Linter
|
||||
name: Golang Linters
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22"
|
||||
- uses: golangci/golangci-lint-action@v4
|
||||
go-version: "1.23.x"
|
||||
- run: "go vet ./..."
|
||||
- uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
args: --timeout 10m --skip-dirs tests --disable errcheck --enable sqlclosecheck --enable misspell --enable gofmt --enable goimports --enable whitespace
|
||||
args: >
|
||||
--timeout 10m
|
||||
--exclude-dirs=tests
|
||||
--disable errcheck
|
||||
--enable sqlclosecheck,misspell,gofmt,goimports,whitespace,gocritic
|
||||
- uses: dominikh/staticcheck-action@v1.3.1
|
||||
with:
|
||||
version: "2024.1.1"
|
||||
install-go: false
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: RPM Packages
|
||||
permissions: read-all
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
@@ -19,8 +20,25 @@ jobs:
|
||||
run: make rpm
|
||||
- name: List generated files
|
||||
run: ls -l *.rpm
|
||||
build-package-manually:
|
||||
if: github.event_name != 'pull_request' && github.event_name != 'push'
|
||||
name: Build Packages Manually
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build RPM Package
|
||||
run: make rpm
|
||||
- name: Upload package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: packages
|
||||
path: "*.rpm"
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
publish-package:
|
||||
if: ${{ ! github.event.pull_request }}
|
||||
if: github.event_name == 'push'
|
||||
name: Publish Packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
go-version: ["1.22"]
|
||||
go-version: ["1.23.x"]
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22"
|
||||
go-version: "1.23.x"
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Postgres client
|
||||
|
||||
+5
-4
@@ -1,6 +1,7 @@
|
||||
miniflux-*
|
||||
./*.sha256
|
||||
./miniflux
|
||||
*.rpm
|
||||
*.deb
|
||||
.idea
|
||||
.vscode
|
||||
.vscode
|
||||
*.deb
|
||||
*.rpm
|
||||
miniflux-*
|
||||
@@ -1,3 +1,370 @@
|
||||
Version 2.2.3 (November 10, 2024)
|
||||
---------------------------------
|
||||
|
||||
* fix: unable to change password due to a typo in SQL parameter
|
||||
* fix: show only one player when there are several audio/video enclosures
|
||||
* feat(mediaproxy): pass original filename in `Content-Disposition` header
|
||||
* feat(mediaproxy): implement referer spoofing for restricted media resources
|
||||
* feat(integration): update Shiori integration to use new API endpoints for login/bookmark
|
||||
* build(deps): bump `golang.org/x/text` from `0.19.0` to `0.20.0`
|
||||
* build(deps): bump `golang.org/x/term` from `0.25.0` to `0.26.0`
|
||||
* build(deps): bump `golang.org/x/oauth2` from `0.23.0` to `0.24.0`
|
||||
* build(deps): bump `golang.org/x/net` from `0.30.0` to `0.31.0`
|
||||
* build(deps): bump `golang.org/x/crypto` from `0.28.0` to `0.29.0`
|
||||
|
||||
Version 2.2.2 (October 29, 2024)
|
||||
--------------------------------
|
||||
|
||||
* fix(webauthn): add backup eligibility flag workaround to avoid a 401 response
|
||||
* fix: update `Last-Modified` if it changes in a 304 response
|
||||
* feat(webauthn): show help message regarding username and non-discoverable credentials
|
||||
* feat(rss): calculate hash based on item title/content for feeds without GUID and link
|
||||
* feat(locale): update Chinese translations
|
||||
* feat(locale): update Polish translations
|
||||
* feat(integration): add Cubox integration
|
||||
* feat(client): add `custom_js` field to Go API client
|
||||
* feat(api): add endpoint for user integration status
|
||||
* feat: update feed icon during force refresh
|
||||
* feat: take `Retry-After` header into consideration for rate limited feeds
|
||||
* feat: set entry URL to rewritten URL if a rewrite rule is defined
|
||||
* feat: replace `xurls` third-party module with an ad-hoc regexp
|
||||
* feat: add new settings option to allow external fonts
|
||||
* feat: add custom user JavaScript similar to custom CSS
|
||||
* chore: update test case comment
|
||||
* build(deps): bump `golang.org/x/net` from `0.29.0` to `0.30.0`
|
||||
* build(deps): bump `github.com/yuin/goldmark` from `1.7.4` to `1.7.8`
|
||||
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.20.37` to `2.21.1`
|
||||
* build(deps): bump `github.com/prometheus/client_golang`
|
||||
* build(deps): bump `github.com/andybalholm/brotli` from `1.1.0` to `1.1.1`
|
||||
|
||||
Version 2.2.1 (September 28, 2024)
|
||||
----------------------------------
|
||||
|
||||
* refactor: split processor package into smaller files
|
||||
* fix(mediaproxy): forward client user-agent to origin to bypass bot protection
|
||||
* fix: use root URL to generate absolute proxy URL
|
||||
* fix: remove progression save on shared entry
|
||||
* fix: add datasource variable and upgrade depecrated panels on the Grafana dashboard
|
||||
* feat(locale): update zh_CN translations
|
||||
* feat(locale): update Ukrainian translations
|
||||
* feat(locale): update Spanish translations
|
||||
* feat(locale): update Dutch translations
|
||||
* feat: use Bilibili API instead of web scraping to get videos watch time
|
||||
* feat: add pagination to shared entries listing
|
||||
* feat: add button to show only starred entries per category
|
||||
* build(deps): bump `golang.org/x/term` from `0.23.0` to `0.24.0`
|
||||
* build(deps): bump `golang.org/x/oauth2` from `0.22.0` to `0.23.0`
|
||||
* build(deps): bump `golang.org/x/net` from `0.28.0` to `0.29.0`
|
||||
* build(deps): bump `github.com/PuerkitoBio/goquery` from `1.9.2` to `1.10.0`
|
||||
* build(deps): bump `github.com/prometheus/client_golang` from `1.20.3` to `1.20.4`
|
||||
* build(deps): bump `github.com/go-webauthn/webauthn` from `0.10.2` to `0.11.2`
|
||||
* build: update go.mod to Go 1.23
|
||||
* build: bump devcontainer version to go 1.23
|
||||
|
||||
Version 2.2.0 (August 18, 2024)
|
||||
-------------------------------
|
||||
|
||||
* refactor: simplify Youtube feeds discovery
|
||||
* fix(integration): define content encoding explicitly when sending article body to Readeck
|
||||
* fix(fever): correct sorting direction when using `max_id` argument
|
||||
* fix(client): Return `nil` and error if endpoint is an empty string
|
||||
* fix: video poster image URL is encoded twice when using `MEDIA_PROXY_MODE=all`
|
||||
* fix: use `BASE_URL` instead of `r.Host` to generate absolute media proxy URL
|
||||
* fix: panic during YouTube channel feed discovery
|
||||
* fix: honor `hide_globally` when creating a new feed through the api
|
||||
* fix: align pagination correctly on small screens with non-English text
|
||||
* fix: `store.GetEnclosure()` should return `nil` if no rows are returned
|
||||
* feat(locale): update Turkish translations
|
||||
* feat(locale): update French translations
|
||||
* feat(locale): update Chinese` translations
|
||||
* feat(integration): add ntfy integration
|
||||
* feat(api): add API routes `/v1/enclosures/{enclosureID}`
|
||||
* feat: validate `OAUTH2_PROVIDER` config option value
|
||||
* feat: remove YouTube video page subscription finder because `meta[itemprop="channelId"]` no longer exists
|
||||
* feat: remove well-known URL parameter trackers
|
||||
* feat: mark media as read when playback reaches 90%
|
||||
* feat: change log level to info when running migrations
|
||||
* feat: allow customizing the display name of the OpenID Connect provider
|
||||
* feat: add support for `base` HTML element when discovering feeds
|
||||
* feat: add support for `aside` HTML element in entry content
|
||||
* feat: Add option to disable local auth form
|
||||
* feat: add license info to Javascript files for LibreJS compatibility
|
||||
* feat: add `FETCH_BILIBILI_WATCH_TIME` config option
|
||||
* docs: update links to filtering rules
|
||||
* chore: avoid using legacy key/value format in Dockerfile
|
||||
* build(deps): bump `golang.org/x/oauth2` from `0.21.0` to `0.22.0`
|
||||
* build(deps): bump `golang.org/x/net` from `0.27.0` to `0.28.0`
|
||||
* build(deps): bump `golang.org/x/crypto` from `0.25.0` to `0.26.0`
|
||||
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.20.36` to `2.20.37`
|
||||
* build(deps): bump `github.com/prometheus/client_golang`
|
||||
* build: update GitHub Actions to Go 1.23
|
||||
* build: publish OCI images only if `PUBLISH_DOCKER_IMAGES=true`
|
||||
* build: bump Alpine Linux build image to v3.20
|
||||
* build: add sha256 checksum file for published binaries
|
||||
|
||||
Version 2.1.4 (July 9, 2024)
|
||||
----------------------------
|
||||
|
||||
* test: add unit tests for `IsModified()` behaviour
|
||||
* refactor: improve YouTube page feed detection
|
||||
* fix(ui): settings form is not populated correctly after validation errors
|
||||
* fix(ui): playback speed indicator precision
|
||||
* fix(ui): playback speed indicator on shared entries
|
||||
* fix(integration): preserve existing Pinboard bookmarks
|
||||
* fix(googlereader): set `CrawlTimeMsec` to the correct precision
|
||||
* fix(build): failed to solve container image `arm64v8/golang:1.22-bookworm`
|
||||
* fix(build): add `distroless` suffix on `latest` tag in GitHub workflow
|
||||
* fix: use `ETag` as a stronger validator than `Last-Modified`
|
||||
* fix: update `theverge.com` rewrite rule to avoid duplicate image
|
||||
* fix: incorrect Go package comment `reader/readingtime`
|
||||
* fix: error out for improper rewrite regexp when processing feed entries
|
||||
* fix: ensures that session cookies are not expiring before the session is cleaned up from the database as per `CLEANUP_REMOVE_SESSIONS_DAYS`
|
||||
* fix: `<img>` aspect ratio with `height: auto`
|
||||
* feat(ui): add `viewport-fit=cover`
|
||||
* feat(sanitizer): add support for HTML hidden attribute
|
||||
* feat(locale): update French translations
|
||||
* feat(integration): add Raindrop integration
|
||||
* feat(integration): add feed name to Telegram message
|
||||
* feat(integration): add Betula integration
|
||||
* feat: use of insecure TLS ciphers when "Allow self-signed or invalid certificates" is enabled to workaround some broken websites
|
||||
* feat: discover feeds from a Youtube playlist pages
|
||||
* feat: add navigation to last/first page
|
||||
* feat: add global block and keep filters
|
||||
* feat: add description field to feed settings
|
||||
* feat: add `pitchfork.com` scraping rule
|
||||
* feat: add `FETCH_NEBULA_WATCH_TIME` config option
|
||||
* Bump `github.com/PuerkitoBio/goquery` from` 1.9.1` to` 1.9.2`
|
||||
* Bump `github.com/prometheus/client_golang` from `1.19.0` to `1.19.1`
|
||||
* build(deps): bump `library/alpine` in `/packaging/docker/alpine`
|
||||
* build(deps): bump `golangci/golangci-lint-action` from `4` to `6`
|
||||
* build(deps): bump `golang.org/x/term` from `0.19.0` to `0.22.0`
|
||||
* build(deps): bump `golang.org/x/oauth2` from `0.19.0` to `0.21.0`
|
||||
* build(deps): bump `golang.org/x/net` from `0.22.0` to `0.27.0`
|
||||
* build(deps): bump `golang.org/x/crypto` from `0.24.0` to `0.25.0`
|
||||
* build(deps): bump `github.com/yuin/goldmark` from `1.7.1` to `1.7.4`
|
||||
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.20.20` to `2.20.36`
|
||||
* build(deps): bump `github.com/coreos/go-oidc/v3` from `3.10.0` to `3.11.0`
|
||||
* build(deps): bump `docker/build-push-action` from `5` to `6`
|
||||
|
||||
Version 2.1.3 (April 27, 2024)
|
||||
------------------------------
|
||||
|
||||
* `api`: `rand.Intn(math.MaxInt64)` causes tests to fail on 32-bit architectures (use `rand.Int()` instead)
|
||||
* `ci`: use `docker/metadata-action` instead of deprecated shell-scripts
|
||||
* `database`: remove `entries_feed_url_idx` index because entry URLs can exceeds btree index size limit
|
||||
* `finder`: find feeds from YouTube playlist
|
||||
* `http/response`: add brotli compression support
|
||||
* `integration/matrix`: fix function name in comment
|
||||
* `packaging`: specify container registry explicitly (e.g., Podman does not use `docker.io` by default)
|
||||
* `packaging`: use `make miniflux` instead of duplicating `go build` arguments (this leverages Go's PIE build mode)
|
||||
* `reader/fetcher`: add brotli content encoding support
|
||||
* `reader/processor`: minimize feed entries HTML content
|
||||
* `reader/rewrite`: add a rule for `oglaf.com`
|
||||
* `storage`: change `GetReadTime()` function to use `entries_feed_id_hash_key` index
|
||||
* `ui`: add seek and speed controls to media player
|
||||
* `ui`: add tag entries page
|
||||
* `ui`: fix JavaScript error when clicking on unread counter
|
||||
* `ui`: use `FORCE_REFRESH_INTERVAL` config for category refresh
|
||||
* Bump `github.com/tdewolff/minify/v2` from `2.20.19` to `2.20.20`
|
||||
* Bump `golang.org/x/net` from `0.22.0` to `0.24.0`
|
||||
* Bump `golang.org/x/term` from `0.18.0` to `0.19.0`
|
||||
* Bump `golang.org/x/oauth2` from `0.18.0` to `0.19.0`
|
||||
* Bump `github.com/yuin/goldmark` from `1.7.0` to `1.7.1`
|
||||
|
||||
Version 2.1.2 (March 30, 2024)
|
||||
------------------------------
|
||||
|
||||
* `api`: rewrite API integration tests without build tags
|
||||
* `ci`: add basic ESLinter checks
|
||||
* `ci`: enable go-critic linter and fix various issues detected
|
||||
* `ci`: fix JavaScript linter path in GitHub Actions
|
||||
* `cli`: avoid misleading error message when creating an admin user automatically
|
||||
* `config`: add `FILTER_ENTRY_MAX_AGE_DAYS` option
|
||||
* `config`: bump the number of simultaneous workers
|
||||
* `config`: rename `PROXY_*` options to `MEDIA_PROXY_*`
|
||||
* `config`: use `crypto.GenerateRandomBytes` instead of doing it by hand
|
||||
* `http/request`: refactor conditions to be more idiomatic
|
||||
* `http/response`: remove legacy `X-XSS-Protection` header
|
||||
* `integration/rssbrige`: fix rssbrige import
|
||||
* `integration/shaarli`: factorize the header+payload concatenation as data
|
||||
* `integration/shaarli`: no need to base64-encode then remove the padding when we can simply encode without padding
|
||||
* `integration/shaarli`: the JWT token was declared as using HS256 as algorithm, but was using HS512
|
||||
* `integration/webhook`: add category title to request body
|
||||
* `locale`: update Turkish translations
|
||||
* `man page`: sort config options in alphabetical order
|
||||
* `mediaproxy`: reduce the internal indentation of `ProxifiedUrl` by inverting some conditions
|
||||
* `mediaproxy`: simplify and refactor the package
|
||||
* `model`: replace` Optional{Int,Int64,Float64}` with a generic function `OptionalNumber()`
|
||||
* `model`: use struct embedding for `FeedCreationRequestFromSubscriptionDiscovery` to reduce code duplication
|
||||
* `reader/atom`: avoid debug message when the date is empty
|
||||
* `reader/atom`: change `if !a { a = } if !a {a = }` constructs into `if !a { a = ; if !a {a = }}` to reduce the number of comparisons and improve readability
|
||||
* `reader/atom`: Move the population of the feed's entries into a new function, to make BuildFeed easier to understand/separate concerns/implementation details
|
||||
* `reader/atom`: refactor Atom parser to use an adapter
|
||||
* `reader/atom`: use `sort+compact` instead of `compact+sort` to remove duplicates
|
||||
* `reader/atom`: when detecting the format, detect its version as well
|
||||
* `reader/encoding`: inline a one-liner function
|
||||
* `reader/handler`: fix force refresh feature
|
||||
* `reader/json`: refactor JSON Feed parser to use an adapter
|
||||
* `reader/media`: remove a superfluous error-check: `strconv.ParseInt` returns `0` when passed an empty string
|
||||
* `reader/media`: simplify switch-case by moving a common condition above it
|
||||
* `reader/processor`: compile block/keep regex only once per feed
|
||||
* `reader/rdf`: refactor RDF parser to use an adapter
|
||||
* `reader/rewrite`: inline some one-line functions
|
||||
* `reader/rewrite`: simplify `removeClickbait`
|
||||
* `reader/rewrite`: transform a free-standing function into a method
|
||||
* `reader/rewrite`: use a proper constant instead of a magic number in `applyFuncOnTextContent`
|
||||
* `reader/rss`: add support for `<media:category>` element
|
||||
* `reader/rss`: don't add empty tags to RSS items
|
||||
* `reader/rss`: refactor RSS parser to use a default namespace to avoid some limitations of the Go XML parser
|
||||
* `reader/rss`: refactor RSS Parser to use an adapter
|
||||
* `reader/rss`: remove some duplicated code in RSS parser
|
||||
* `reader`: ensure that enclosure URLs are always absolute
|
||||
* `reader`: move iTunes and GooglePlay XML definitions to their own packages
|
||||
* `reader`: parse podcast categories
|
||||
* `reader`: remove trailing space in `SiteURL` and `FeedURL`
|
||||
* `storage`: do not store empty tags
|
||||
* `storage`: simplify `removeDuplicates()` to use a `sort`+`compact` construct instead of doing it by hand with a hashmap
|
||||
* `storage`: Use plain strings concatenation instead of building an array and then joining it
|
||||
* `timezone`: make sure the tests pass when the timezone database is not installed on the host
|
||||
* `ui/css`: align `min-width` with the other `min-width` values
|
||||
* `ui/css`: fix regression: "Add to Home Screen" button is unreadable
|
||||
* `ui/js`: don't use lambdas to return a function, use directly the function instead
|
||||
* `ui/js`: enable trusted-types
|
||||
* `ui/js`: fix download button loading label
|
||||
* `ui/js`: fix JavaScript error on the login page when the user not authenticated
|
||||
* `ui/js`: inline one-line functions
|
||||
* `ui/js`: inline some `querySelectorAll` calls
|
||||
* `ui/js`: reduce the scope of some variables
|
||||
* `ui/js`: remove a hack for "Chrome 67 and earlier" since it was released in 2018
|
||||
* `ui/js`: replace `DomHelper.findParent` with `.closest`
|
||||
* `ui/js`: replace `let` with `const`
|
||||
* `ui/js`: simplify `DomHelper.getVisibleElements` by using a `filter` instead of a loop with an index
|
||||
* `ui/js`: use a `Set` instead of an array in a `KeyboardHandler`'s member
|
||||
* `ui/js`: use some ternaries where it makes sense
|
||||
* `ui/static`: make use of `HashFromBytes` everywhere
|
||||
* `ui/static`: set minifier ECMAScript version
|
||||
* `ui`: add keyboard shortcuts for scrolling to top/bottom of the item list
|
||||
* `ui`: add media player control playback speed
|
||||
* `ui`: remove unused variables and improve JSON decoding in `saveEnclosureProgression()`
|
||||
* `validator`: display an error message on edit feed page when the feed URL is not unique
|
||||
* Bump `github.com/coreos/go-oidc/v3` from `3.9.0` to `3.10.0`
|
||||
* Bump `github.com/go-webauthn/webauthn` from `0.10.1` to `0.10.2`
|
||||
* Bump `github.com/tdewolff/minify/v2` from `2.20.18` to `2.20.19`
|
||||
* Bump `google.golang.org/protobuf` from `1.32.0` to `1.33.0`
|
||||
|
||||
Version 2.1.1 (March 10, 2024)
|
||||
-----------------------------
|
||||
|
||||
* Move search form to a dedicated page
|
||||
* Add Readeck integration
|
||||
* Add feed option to disable HTTP/2 to avoid fingerprinting
|
||||
* Add `Enter` key as a hotkey to open selected item
|
||||
* Proxify `video` element `poster` attribute
|
||||
* Add a couple of new possible locations for feeds
|
||||
* Hugo likes to generate `index.xml`
|
||||
* `feed.atom` and `feed.rss` are used by enterprise-scale/old-school gigantic CMS
|
||||
* Fix categories import from Thunderbird's OPML
|
||||
* Fix logo misalignment when using languages that are more verbose than English
|
||||
* Google Reader: Do not return a 500 error when no items is returned
|
||||
* Handle RDF feeds with duplicated `<title>` elements
|
||||
* Sort integrations alphabetically
|
||||
* Add more URL validation in media proxy
|
||||
* Add unit test to ensure each translation has the correct number of plurals
|
||||
* Add missing plurals for some languages
|
||||
* Makefile: quiet `git describe` and `rev-parse` stderr: When building from a tarball instead of a cloned git repo, there would be two `fatal: not a git repository` errors emitted even though the build succeeds. This is because of how `VERSION` and `COMMIT` are set in the Makefile. This PR suppresses the stderr for these variable assignments.
|
||||
* Makefile: do not force `CGO_ENABLED=0` for `miniflux` target
|
||||
* Add GitHub Action pipeline to build packages on-demand
|
||||
* Remove Golint (deprecated), use `staticcheck` and `golangci-lint` instead
|
||||
* Build amd64/arm64 Debian packages with CGO disabled
|
||||
* Update `go.mod` and add `.exe` suffix to Windows binary
|
||||
* Add a couple of fuzzers
|
||||
* Fix CodeQL workflow
|
||||
* Code and performance improvements:
|
||||
* Use an `io.ReadSeeker` instead of an `io.Reader` to parse feeds
|
||||
* Speed up the sanitizer:
|
||||
- Allow Youtube URLs to start with `www`
|
||||
- Use `strings.Builder` instead of a `bytes.Buffer`
|
||||
- Use a `strings.NewReader` instead of a `bytes.NewBufferString`
|
||||
- Sprinkles a couple of `continue` to make the code-flow more obvious
|
||||
- Inline calls to `inList`, and put their parameters in the right order
|
||||
- Simplify `isPixelTracker`
|
||||
- Simplify `isValidIframeSource`, by extracting the hostname and comparing it directly, instead of using the full url and checking if it starts with multiple variations of the same one (`//`, `http:`, `https://` multiplied by `/www.`)
|
||||
- Add a benchmark
|
||||
- Instead of having to allocate a ~100 keys map containing possibly dynamic values (at least to the go compiler), allocate it once in a global variable. This significantly speeds things up, by reducing the garbage
|
||||
- Use constant time access for maps instead of iterating on them
|
||||
- Build a ~large whitelist map inline instead of constructing it item by item (and remove a duplicate key/value pair)
|
||||
- Use `slices` instead of hand-rolled loops
|
||||
collector/allocator involvements.
|
||||
* Reuse a `Reader` instead of copying to a buffer when parsing an Atom feed
|
||||
* Preallocate memory when exporting to OPML: This should marginally increase performance when exporting a large amount of feeds to OPML
|
||||
* Delay call of `view.New` after logging the user in: There is no need to do extra work like creating a session and its associated view until the user has been properly identified and as many possibly-failing sql request have been successfully run
|
||||
* Use constant-time comparison for anti-csrf tokens: This is probably completely overkill, but since anti-csrf tokens are secrets, they should be compared against untrusted inputs in constant time
|
||||
* Simplify and optimize `genericProxyRewriter`
|
||||
- Reduce the amount of nested loops: it's preferable to search the whole page once and filter on it (even with filters that should always be false), than searching it again for every element we're looking for.
|
||||
- Factorize the proxying conditions into a `shouldProxy` function to reduce the copy-pasta.
|
||||
* Speed up `removeUnlikelyCandidates`: `.Not` returns a brand new `Selection`, copied element by element
|
||||
* Improve `EstimateReadingTime`'s speed by a factor 7
|
||||
- Refactorise the tests and add some
|
||||
- Use 250 signs instead of the whole text
|
||||
- Only check for Korean, Chinese and Japanese script
|
||||
- Add a benchmark
|
||||
- Use a more idiomatic control flow
|
||||
* Don't compute reading-time when unused: If the user doesn't display reading times, there is no need to compute them. This should speed things up a bit, since `whatlanggo.Detect` is abysmally slow.
|
||||
* Simplify `username` generation for the integration tests: No need to generate random numbers 10 times, generate a single big-enough one. A single int64 should be more than enough
|
||||
* Add missing regex anchor detected by CodeQL
|
||||
* Don't mix up slices capacity and length
|
||||
* Use prepared statements for intervals, `ArchiveEntries` and `updateEnclosures`
|
||||
* Use modern for-loops introduced with Go 1.22
|
||||
* Remove a superfluous condition: No need to check if the length of `line` is positive since we're checking afterwards that it contains the `=` sign
|
||||
* Close resources as soon as possible, instead of using `defer()` in a loop
|
||||
* Remove superfluous escaping in a regex
|
||||
* Use `strings.ReplaceAll` instead of `strings.Replace(…, -1)`
|
||||
* Use `strings.EqualFold` instead of `strings.ToLower(…) ==`
|
||||
* Use `.WriteString(` instead of `.Write([]byte(…`
|
||||
* Use `%q` instead of `"%s"`
|
||||
* Make `internal/worker/worker.go` read-only
|
||||
* Use a switch-case construct in `internal/locale/plural.go` instead of an avalanche of `if`
|
||||
* Template functions: simplify `formatFileSize` and `duration` implementation
|
||||
* Inline some templating functions
|
||||
* Make use of `printer.Print` when possible
|
||||
* Add a `printer.Print` to `internal/locale/printer.go`: No need to use variadic functions with string format interpolation to generate static strings
|
||||
* Minor code simplification in `internal/ui/view/view.go`: No need to create the map item by item when we can create it in one go
|
||||
* Build the map inline in `CountAllFeeds()`: No need to build an empty map to then add more fields in it one by one
|
||||
* Miscellaneous improvements to `internal/reader/subscription/finder.go`:
|
||||
- Surface `localizedError` in `FindSubscriptionsFromWellKnownURLs` via `slog`
|
||||
- Use an inline declaration for new subscriptions, like done elsewhere in the
|
||||
file, if only for consistency's sake
|
||||
- Preallocate the `subscriptions` slice when using an RSS-bridge,
|
||||
* Use an update-where for `MarkCategoryAsRead` instead of a subquery
|
||||
* Simplify `CleanOldUserSessions`' query: No need for a subquery, filtering on `created_at` directly is enough
|
||||
* Simplify `cleanupEntries`' query
|
||||
- `NOT (hash=ANY(%4))` can be expressed as `hash NOT IN $4`
|
||||
- There is no need for a subquery operating on the same table, moving the conditions out is equivalent.
|
||||
* Reformat `ArchiveEntries`'s query for consistency's sake and replace the `=ANY` with an `IN`
|
||||
* Reformat the query in `GetEntryIDs` and `GetReadTime`'s query for consistency's sake
|
||||
* Simplify `WeeklyFeedEntryCount`: No need for a `BETWEEN`: we want to filter on entries published in the last week, no need to express is as "entries published between now and last week", "entries published after last week" is enough
|
||||
* Add some tests for `add_image_title`
|
||||
* Remove `github.com/google/uuid` dependencies: Replace it with a hand-rolled implementation. Heck, an UUID isn't even a requirement according to Omnivore API docs
|
||||
* Simplify `internal/reader/icon/finder.go`:
|
||||
- Use a simple regex to parse data uri instead of a hand-rolled parser, and document what fields are considered mandatory.
|
||||
- Use case-insensitive matching to find (fav)icons, instead of doing the same query twice with different letter cases
|
||||
- Add `apple-touch-icon-precomposed.png` as a fallback `favicon`
|
||||
- Reorder the queries to have `icon` first, since it seems to be the most popular one. It used to be last, meaning that pages had to be parsed completely 4 times, instead of one now.
|
||||
- Minor factorisation in `findIconURLsFromHTMLDocument`
|
||||
* Small refactoring of `internal/reader/date/parser.go`:
|
||||
- Split dates formats into those that require local times and those who don't, so that there is no need to have a switch-case in the for loop with around 250 iterations at most.
|
||||
- Be more strict when it comes to timezones, previously invalid ones like -13 were accepted. Also add a test for this.
|
||||
- Bail out early if the date is an empty string.
|
||||
* Make use of Go ≥ 1.21 slices package instead of hand-rolled loops
|
||||
* Reorder the fields of the `Entry` struct to save some memory
|
||||
* Dependencies update:
|
||||
* Bump `golang.org/x/oauth2` from `0.17.0` to `0.18.0`
|
||||
* Bump `github.com/prometheus/client_golang` from `1.18.0` to `1.19.0`
|
||||
* Bump `github.com/tdewolff/minify/v2` from `2.20.16` to `2.20.18`
|
||||
* Bump `github.com/PuerkitoBio/goquery` from `1.8.1` to `1.9.1`
|
||||
* Bump `golang.org/x/crypto` from `0.19.0` to `0.20.0`
|
||||
* Bump `github.com/go-jose/go-jose/v3` from `3.0.1` to `3.0.3`
|
||||
|
||||
Version 2.1.0 (February 17, 2024)
|
||||
---------------------------------
|
||||
|
||||
@@ -14,7 +381,7 @@ Version 2.1.0 (February 17, 2024)
|
||||
* Always try to use HTTP/2 when fetching feeds if available
|
||||
* Add `type` attribute in OPML export as per OPML 2.0 specs
|
||||
* Fix missing translation argument for the key `error.unable_to_parse_feed`
|
||||
* Fix Debian package builder when using Go 1.12 and `armhf` architecture
|
||||
* Fix Debian package builder when using Go 1.22 and `armhf` architecture
|
||||
* Fix typo in log message
|
||||
* Fix incorrect label shown when saving an article
|
||||
* Fix incorrect condition in refresh feeds cli
|
||||
@@ -36,7 +403,7 @@ Version 2.1.0 (February 17, 2024)
|
||||
* Add `nav` landmark to page header links
|
||||
* Limit feed/category entry pagination to unread entries when coming from unread entry list
|
||||
* Update German translation
|
||||
* Update GitHub Actions to Go 1.12
|
||||
* Update GitHub Actions to Go 1.22
|
||||
* Bump `golang.org/x/term` from `0.16.0` to `0.17.0`
|
||||
* Bump `github.com/google/uuid` from `1.5.0` to `1.6.0`
|
||||
* Bump `github.com/yuin/goldmark` from `1.6.0` to `1.7.0`
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
APP := miniflux
|
||||
DOCKER_IMAGE := miniflux/miniflux
|
||||
VERSION := $(shell git describe --tags --abbrev=0)
|
||||
COMMIT := $(shell git rev-parse --short HEAD)
|
||||
BUILD_DATE := `date +%FT%T%z`
|
||||
LD_FLAGS := "-s -w -X 'miniflux.app/v2/internal/version.Version=$(VERSION)' -X 'miniflux.app/v2/internal/version.Commit=$(COMMIT)' -X 'miniflux.app/v2/internal/version.BuildDate=$(BUILD_DATE)'"
|
||||
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
|
||||
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
|
||||
DEB_IMG_ARCH := amd64
|
||||
APP := miniflux
|
||||
DOCKER_IMAGE := miniflux/miniflux
|
||||
VERSION := $(shell git describe --tags --abbrev=0 2>/dev/null)
|
||||
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null)
|
||||
BUILD_DATE := `date +%FT%T%z`
|
||||
LD_FLAGS := "-s -w -X 'miniflux.app/v2/internal/version.Version=$(VERSION)' -X 'miniflux.app/v2/internal/version.Commit=$(COMMIT)' -X 'miniflux.app/v2/internal/version.BuildDate=$(BUILD_DATE)'"
|
||||
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
|
||||
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
|
||||
DOCKER_PLATFORM := amd64
|
||||
|
||||
export PGPASSWORD := postgres
|
||||
|
||||
@@ -44,40 +44,50 @@ export PGPASSWORD := postgres
|
||||
debian-packages
|
||||
|
||||
miniflux:
|
||||
@ CGO_ENABLED=0 go build -buildmode=pie -ldflags=$(LD_FLAGS) -o $(APP) main.go
|
||||
@ go build -buildmode=pie -ldflags=$(LD_FLAGS) -o $(APP) main.go
|
||||
|
||||
miniflux-no-pie:
|
||||
@ go build -ldflags=$(LD_FLAGS) -o $(APP) main.go
|
||||
|
||||
linux-amd64:
|
||||
@ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
||||
|
||||
linux-arm64:
|
||||
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
||||
|
||||
linux-armv7:
|
||||
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
||||
|
||||
linux-armv6:
|
||||
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
||||
|
||||
linux-armv5:
|
||||
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
||||
|
||||
darwin-amd64:
|
||||
@ GOOS=darwin GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
||||
|
||||
darwin-arm64:
|
||||
@ GOOS=darwin GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
||||
|
||||
freebsd-amd64:
|
||||
@ CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
||||
|
||||
openbsd-amd64:
|
||||
@ GOOS=openbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
||||
|
||||
windows-amd64:
|
||||
@ GOOS=windows GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
@ GOOS=windows GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@.exe main.go
|
||||
@ sha256sum $(APP)-$@.exe > $(APP)-$@.exe.sha256
|
||||
|
||||
build: linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-armv5 darwin-amd64 darwin-arm64 freebsd-amd64 openbsd-amd64 windows-amd64
|
||||
|
||||
@@ -98,19 +108,21 @@ openbsd-x86:
|
||||
@ GOOS=openbsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
|
||||
windows-x86:
|
||||
@ GOOS=windows GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
@ GOOS=windows GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@.exe main.go
|
||||
|
||||
run:
|
||||
@ LOG_DATE_TIME=1 DEBUG=1 RUN_MIGRATIONS=1 CREATE_ADMIN=1 ADMIN_USERNAME=admin ADMIN_PASSWORD=test123 go run main.go
|
||||
@ LOG_DATE_TIME=1 LOG_LEVEL=debug RUN_MIGRATIONS=1 CREATE_ADMIN=1 ADMIN_USERNAME=admin ADMIN_PASSWORD=test123 go run main.go
|
||||
|
||||
clean:
|
||||
@ rm -f $(APP)-* $(APP) $(APP)*.rpm $(APP)*.deb
|
||||
@ rm -f $(APP)-* $(APP) $(APP)*.rpm $(APP)*.deb $(APP)*.exe $(APP)*.sha256
|
||||
|
||||
test:
|
||||
go test -cover -race -count=1 ./...
|
||||
|
||||
lint:
|
||||
golint -set_exit_status ${PKG_LIST}
|
||||
go vet ./...
|
||||
staticcheck ./...
|
||||
golangci-lint run --disable errcheck --enable sqlclosecheck --enable misspell --enable gofmt --enable goimports --enable whitespace
|
||||
|
||||
integration-test:
|
||||
psql -U postgres -c 'drop database if exists miniflux_test;'
|
||||
@@ -124,9 +136,13 @@ integration-test:
|
||||
RUN_MIGRATIONS=1 \
|
||||
DEBUG=1 \
|
||||
./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
|
||||
|
||||
|
||||
while ! nc -z localhost 8080; do sleep 1; done
|
||||
go test -v -tags=integration -count=1 miniflux.app/v2/internal/tests
|
||||
|
||||
TEST_MINIFLUX_BASE_URL=http://127.0.0.1:8080 \
|
||||
TEST_MINIFLUX_ADMIN_USERNAME=admin \
|
||||
TEST_MINIFLUX_ADMIN_PASSWORD=test123 \
|
||||
go test -v -count=1 ./internal/api
|
||||
|
||||
clean-integration-test:
|
||||
@ kill -9 `cat /tmp/miniflux.pid`
|
||||
@@ -157,15 +173,15 @@ rpm: clean
|
||||
rpmbuild -bb --define "_miniflux_version $(VERSION)" /root/rpmbuild/SPECS/miniflux.spec
|
||||
|
||||
debian:
|
||||
@ docker build --load \
|
||||
--build-arg BASE_IMAGE_ARCH=$(DEB_IMG_ARCH) \
|
||||
-t $(DEB_IMG_ARCH)/miniflux-deb-builder \
|
||||
@ docker buildx build --load \
|
||||
--platform linux/$(DOCKER_PLATFORM) \
|
||||
-t miniflux-deb-builder \
|
||||
-f packaging/debian/Dockerfile \
|
||||
.
|
||||
@ docker run --rm \
|
||||
-v ${PWD}:/pkg $(DEB_IMG_ARCH)/miniflux-deb-builder
|
||||
@ docker run --rm --platform linux/$(DOCKER_PLATFORM) \
|
||||
-v ${PWD}:/pkg miniflux-deb-builder
|
||||
|
||||
debian-packages: clean
|
||||
$(MAKE) debian DEB_IMG_ARCH=amd64
|
||||
$(MAKE) debian DEB_IMG_ARCH=arm64v8
|
||||
$(MAKE) debian DEB_IMG_ARCH=arm32v7
|
||||
$(MAKE) debian DOCKER_PLATFORM=amd64
|
||||
$(MAKE) debian DOCKER_PLATFORM=arm64
|
||||
$(MAKE) debian DOCKER_PLATFORM=arm/v7
|
||||
|
||||
+97
-5
@@ -18,16 +18,44 @@ type Client struct {
|
||||
}
|
||||
|
||||
// New returns a new Miniflux client.
|
||||
// Deprecated: use NewClient instead.
|
||||
func New(endpoint string, credentials ...string) *Client {
|
||||
// Web gives "API Endpoint = https://miniflux.app/v1/", it doesn't work (/v1/v1/me)
|
||||
return NewClient(endpoint, credentials...)
|
||||
}
|
||||
|
||||
// NewClient returns a new Miniflux client.
|
||||
func NewClient(endpoint string, credentials ...string) *Client {
|
||||
// Trim trailing slashes and /v1 from the endpoint.
|
||||
endpoint = strings.TrimSuffix(endpoint, "/")
|
||||
endpoint = strings.TrimSuffix(endpoint, "/v1")
|
||||
// trim to https://miniflux.app
|
||||
|
||||
if len(credentials) == 2 {
|
||||
switch len(credentials) {
|
||||
case 2:
|
||||
return &Client{request: &request{endpoint: endpoint, username: credentials[0], password: credentials[1]}}
|
||||
case 1:
|
||||
return &Client{request: &request{endpoint: endpoint, apiKey: credentials[0]}}
|
||||
default:
|
||||
return &Client{request: &request{endpoint: endpoint}}
|
||||
}
|
||||
return &Client{request: &request{endpoint: endpoint, apiKey: credentials[0]}}
|
||||
}
|
||||
|
||||
// Healthcheck checks if the application is up and running.
|
||||
func (c *Client) Healthcheck() error {
|
||||
body, err := c.request.Get("/healthcheck")
|
||||
if err != nil {
|
||||
return fmt.Errorf("miniflux: unable to perform healthcheck: %w", err)
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
responseBodyContent, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("miniflux: unable to read healthcheck response: %w", err)
|
||||
}
|
||||
|
||||
if string(responseBodyContent) != "OK" {
|
||||
return fmt.Errorf("miniflux: invalid healthcheck response: %q", responseBodyContent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Version returns the version of the Miniflux instance.
|
||||
@@ -157,6 +185,25 @@ func (c *Client) MarkAllAsRead(userID int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// IntegrationsStatus fetches the integrations status for the logged user.
|
||||
func (c *Client) IntegrationsStatus() (bool, error) {
|
||||
body, err := c.request.Get("/v1/integrations/status")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var response struct {
|
||||
HasIntegrations bool `json:"has_integrations"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(body).Decode(&response); err != nil {
|
||||
return false, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return response.HasIntegrations, nil
|
||||
}
|
||||
|
||||
// Discover try to find subscriptions from a website.
|
||||
func (c *Client) Discover(url string) (Subscriptions, error) {
|
||||
body, err := c.request.Post("/v1/discover", map[string]string{"url": url})
|
||||
@@ -528,6 +575,25 @@ func (c *Client) SaveEntry(entryID int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// FetchEntryOriginalContent fetches the original content of an entry using the scraper.
|
||||
func (c *Client) FetchEntryOriginalContent(entryID int64) (string, error) {
|
||||
body, err := c.request.Get(fmt.Sprintf("/v1/entries/%d/fetch-content", entryID))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var response struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(body).Decode(&response); err != nil {
|
||||
return "", fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return response.Content, nil
|
||||
}
|
||||
|
||||
// FetchCounters fetches feed counters.
|
||||
func (c *Client) FetchCounters() (*FeedCounters, error) {
|
||||
body, err := c.request.Get("/v1/feeds/counters")
|
||||
@@ -566,6 +632,28 @@ func (c *Client) Icon(iconID int64) (*FeedIcon, error) {
|
||||
return feedIcon, nil
|
||||
}
|
||||
|
||||
// Enclosure fetches a specific enclosure.
|
||||
func (c *Client) Enclosure(enclosureID int64) (*Enclosure, error) {
|
||||
body, err := c.request.Get(fmt.Sprintf("/v1/enclosures/%d", enclosureID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var enclosure *Enclosure
|
||||
if err := json.NewDecoder(body).Decode(&enclosure); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error(%v)", err)
|
||||
}
|
||||
|
||||
return enclosure, nil
|
||||
}
|
||||
|
||||
// UpdateEnclosure updates an enclosure.
|
||||
func (c *Client) UpdateEnclosure(enclosureID int64, enclosureUpdate *EnclosureUpdateRequest) error {
|
||||
_, err := c.request.Put(fmt.Sprintf("/v1/enclosures/%d", enclosureID), enclosureUpdate)
|
||||
return err
|
||||
}
|
||||
|
||||
func buildFilterQueryString(path string, filter *Filter) string {
|
||||
if filter != nil {
|
||||
values := url.Values{}
|
||||
@@ -638,6 +726,10 @@ func buildFilterQueryString(path string, filter *Filter) string {
|
||||
values.Set("feed_id", strconv.FormatInt(filter.FeedID, 10))
|
||||
}
|
||||
|
||||
if filter.GloballyVisible {
|
||||
values.Set("globally_visible", "true")
|
||||
}
|
||||
|
||||
for _, status := range filter.Statuses {
|
||||
values.Add("status", status)
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ This code snippet fetch the list of users:
|
||||
miniflux "miniflux.app/v2/client"
|
||||
)
|
||||
|
||||
client := miniflux.New("https://api.example.org", "admin", "secret")
|
||||
client := miniflux.NewClient("https://api.example.org", "admin", "secret")
|
||||
users, err := client.Users()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
+62
-39
@@ -27,6 +27,7 @@ type User struct {
|
||||
EntryDirection string `json:"entry_sorting_direction"`
|
||||
EntryOrder string `json:"entry_sorting_order"`
|
||||
Stylesheet string `json:"stylesheet"`
|
||||
CustomJS string `json:"custom_js"`
|
||||
GoogleID string `json:"google_id"`
|
||||
OpenIDConnectID string `json:"openid_connect_id"`
|
||||
EntriesPerPage int `json:"entries_per_page"`
|
||||
@@ -41,6 +42,10 @@ type User struct {
|
||||
DefaultHomePage string `json:"default_home_page"`
|
||||
CategoriesSortingOrder string `json:"categories_sorting_order"`
|
||||
MarkReadOnView bool `json:"mark_read_on_view"`
|
||||
MediaPlaybackRate float64 `json:"media_playback_rate"`
|
||||
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
||||
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
||||
ExternalFontHosts string `json:"external_font_hosts"`
|
||||
}
|
||||
|
||||
func (u User) String() string {
|
||||
@@ -58,28 +63,33 @@ type UserCreationRequest struct {
|
||||
|
||||
// UserModificationRequest represents the request to update a user.
|
||||
type UserModificationRequest struct {
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
IsAdmin *bool `json:"is_admin"`
|
||||
Theme *string `json:"theme"`
|
||||
Language *string `json:"language"`
|
||||
Timezone *string `json:"timezone"`
|
||||
EntryDirection *string `json:"entry_sorting_direction"`
|
||||
EntryOrder *string `json:"entry_sorting_order"`
|
||||
Stylesheet *string `json:"stylesheet"`
|
||||
GoogleID *string `json:"google_id"`
|
||||
OpenIDConnectID *string `json:"openid_connect_id"`
|
||||
EntriesPerPage *int `json:"entries_per_page"`
|
||||
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
||||
ShowReadingTime *bool `json:"show_reading_time"`
|
||||
EntrySwipe *bool `json:"entry_swipe"`
|
||||
GestureNav *string `json:"gesture_nav"`
|
||||
DisplayMode *string `json:"display_mode"`
|
||||
DefaultReadingSpeed *int `json:"default_reading_speed"`
|
||||
CJKReadingSpeed *int `json:"cjk_reading_speed"`
|
||||
DefaultHomePage *string `json:"default_home_page"`
|
||||
CategoriesSortingOrder *string `json:"categories_sorting_order"`
|
||||
MarkReadOnView *bool `json:"mark_read_on_view"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
IsAdmin *bool `json:"is_admin"`
|
||||
Theme *string `json:"theme"`
|
||||
Language *string `json:"language"`
|
||||
Timezone *string `json:"timezone"`
|
||||
EntryDirection *string `json:"entry_sorting_direction"`
|
||||
EntryOrder *string `json:"entry_sorting_order"`
|
||||
Stylesheet *string `json:"stylesheet"`
|
||||
CustomJS *string `json:"custom_js"`
|
||||
GoogleID *string `json:"google_id"`
|
||||
OpenIDConnectID *string `json:"openid_connect_id"`
|
||||
EntriesPerPage *int `json:"entries_per_page"`
|
||||
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
||||
ShowReadingTime *bool `json:"show_reading_time"`
|
||||
EntrySwipe *bool `json:"entry_swipe"`
|
||||
GestureNav *string `json:"gesture_nav"`
|
||||
DisplayMode *string `json:"display_mode"`
|
||||
DefaultReadingSpeed *int `json:"default_reading_speed"`
|
||||
CJKReadingSpeed *int `json:"cjk_reading_speed"`
|
||||
DefaultHomePage *string `json:"default_home_page"`
|
||||
CategoriesSortingOrder *string `json:"categories_sorting_order"`
|
||||
MarkReadOnView *bool `json:"mark_read_on_view"`
|
||||
MediaPlaybackRate *float64 `json:"media_playback_rate"`
|
||||
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
|
||||
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
|
||||
ExternalFontHosts *string `json:"external_font_hosts"`
|
||||
}
|
||||
|
||||
// Users represents a list of users.
|
||||
@@ -107,7 +117,7 @@ type Subscription struct {
|
||||
}
|
||||
|
||||
func (s Subscription) String() string {
|
||||
return fmt.Sprintf(`Title="%s", URL="%s", Type="%s"`, s.Title, s.URL, s.Type)
|
||||
return fmt.Sprintf(`Title=%q, URL=%q, Type=%q`, s.Title, s.URL, s.Type)
|
||||
}
|
||||
|
||||
// Subscriptions represents a list of subscriptions.
|
||||
@@ -140,6 +150,7 @@ type Feed struct {
|
||||
Password string `json:"password"`
|
||||
Category *Category `json:"category,omitempty"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
DisableHTTP2 bool `json:"disable_http2"`
|
||||
}
|
||||
|
||||
// FeedCreationRequest represents the request to create a feed.
|
||||
@@ -160,6 +171,7 @@ type FeedCreationRequest struct {
|
||||
BlocklistRules string `json:"blocklist_rules"`
|
||||
KeeplistRules string `json:"keeplist_rules"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
DisableHTTP2 bool `json:"disable_http2"`
|
||||
}
|
||||
|
||||
// FeedModificationRequest represents the request to update a feed.
|
||||
@@ -182,6 +194,7 @@ type FeedModificationRequest struct {
|
||||
AllowSelfSignedCertificates *bool `json:"allow_self_signed_certificates"`
|
||||
FetchViaProxy *bool `json:"fetch_via_proxy"`
|
||||
HideGlobally *bool `json:"hide_globally"`
|
||||
DisableHTTP2 *bool `json:"disable_http2"`
|
||||
}
|
||||
|
||||
// FeedIcon represents the feed icon.
|
||||
@@ -202,24 +215,24 @@ type Feeds []*Feed
|
||||
// Entry represents a subscription item in the system.
|
||||
type Entry struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
FeedID int64 `json:"feed_id"`
|
||||
Status string `json:"status"`
|
||||
Date time.Time `json:"published_at"`
|
||||
ChangedAt time.Time `json:"changed_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Feed *Feed `json:"feed,omitempty"`
|
||||
Hash string `json:"hash"`
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
CommentsURL string `json:"comments_url"`
|
||||
Date time.Time `json:"published_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ChangedAt time.Time `json:"changed_at"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Content string `json:"content"`
|
||||
Author string `json:"author"`
|
||||
ShareCode string `json:"share_code"`
|
||||
Starred bool `json:"starred"`
|
||||
ReadingTime int `json:"reading_time"`
|
||||
Enclosures Enclosures `json:"enclosures,omitempty"`
|
||||
Feed *Feed `json:"feed,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
ReadingTime int `json:"reading_time"`
|
||||
UserID int64 `json:"user_id"`
|
||||
FeedID int64 `json:"feed_id"`
|
||||
Starred bool `json:"starred"`
|
||||
}
|
||||
|
||||
// EntryModificationRequest represents a request to modify an entry.
|
||||
@@ -233,12 +246,17 @@ type Entries []*Entry
|
||||
|
||||
// Enclosure represents an attachment.
|
||||
type Enclosure struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
EntryID int64 `json:"entry_id"`
|
||||
URL string `json:"url"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Size int `json:"size"`
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
EntryID int64 `json:"entry_id"`
|
||||
URL string `json:"url"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Size int `json:"size"`
|
||||
MediaProgression int64 `json:"media_progression"`
|
||||
}
|
||||
|
||||
type EnclosureUpdateRequest struct {
|
||||
MediaProgression int64 `json:"media_progression"`
|
||||
}
|
||||
|
||||
// Enclosures represents a list of attachments.
|
||||
@@ -269,6 +287,7 @@ type Filter struct {
|
||||
CategoryID int64
|
||||
FeedID int64
|
||||
Statuses []string
|
||||
GloballyVisible bool
|
||||
}
|
||||
|
||||
// EntryResultSet represents the response when fetching entries.
|
||||
@@ -287,3 +306,7 @@ type VersionResponse struct {
|
||||
Arch string `json:"arch"`
|
||||
OS string `json:"os"`
|
||||
}
|
||||
|
||||
func SetOptionalField[T any](value T) *T {
|
||||
return &value
|
||||
}
|
||||
|
||||
+7
-2
@@ -26,6 +26,8 @@ var (
|
||||
ErrForbidden = errors.New("miniflux: access forbidden")
|
||||
ErrServerError = errors.New("miniflux: internal server error")
|
||||
ErrNotFound = errors.New("miniflux: resource not found")
|
||||
ErrBadRequest = errors.New("miniflux: bad request")
|
||||
ErrEmptyEndpoint = errors.New("miniflux: empty endpoint provided")
|
||||
)
|
||||
|
||||
type errorResponse struct {
|
||||
@@ -61,6 +63,9 @@ func (r *request) Delete(path string) error {
|
||||
}
|
||||
|
||||
func (r *request) execute(method, path string, data interface{}) (io.ReadCloser, error) {
|
||||
if r.endpoint == "" {
|
||||
return nil, ErrEmptyEndpoint
|
||||
}
|
||||
if r.endpoint[len(r.endpoint)-1:] == "/" {
|
||||
r.endpoint = r.endpoint[:len(r.endpoint)-1]
|
||||
}
|
||||
@@ -124,10 +129,10 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
|
||||
var resp errorResponse
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err := decoder.Decode(&resp); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: bad request error (%v)", err)
|
||||
return nil, fmt.Errorf("%w (%v)", ErrBadRequest, err)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("miniflux: bad request (%s)", resp.ErrorMessage)
|
||||
return nil, fmt.Errorf("%w (%s)", ErrBadRequest, resp.ErrorMessage)
|
||||
}
|
||||
|
||||
if response.StatusCode > 400 {
|
||||
|
||||
+1639
-1458
File diff suppressed because it is too large
Load Diff
@@ -1,49 +1,48 @@
|
||||
module miniflux.app/v2
|
||||
|
||||
// +heroku goVersion go1.21
|
||||
// +heroku goVersion go1.23
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/PuerkitoBio/goquery v1.10.0
|
||||
github.com/abadojack/whatlanggo v1.0.1
|
||||
github.com/coreos/go-oidc/v3 v3.9.0
|
||||
github.com/go-webauthn/webauthn v0.10.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/andybalholm/brotli v1.1.1
|
||||
github.com/coreos/go-oidc/v3 v3.11.0
|
||||
github.com/go-webauthn/webauthn v0.11.2
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/tdewolff/minify/v2 v2.20.16
|
||||
github.com/yuin/goldmark v1.7.0
|
||||
golang.org/x/crypto v0.19.0
|
||||
golang.org/x/net v0.21.0
|
||||
golang.org/x/oauth2 v0.17.0
|
||||
golang.org/x/term v0.17.0
|
||||
mvdan.cc/xurls/v2 v2.5.0
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/tdewolff/minify/v2 v2.21.1
|
||||
github.com/yuin/goldmark v1.7.8
|
||||
golang.org/x/crypto v0.29.0
|
||||
golang.org/x/net v0.31.0
|
||||
golang.org/x/oauth2 v0.24.0
|
||||
golang.org/x/term v0.26.0
|
||||
golang.org/x/text v0.20.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-webauthn/x v0.1.8 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
|
||||
github.com/google/go-tpm v0.9.0 // indirect
|
||||
github.com/go-webauthn/x v0.1.14 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/google/go-tpm v0.9.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.7.11 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.7.18 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
)
|
||||
|
||||
go 1.21
|
||||
go 1.23
|
||||
|
||||
@@ -1,138 +1,119 @@
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
|
||||
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
|
||||
github.com/abadojack/whatlanggo v1.0.1 h1:19N6YogDnf71CTHm3Mp2qhYfkRdyvbgwWdd2EPxJRG4=
|
||||
github.com/abadojack/whatlanggo v1.0.1/go.mod h1:66WiQbSbJBIlOZMsvbKe5m6pzQovxCH9B/K8tQB2uoc=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
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.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
|
||||
github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
||||
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
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=
|
||||
github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
|
||||
github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
|
||||
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
||||
github.com/go-webauthn/webauthn v0.10.1 h1:+RFKj4yHPy282teiiy5sqTYPfRilzBpJyedrz9KsNFE=
|
||||
github.com/go-webauthn/webauthn v0.10.1/go.mod h1:a7BwAtrSMkeuJXtIKz433Av99nAv01pdfzB0a9xkDnI=
|
||||
github.com/go-webauthn/x v0.1.8 h1:f1C6k1AyUlDvnIzWSW+G9rN9nbp1hhLXZagUtyxZ8nc=
|
||||
github.com/go-webauthn/x v0.1.8/go.mod h1:i8UNlGVt3oy6oAFcP4SZB1djZLx/4pbekCbWowjTaJg=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
|
||||
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
|
||||
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
|
||||
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
|
||||
github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0=
|
||||
github.com/go-webauthn/x v0.1.14/go.mod h1:UuVvFZ8/NbOnkDz3y1NaxtUN87pmtpC1PQ+/5BBQRdc=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM=
|
||||
github.com/google/go-tpm v0.9.1/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=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
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/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
||||
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=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tdewolff/minify/v2 v2.20.16 h1:/C8dtRkxLTIyUlKlBz46gDiktCrE8a6+c1gTrnPFz+U=
|
||||
github.com/tdewolff/minify/v2 v2.20.16/go.mod h1:/FvxV9KaTrFu35J9I2FhRvWSBxcHj8sDSdwBFh5voxM=
|
||||
github.com/tdewolff/parse/v2 v2.7.11 h1:v+W45LnzmjndVlfqPCT5gGjAAZKd1GJGOPJveTIkBY8=
|
||||
github.com/tdewolff/parse/v2 v2.7.11/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tdewolff/minify/v2 v2.21.1 h1:AAf5iltw6+KlUvjRNPAPrANIXl3XEJNBBzuZom5iCAM=
|
||||
github.com/tdewolff/minify/v2 v2.21.1/go.mod h1:PoqFH8ugcuTUvKqVM9vOqXw4msxvuhL/DTmV5ZXhSCI=
|
||||
github.com/tdewolff/parse/v2 v2.7.18 h1:uSqjEMT2lwCj5oifBHDcWU2kN1pbLrRENgFWDJa57eI=
|
||||
github.com/tdewolff/parse/v2 v2.7.18/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
|
||||
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
|
||||
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
|
||||
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
|
||||
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
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=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
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=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
||||
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
||||
|
||||
@@ -72,6 +72,9 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
|
||||
sr.HandleFunc("/entries/{entryID}/fetch-content", handler.fetchContent).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/flush-history", handler.flushHistory).Methods(http.MethodPut, http.MethodDelete)
|
||||
sr.HandleFunc("/icons/{iconID}", handler.getIconByIconID).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/enclosures/{enclosureID}", handler.getEnclosureByID).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/enclosures/{enclosureID}", handler.updateEnclosureByID).Methods(http.MethodPut)
|
||||
sr.HandleFunc("/integrations/status", handler.getIntegrationsStatus).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/version", handler.versionHandler).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api // import "miniflux.app/v2/internal/api"
|
||||
|
||||
import (
|
||||
json_parser "encoding/json"
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/validator"
|
||||
)
|
||||
|
||||
func (h *handler) getEnclosureByID(w http.ResponseWriter, r *http.Request) {
|
||||
enclosureID := request.RouteInt64Param(r, "enclosureID")
|
||||
|
||||
enclosure, err := h.store.GetEnclosure(enclosureID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if enclosure == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
userID := request.UserID(r)
|
||||
if enclosure.UserID != userID {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
enclosure.ProxifyEnclosureURL(h.router)
|
||||
|
||||
json.OK(w, r, enclosure)
|
||||
}
|
||||
|
||||
func (h *handler) updateEnclosureByID(w http.ResponseWriter, r *http.Request) {
|
||||
enclosureID := request.RouteInt64Param(r, "enclosureID")
|
||||
|
||||
var enclosureUpdateRequest model.EnclosureUpdateRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&enclosureUpdateRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validator.ValidateEnclosureUpdateRequest(&enclosureUpdateRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
enclosure, err := h.store.GetEnclosure(enclosureID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if enclosure == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
userID := request.UserID(r)
|
||||
if enclosure.UserID != userID {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
enclosure.MediaProgression = enclosureUpdateRequest.MediaProgression
|
||||
if err := h.store.UpdateEnclosure(enclosure); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
+16
-18
@@ -8,19 +8,16 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/integration"
|
||||
"miniflux.app/v2/internal/mediaproxy"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/proxy"
|
||||
"miniflux.app/v2/internal/reader/processor"
|
||||
"miniflux.app/v2/internal/reader/readingtime"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
"miniflux.app/v2/internal/validator"
|
||||
)
|
||||
|
||||
@@ -36,19 +33,9 @@ func (h *handler) getEntryFromBuilder(w http.ResponseWriter, r *http.Request, b
|
||||
return
|
||||
}
|
||||
|
||||
entry.Content = proxy.AbsoluteProxyRewriter(h.router, r.Host, entry.Content)
|
||||
proxyOption := config.Opts.ProxyOption()
|
||||
entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content)
|
||||
|
||||
for i := range entry.Enclosures {
|
||||
if proxyOption == "all" || proxyOption != "none" && !urllib.IsHTTPS(entry.Enclosures[i].URL) {
|
||||
for _, mediaType := range config.Opts.ProxyMediaTypes() {
|
||||
if strings.HasPrefix(entry.Enclosures[i].MimeType, mediaType+"/") {
|
||||
entry.Enclosures[i].URL = proxy.AbsoluteProxifyURL(h.router, r.Host, entry.Enclosures[i].URL)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
entry.Enclosures.ProxifyEnclosureURL(h.router)
|
||||
|
||||
json.OK(w, r, entry)
|
||||
}
|
||||
@@ -149,6 +136,15 @@ func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int
|
||||
builder.WithLimit(limit)
|
||||
builder.WithTags(tags)
|
||||
builder.WithEnclosures()
|
||||
|
||||
if request.HasQueryParam(r, "globally_visible") {
|
||||
globallyVisible := request.QueryBoolParam(r, "globally_visible", true)
|
||||
|
||||
if globallyVisible {
|
||||
builder.WithGloballyVisible()
|
||||
}
|
||||
}
|
||||
|
||||
configureFilters(builder, r)
|
||||
|
||||
entries, err := builder.GetEntries()
|
||||
@@ -164,7 +160,7 @@ func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int
|
||||
}
|
||||
|
||||
for i := range entries {
|
||||
entries[i].Content = proxy.AbsoluteProxyRewriter(h.router, r.Host, entries[i].Content)
|
||||
entries[i].Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entries[i].Content)
|
||||
}
|
||||
|
||||
json.OK(w, r, &entriesResponse{Total: count, Entries: entries})
|
||||
@@ -275,7 +271,9 @@ func (h *handler) updateEntry(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
entryUpdateRequest.Patch(entry)
|
||||
entry.ReadingTime = readingtime.EstimateReadingTime(entry.Content, user.DefaultReadingSpeed, user.CJKReadingSpeed)
|
||||
if user.ShowReadingTime {
|
||||
entry.ReadingTime = readingtime.EstimateReadingTime(entry.Content, user.DefaultReadingSpeed, user.CJKReadingSpeed)
|
||||
}
|
||||
|
||||
if err := h.store.UpdateEntryTitleAndContent(entry); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
|
||||
@@ -115,7 +115,7 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateFeedModification(h.store, userID, &feedModificationRequest); validationErr != nil {
|
||||
if validationErr := validator.ValidateFeedModification(h.store, userID, originalFeed.ID, &feedModificationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
func (h *handler) getIconByFeedID(w http.ResponseWriter, r *http.Request) {
|
||||
feedID := request.RouteInt64Param(r, "feedID")
|
||||
|
||||
if !h.store.HasIcon(feedID) {
|
||||
if !h.store.HasFeedIcon(feedID) {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ func (h *handler) discoverSubscriptions(w http.ResponseWriter, r *http.Request)
|
||||
requestBuilder.WithUsernameAndPassword(subscriptionDiscoveryRequest.Username, subscriptionDiscoveryRequest.Password)
|
||||
requestBuilder.UseProxy(subscriptionDiscoveryRequest.FetchViaProxy)
|
||||
requestBuilder.IgnoreTLSErrors(subscriptionDiscoveryRequest.AllowSelfSignedCertificates)
|
||||
requestBuilder.DisableHTTP2(subscriptionDiscoveryRequest.DisableHTTP2)
|
||||
|
||||
subscriptions, localizedError := subscription.NewSubscriptionFinder(requestBuilder).FindSubscriptions(
|
||||
subscriptionDiscoveryRequest.URL,
|
||||
|
||||
+37
-4
@@ -7,6 +7,8 @@ import (
|
||||
json_parser "encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
@@ -77,11 +79,23 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if userModificationRequest.IsAdmin != nil && *userModificationRequest.IsAdmin {
|
||||
json.BadRequest(w, r, errors.New("Only administrators can change permissions of standard users"))
|
||||
json.BadRequest(w, r, errors.New("only administrators can change permissions of standard users"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cleanEnd := regexp.MustCompile(`(?m)\r\n\s*$`)
|
||||
if userModificationRequest.BlockFilterEntryRules != nil {
|
||||
*userModificationRequest.BlockFilterEntryRules = cleanEnd.ReplaceAllLiteralString(*userModificationRequest.BlockFilterEntryRules, "")
|
||||
// Clean carriage returns for Windows environments
|
||||
*userModificationRequest.BlockFilterEntryRules = strings.ReplaceAll(*userModificationRequest.BlockFilterEntryRules, "\r\n", "\n")
|
||||
}
|
||||
if userModificationRequest.KeepFilterEntryRules != nil {
|
||||
*userModificationRequest.KeepFilterEntryRules = cleanEnd.ReplaceAllLiteralString(*userModificationRequest.KeepFilterEntryRules, "")
|
||||
// Clean carriage returns for Windows environments
|
||||
*userModificationRequest.KeepFilterEntryRules = strings.ReplaceAll(*userModificationRequest.KeepFilterEntryRules, "\r\n", "\n")
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateUserModification(h.store, originalUser.ID, &userModificationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
@@ -116,6 +130,25 @@ func (h *handler) markUserAsRead(w http.ResponseWriter, r *http.Request) {
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
|
||||
func (h *handler) getIntegrationsStatus(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
|
||||
if _, err := h.store.UserByID(userID); err != nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
hasIntegrations := h.store.HasSaveEntry(userID)
|
||||
|
||||
response := struct {
|
||||
HasIntegrations bool `json:"has_integrations"`
|
||||
}{
|
||||
HasIntegrations: hasIntegrations,
|
||||
}
|
||||
|
||||
json.OK(w, r, response)
|
||||
}
|
||||
|
||||
func (h *handler) users(w http.ResponseWriter, r *http.Request) {
|
||||
if !request.IsAdminUser(r) {
|
||||
json.Forbidden(w, r)
|
||||
@@ -141,7 +174,7 @@ func (h *handler) userByID(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.RouteInt64Param(r, "userID")
|
||||
user, err := h.store.UserByID(userID)
|
||||
if err != nil {
|
||||
json.BadRequest(w, r, errors.New("Unable to fetch this user from the database"))
|
||||
json.BadRequest(w, r, errors.New("unable to fetch this user from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -163,7 +196,7 @@ func (h *handler) userByUsername(w http.ResponseWriter, r *http.Request) {
|
||||
username := request.RouteStringParam(r, "username")
|
||||
user, err := h.store.UserByUsername(username)
|
||||
if err != nil {
|
||||
json.BadRequest(w, r, errors.New("Unable to fetch this user from the database"))
|
||||
json.BadRequest(w, r, errors.New("unable to fetch this user from the database"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -194,7 +227,7 @@ func (h *handler) removeUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if user.ID == request.UserID(r) {
|
||||
json.BadRequest(w, r, errors.New("You cannot remove yourself"))
|
||||
json.BadRequest(w, r, errors.New("you cannot remove yourself"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ func askCredentials() (string, string) {
|
||||
fd := int(os.Stdin.Fd())
|
||||
|
||||
if !term.IsTerminal(fd) {
|
||||
printErrorAndExit(fmt.Errorf("this is not a terminal, exiting"))
|
||||
printErrorAndExit(fmt.Errorf("this is not an interactive terminal, exiting"))
|
||||
}
|
||||
|
||||
fmt.Print("Enter Username: ")
|
||||
|
||||
+21
-4
@@ -4,6 +4,7 @@
|
||||
package cli // import "miniflux.app/v2/internal/cli"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -23,7 +24,7 @@ const (
|
||||
flagVersionHelp = "Show application version"
|
||||
flagMigrateHelp = "Run SQL migrations"
|
||||
flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
|
||||
flagCreateAdminHelp = "Create admin user"
|
||||
flagCreateAdminHelp = "Create an admin user from an interactive terminal"
|
||||
flagResetPasswordHelp = "Reset user password"
|
||||
flagResetFeedErrorsHelp = "Clear all feed errors for all users"
|
||||
flagDebugModeHelp = "Show debug logs"
|
||||
@@ -88,6 +89,23 @@ func Parse() {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
if oauth2Provider := config.Opts.OAuth2Provider(); oauth2Provider != "" {
|
||||
if oauth2Provider != "oidc" && oauth2Provider != "google" {
|
||||
printErrorAndExit(fmt.Errorf(`unsupported OAuth2 provider: %q (Possible values are "google" or "oidc")`, oauth2Provider))
|
||||
}
|
||||
}
|
||||
|
||||
if config.Opts.DisableLocalAuth() {
|
||||
switch {
|
||||
case config.Opts.OAuth2Provider() == "" && config.Opts.AuthProxyHeader() == "":
|
||||
printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled but neither OAUTH2_PROVIDER nor AUTH_PROXY_HEADER is not set. Please enable at least one authentication source"))
|
||||
case config.Opts.OAuth2Provider() != "" && !config.Opts.IsOAuth2UserCreationAllowed():
|
||||
printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled and an OAUTH2_PROVIDER is configured, but OAUTH2_USER_CREATION is not enabled"))
|
||||
case config.Opts.AuthProxyHeader() != "" && !config.Opts.IsAuthProxyUserCreationAllowed():
|
||||
printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled and an AUTH_PROXY_HEADER is configured, but AUTH_PROXY_USER_CREATION is not enabled"))
|
||||
}
|
||||
}
|
||||
|
||||
if flagConfigDump {
|
||||
fmt.Print(config.Opts)
|
||||
return
|
||||
@@ -191,7 +209,7 @@ func Parse() {
|
||||
}
|
||||
|
||||
if flagCreateAdmin {
|
||||
createAdmin(store)
|
||||
createAdminUserFromInteractiveTerminal(store)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -211,9 +229,8 @@ func Parse() {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
// Create admin user and start the daemon.
|
||||
if config.Opts.CreateAdmin() {
|
||||
createAdmin(store)
|
||||
createAdminUserFromEnvironmentVariables(store)
|
||||
}
|
||||
|
||||
if flagRefreshFeeds {
|
||||
|
||||
@@ -12,15 +12,20 @@ import (
|
||||
"miniflux.app/v2/internal/validator"
|
||||
)
|
||||
|
||||
func createAdmin(store *storage.Storage) {
|
||||
userCreationRequest := &model.UserCreationRequest{
|
||||
Username: config.Opts.AdminUsername(),
|
||||
Password: config.Opts.AdminPassword(),
|
||||
IsAdmin: true,
|
||||
}
|
||||
func createAdminUserFromEnvironmentVariables(store *storage.Storage) {
|
||||
createAdminUser(store, config.Opts.AdminUsername(), config.Opts.AdminPassword())
|
||||
}
|
||||
|
||||
if userCreationRequest.Username == "" || userCreationRequest.Password == "" {
|
||||
userCreationRequest.Username, userCreationRequest.Password = askCredentials()
|
||||
func createAdminUserFromInteractiveTerminal(store *storage.Storage) {
|
||||
username, password := askCredentials()
|
||||
createAdminUser(store, username, password)
|
||||
}
|
||||
|
||||
func createAdminUser(store *storage.Storage, username, password string) {
|
||||
userCreationRequest := &model.UserCreationRequest{
|
||||
Username: username,
|
||||
Password: password,
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
if store.UserExists(userCreationRequest.Username) {
|
||||
@@ -34,7 +39,12 @@ func createAdmin(store *storage.Storage) {
|
||||
printErrorAndExit(validationErr.Error())
|
||||
}
|
||||
|
||||
if _, err := store.CreateUser(userCreationRequest); err != nil {
|
||||
if user, err := store.CreateUser(userCreationRequest); err != nil {
|
||||
printErrorAndExit(err)
|
||||
} else {
|
||||
slog.Info("Created new admin user",
|
||||
slog.String("username", user.Username),
|
||||
slog.Int64("user_id", user.ID),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func refreshFeeds(store *storage.Storage) {
|
||||
slog.Int("nb_workers", config.Opts.WorkerPoolSize()),
|
||||
)
|
||||
|
||||
for i := 0; i < config.Opts.WorkerPoolSize(); i++ {
|
||||
for i := range config.Opts.WorkerPoolSize() {
|
||||
wg.Add(1)
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
|
||||
+246
-44
@@ -4,6 +4,7 @@
|
||||
package config // import "miniflux.app/v2/internal/config"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
@@ -258,6 +259,29 @@ func TestCustomBaseURLWithTrailingSlash(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomBaseURLWithCustomPort(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("BASE_URL", "http://example.org:88/folder/")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.BaseURL() != "http://example.org:88/folder" {
|
||||
t.Fatalf(`Unexpected base URL, got "%s"`, opts.BaseURL())
|
||||
}
|
||||
|
||||
if opts.RootURL() != "http://example.org:88" {
|
||||
t.Fatalf(`Unexpected root URL, got "%s"`, opts.RootURL())
|
||||
}
|
||||
|
||||
if opts.BasePath() != "/folder" {
|
||||
t.Fatalf(`Unexpected base path, got "%s"`, opts.BasePath())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseURLWithoutScheme(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("BASE_URL", "example.org/folder/")
|
||||
@@ -1442,9 +1466,9 @@ func TestPocketConsumerKeyFromUserPrefs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyOption(t *testing.T) {
|
||||
func TestMediaProxyMode(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
@@ -1453,14 +1477,14 @@ func TestProxyOption(t *testing.T) {
|
||||
}
|
||||
|
||||
expected := "all"
|
||||
result := opts.ProxyOption()
|
||||
result := opts.MediaProxyMode()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expected)
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_MODE value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultProxyOptionValue(t *testing.T) {
|
||||
func TestDefaultMediaProxyModeValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
@@ -1469,17 +1493,17 @@ func TestDefaultProxyOptionValue(t *testing.T) {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := defaultProxyOption
|
||||
result := opts.ProxyOption()
|
||||
expected := defaultMediaProxyMode
|
||||
result := opts.MediaProxyMode()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expected)
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_MODE value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyMediaTypes(t *testing.T) {
|
||||
func TestMediaProxyResourceTypes(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image,audio")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image,audio")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
@@ -1489,25 +1513,25 @@ func TestProxyMediaTypes(t *testing.T) {
|
||||
|
||||
expected := []string{"audio", "image"}
|
||||
|
||||
if len(expected) != len(opts.ProxyMediaTypes()) {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
if len(expected) != len(opts.MediaProxyResourceTypes()) {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
|
||||
resultMap := make(map[string]bool)
|
||||
for _, mediaType := range opts.ProxyMediaTypes() {
|
||||
for _, mediaType := range opts.MediaProxyResourceTypes() {
|
||||
resultMap[mediaType] = true
|
||||
}
|
||||
|
||||
for _, mediaType := range expected {
|
||||
if !resultMap[mediaType] {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyMediaTypesWithDuplicatedValues(t *testing.T) {
|
||||
func TestMediaProxyResourceTypesWithDuplicatedValues(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image,audio, image")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image,audio, image")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
@@ -1516,23 +1540,119 @@ func TestProxyMediaTypesWithDuplicatedValues(t *testing.T) {
|
||||
}
|
||||
|
||||
expected := []string{"audio", "image"}
|
||||
if len(expected) != len(opts.ProxyMediaTypes()) {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
if len(expected) != len(opts.MediaProxyResourceTypes()) {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
|
||||
resultMap := make(map[string]bool)
|
||||
for _, mediaType := range opts.ProxyMediaTypes() {
|
||||
for _, mediaType := range opts.MediaProxyResourceTypes() {
|
||||
resultMap[mediaType] = true
|
||||
}
|
||||
|
||||
for _, mediaType := range expected {
|
||||
if !resultMap[mediaType] {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyImagesOptionBackwardCompatibility(t *testing.T) {
|
||||
func TestDefaultMediaProxyResourceTypes(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := []string{"image"}
|
||||
|
||||
if len(expected) != len(opts.MediaProxyResourceTypes()) {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
|
||||
resultMap := make(map[string]bool)
|
||||
for _, mediaType := range opts.MediaProxyResourceTypes() {
|
||||
resultMap[mediaType] = true
|
||||
}
|
||||
|
||||
for _, mediaType := range expected {
|
||||
if !resultMap[mediaType] {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaProxyHTTPClientTimeout(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("MEDIA_PROXY_HTTP_CLIENT_TIMEOUT", "24")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := 24
|
||||
result := opts.MediaProxyHTTPClientTimeout()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultMediaProxyHTTPClientTimeoutValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := defaultMediaProxyHTTPClientTimeout
|
||||
result := opts.MediaProxyHTTPClientTimeout()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaProxyCustomURL(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("MEDIA_PROXY_CUSTOM_URL", "http://example.org/proxy")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
expected := "http://example.org/proxy"
|
||||
result := opts.MediaCustomProxyURL()
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_CUSTOM_URL value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaProxyPrivateKey(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "foobar")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := []byte("foobar")
|
||||
result := opts.MediaProxyPrivateKey()
|
||||
|
||||
if !bytes.Equal(result, expected) {
|
||||
t.Fatalf(`Unexpected MEDIA_PROXY_PRIVATE_KEY value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyImagesOptionForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGES", "all")
|
||||
|
||||
@@ -1543,30 +1663,31 @@ func TestProxyImagesOptionBackwardCompatibility(t *testing.T) {
|
||||
}
|
||||
|
||||
expected := []string{"image"}
|
||||
if len(expected) != len(opts.ProxyMediaTypes()) {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
if len(expected) != len(opts.MediaProxyResourceTypes()) {
|
||||
t.Fatalf(`Unexpected PROXY_IMAGES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
|
||||
resultMap := make(map[string]bool)
|
||||
for _, mediaType := range opts.ProxyMediaTypes() {
|
||||
for _, mediaType := range opts.MediaProxyResourceTypes() {
|
||||
resultMap[mediaType] = true
|
||||
}
|
||||
|
||||
for _, mediaType := range expected {
|
||||
if !resultMap[mediaType] {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
t.Fatalf(`Unexpected PROXY_IMAGES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
expectedProxyOption := "all"
|
||||
result := opts.ProxyOption()
|
||||
result := opts.MediaProxyMode()
|
||||
if result != expectedProxyOption {
|
||||
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expectedProxyOption)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultProxyMediaTypes(t *testing.T) {
|
||||
func TestProxyImageURLForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGE_URL", "http://example.org/proxy")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
@@ -1574,25 +1695,73 @@ func TestDefaultProxyMediaTypes(t *testing.T) {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := []string{"image"}
|
||||
expected := "http://example.org/proxy"
|
||||
result := opts.MediaCustomProxyURL()
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected PROXY_IMAGE_URL value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
if len(expected) != len(opts.ProxyMediaTypes()) {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
func TestProxyURLOptionForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_URL", "http://example.org/proxy")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := "http://example.org/proxy"
|
||||
result := opts.MediaCustomProxyURL()
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected PROXY_URL value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyMediaTypesOptionForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image,audio")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
expected := []string{"audio", "image"}
|
||||
if len(expected) != len(opts.MediaProxyResourceTypes()) {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
|
||||
resultMap := make(map[string]bool)
|
||||
for _, mediaType := range opts.ProxyMediaTypes() {
|
||||
for _, mediaType := range opts.MediaProxyResourceTypes() {
|
||||
resultMap[mediaType] = true
|
||||
}
|
||||
|
||||
for _, mediaType := range expected {
|
||||
if !resultMap[mediaType] {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyHTTPClientTimeout(t *testing.T) {
|
||||
func TestProxyOptionForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
expected := "all"
|
||||
result := opts.MediaProxyMode()
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyHTTPClientTimeoutOptionForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_HTTP_CLIENT_TIMEOUT", "24")
|
||||
|
||||
@@ -1601,29 +1770,26 @@ func TestProxyHTTPClientTimeout(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := 24
|
||||
result := opts.ProxyHTTPClientTimeout()
|
||||
|
||||
result := opts.MediaProxyHTTPClientTimeout()
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultProxyHTTPClientTimeoutValue(t *testing.T) {
|
||||
func TestProxyPrivateKeyOptionForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "foobar")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := defaultProxyHTTPClientTimeout
|
||||
result := opts.ProxyHTTPClientTimeout()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
|
||||
expected := []byte("foobar")
|
||||
result := opts.MediaProxyPrivateKey()
|
||||
if !bytes.Equal(result, expected) {
|
||||
t.Fatalf(`Unexpected PROXY_PRIVATE_KEY value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1878,6 +2044,42 @@ func TestAuthProxyUserCreationAdmin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchBilibiliWatchTime(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("FETCH_BILIBILI_WATCH_TIME", "1")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := true
|
||||
result := opts.FetchBilibiliWatchTime()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected FETCH_BILIBILI_WATCH_TIME value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchNebulaWatchTime(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("FETCH_NEBULA_WATCH_TIME", "1")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := true
|
||||
result := opts.FetchNebulaWatchTime()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected FETCH_NEBULA_WATCH_TIME value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchOdyseeWatchTime(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("FETCH_ODYSEE_WATCH_TIME", "1")
|
||||
@@ -1944,7 +2146,7 @@ func TestParseConfigDumpOutput(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := tmpfile.Write([]byte(serialized)); err != nil {
|
||||
if _, err := tmpfile.WriteString(serialized); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
+85
-41
@@ -4,12 +4,12 @@
|
||||
package config // import "miniflux.app/v2/internal/config"
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/crypto"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ const (
|
||||
defaultBaseURL = "http://localhost"
|
||||
defaultRootURL = "http://localhost"
|
||||
defaultBasePath = ""
|
||||
defaultWorkerPoolSize = 5
|
||||
defaultWorkerPoolSize = 16
|
||||
defaultPollingFrequency = 60
|
||||
defaultForceRefreshInterval = 30
|
||||
defaultBatchSize = 100
|
||||
@@ -51,10 +51,13 @@ const (
|
||||
defaultCleanupArchiveUnreadDays = 180
|
||||
defaultCleanupArchiveBatchSize = 10000
|
||||
defaultCleanupRemoveSessionsDays = 30
|
||||
defaultProxyHTTPClientTimeout = 120
|
||||
defaultProxyOption = "http-only"
|
||||
defaultProxyMediaTypes = "image"
|
||||
defaultProxyUrl = ""
|
||||
defaultMediaProxyHTTPClientTimeout = 120
|
||||
defaultMediaProxyMode = "http-only"
|
||||
defaultMediaResourceTypes = "image"
|
||||
defaultMediaProxyURL = ""
|
||||
defaultFilterEntryMaxAgeDays = 0
|
||||
defaultFetchBilibiliWatchTime = false
|
||||
defaultFetchNebulaWatchTime = false
|
||||
defaultFetchOdyseeWatchTime = false
|
||||
defaultFetchYouTubeWatchTime = false
|
||||
defaultYouTubeEmbedUrlOverride = "https://www.youtube-nocookie.com/embed/"
|
||||
@@ -66,7 +69,9 @@ const (
|
||||
defaultOAuth2ClientSecret = ""
|
||||
defaultOAuth2RedirectURL = ""
|
||||
defaultOAuth2OidcDiscoveryEndpoint = ""
|
||||
defaultOauth2OidcProviderName = "OpenID Connect"
|
||||
defaultOAuth2Provider = ""
|
||||
defaultDisableLocalAuth = false
|
||||
defaultPocketConsumerKey = ""
|
||||
defaultHTTPClientTimeout = 20
|
||||
defaultHTTPClientMaxBodySize = 15
|
||||
@@ -135,19 +140,24 @@ type Options struct {
|
||||
createAdmin bool
|
||||
adminUsername string
|
||||
adminPassword string
|
||||
proxyHTTPClientTimeout int
|
||||
proxyOption string
|
||||
proxyMediaTypes []string
|
||||
proxyUrl string
|
||||
mediaProxyHTTPClientTimeout int
|
||||
mediaProxyMode string
|
||||
mediaProxyResourceTypes []string
|
||||
mediaProxyCustomURL string
|
||||
fetchBilibiliWatchTime bool
|
||||
fetchNebulaWatchTime bool
|
||||
fetchOdyseeWatchTime bool
|
||||
fetchYouTubeWatchTime bool
|
||||
filterEntryMaxAgeDays int
|
||||
youTubeEmbedUrlOverride string
|
||||
oauth2UserCreationAllowed bool
|
||||
oauth2ClientID string
|
||||
oauth2ClientSecret string
|
||||
oauth2RedirectURL string
|
||||
oidcDiscoveryEndpoint string
|
||||
oidcProviderName string
|
||||
oauth2Provider string
|
||||
disableLocalAuth bool
|
||||
pocketConsumerKey string
|
||||
httpClientTimeout int
|
||||
httpClientMaxBodySize int64
|
||||
@@ -165,15 +175,12 @@ type Options struct {
|
||||
metricsPassword string
|
||||
watchdog bool
|
||||
invidiousInstance string
|
||||
proxyPrivateKey []byte
|
||||
mediaProxyPrivateKey []byte
|
||||
webAuthn bool
|
||||
}
|
||||
|
||||
// NewOptions returns Options with default values.
|
||||
func NewOptions() *Options {
|
||||
randomKey := make([]byte, 16)
|
||||
rand.Read(randomKey)
|
||||
|
||||
return &Options{
|
||||
HTTPS: defaultHTTPS,
|
||||
logFile: defaultLogFile,
|
||||
@@ -212,10 +219,13 @@ func NewOptions() *Options {
|
||||
pollingParsingErrorLimit: defaultPollingParsingErrorLimit,
|
||||
workerPoolSize: defaultWorkerPoolSize,
|
||||
createAdmin: defaultCreateAdmin,
|
||||
proxyHTTPClientTimeout: defaultProxyHTTPClientTimeout,
|
||||
proxyOption: defaultProxyOption,
|
||||
proxyMediaTypes: []string{defaultProxyMediaTypes},
|
||||
proxyUrl: defaultProxyUrl,
|
||||
mediaProxyHTTPClientTimeout: defaultMediaProxyHTTPClientTimeout,
|
||||
mediaProxyMode: defaultMediaProxyMode,
|
||||
mediaProxyResourceTypes: []string{defaultMediaResourceTypes},
|
||||
mediaProxyCustomURL: defaultMediaProxyURL,
|
||||
filterEntryMaxAgeDays: defaultFilterEntryMaxAgeDays,
|
||||
fetchBilibiliWatchTime: defaultFetchBilibiliWatchTime,
|
||||
fetchNebulaWatchTime: defaultFetchNebulaWatchTime,
|
||||
fetchOdyseeWatchTime: defaultFetchOdyseeWatchTime,
|
||||
fetchYouTubeWatchTime: defaultFetchYouTubeWatchTime,
|
||||
youTubeEmbedUrlOverride: defaultYouTubeEmbedUrlOverride,
|
||||
@@ -224,7 +234,9 @@ func NewOptions() *Options {
|
||||
oauth2ClientSecret: defaultOAuth2ClientSecret,
|
||||
oauth2RedirectURL: defaultOAuth2RedirectURL,
|
||||
oidcDiscoveryEndpoint: defaultOAuth2OidcDiscoveryEndpoint,
|
||||
oidcProviderName: defaultOauth2OidcProviderName,
|
||||
oauth2Provider: defaultOAuth2Provider,
|
||||
disableLocalAuth: defaultDisableLocalAuth,
|
||||
pocketConsumerKey: defaultPocketConsumerKey,
|
||||
httpClientTimeout: defaultHTTPClientTimeout,
|
||||
httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024,
|
||||
@@ -242,7 +254,7 @@ func NewOptions() *Options {
|
||||
metricsPassword: defaultMetricsPassword,
|
||||
watchdog: defaultWatchdog,
|
||||
invidiousInstance: defaultInvidiousInstance,
|
||||
proxyPrivateKey: randomKey,
|
||||
mediaProxyPrivateKey: crypto.GenerateRandomBytes(16),
|
||||
webAuthn: defaultWebAuthn,
|
||||
}
|
||||
}
|
||||
@@ -445,11 +457,21 @@ func (o *Options) OIDCDiscoveryEndpoint() string {
|
||||
return o.oidcDiscoveryEndpoint
|
||||
}
|
||||
|
||||
// OIDCProviderName returns the OAuth2 OIDC provider's display name
|
||||
func (o *Options) OIDCProviderName() string {
|
||||
return o.oidcProviderName
|
||||
}
|
||||
|
||||
// OAuth2Provider returns the name of the OAuth2 provider configured.
|
||||
func (o *Options) OAuth2Provider() string {
|
||||
return o.oauth2Provider
|
||||
}
|
||||
|
||||
// DisableLocalAUth returns true if the local user database should not be used to authenticate users
|
||||
func (o *Options) DisableLocalAuth() bool {
|
||||
return o.disableLocalAuth
|
||||
}
|
||||
|
||||
// HasHSTS returns true if HTTP Strict Transport Security is enabled.
|
||||
func (o *Options) HasHSTS() bool {
|
||||
return o.hsts
|
||||
@@ -486,30 +508,47 @@ func (o *Options) YouTubeEmbedUrlOverride() string {
|
||||
return o.youTubeEmbedUrlOverride
|
||||
}
|
||||
|
||||
// FetchNebulaWatchTime returns true if the Nebula video duration
|
||||
// should be fetched and used as a reading time.
|
||||
func (o *Options) FetchNebulaWatchTime() bool {
|
||||
return o.fetchNebulaWatchTime
|
||||
}
|
||||
|
||||
// FetchOdyseeWatchTime returns true if the Odysee video duration
|
||||
// should be fetched and used as a reading time.
|
||||
func (o *Options) FetchOdyseeWatchTime() bool {
|
||||
return o.fetchOdyseeWatchTime
|
||||
}
|
||||
|
||||
// ProxyOption returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
|
||||
func (o *Options) ProxyOption() string {
|
||||
return o.proxyOption
|
||||
// FetchBilibiliWatchTime returns true if the Bilibili video duration
|
||||
// should be fetched and used as a reading time.
|
||||
func (o *Options) FetchBilibiliWatchTime() bool {
|
||||
return o.fetchBilibiliWatchTime
|
||||
}
|
||||
|
||||
// ProxyMediaTypes returns a slice of media types to proxy.
|
||||
func (o *Options) ProxyMediaTypes() []string {
|
||||
return o.proxyMediaTypes
|
||||
// MediaProxyMode returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
|
||||
func (o *Options) MediaProxyMode() string {
|
||||
return o.mediaProxyMode
|
||||
}
|
||||
|
||||
// ProxyUrl returns a string of a URL to use to proxy image requests
|
||||
func (o *Options) ProxyUrl() string {
|
||||
return o.proxyUrl
|
||||
// MediaProxyResourceTypes returns a slice of resource types to proxy.
|
||||
func (o *Options) MediaProxyResourceTypes() []string {
|
||||
return o.mediaProxyResourceTypes
|
||||
}
|
||||
|
||||
// ProxyHTTPClientTimeout returns the time limit in seconds before the proxy HTTP client cancel the request.
|
||||
func (o *Options) ProxyHTTPClientTimeout() int {
|
||||
return o.proxyHTTPClientTimeout
|
||||
// MediaCustomProxyURL returns the custom proxy URL for medias.
|
||||
func (o *Options) MediaCustomProxyURL() string {
|
||||
return o.mediaProxyCustomURL
|
||||
}
|
||||
|
||||
// MediaProxyHTTPClientTimeout returns the time limit in seconds before the proxy HTTP client cancel the request.
|
||||
func (o *Options) MediaProxyHTTPClientTimeout() int {
|
||||
return o.mediaProxyHTTPClientTimeout
|
||||
}
|
||||
|
||||
// MediaProxyPrivateKey returns the private key used by the media proxy.
|
||||
func (o *Options) MediaProxyPrivateKey() []byte {
|
||||
return o.mediaProxyPrivateKey
|
||||
}
|
||||
|
||||
// HasHTTPService returns true if the HTTP service is enabled.
|
||||
@@ -605,16 +644,16 @@ func (o *Options) InvidiousInstance() string {
|
||||
return o.invidiousInstance
|
||||
}
|
||||
|
||||
// ProxyPrivateKey returns the private key used by the media proxy
|
||||
func (o *Options) ProxyPrivateKey() []byte {
|
||||
return o.proxyPrivateKey
|
||||
}
|
||||
|
||||
// WebAuthn returns true if WebAuthn logins are supported
|
||||
func (o *Options) WebAuthn() bool {
|
||||
return o.webAuthn
|
||||
}
|
||||
|
||||
// FilterEntryMaxAgeDays returns the number of days after which entries should be retained.
|
||||
func (o *Options) FilterEntryMaxAgeDays() int {
|
||||
return o.filterEntryMaxAgeDays
|
||||
}
|
||||
|
||||
// SortedOptions returns options as a list of key value pairs, sorted by keys.
|
||||
func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
||||
var keyValues = map[string]interface{}{
|
||||
@@ -640,8 +679,11 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
||||
"DISABLE_HSTS": !o.hsts,
|
||||
"DISABLE_HTTP_SERVICE": !o.httpService,
|
||||
"DISABLE_SCHEDULER_SERVICE": !o.schedulerService,
|
||||
"FILTER_ENTRY_MAX_AGE_DAYS": o.filterEntryMaxAgeDays,
|
||||
"FETCH_YOUTUBE_WATCH_TIME": o.fetchYouTubeWatchTime,
|
||||
"FETCH_NEBULA_WATCH_TIME": o.fetchNebulaWatchTime,
|
||||
"FETCH_ODYSEE_WATCH_TIME": o.fetchOdyseeWatchTime,
|
||||
"FETCH_BILIBILI_WATCH_TIME": o.fetchBilibiliWatchTime,
|
||||
"HTTPS": o.HTTPS,
|
||||
"HTTP_CLIENT_MAX_BODY_SIZE": o.httpClientMaxBodySize,
|
||||
"HTTP_CLIENT_PROXY": o.httpClientProxy,
|
||||
@@ -666,19 +708,21 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
||||
"OAUTH2_CLIENT_ID": o.oauth2ClientID,
|
||||
"OAUTH2_CLIENT_SECRET": redactSecretValue(o.oauth2ClientSecret, redactSecret),
|
||||
"OAUTH2_OIDC_DISCOVERY_ENDPOINT": o.oidcDiscoveryEndpoint,
|
||||
"OAUTH2_OIDC_PROVIDER_NAME": o.oidcProviderName,
|
||||
"OAUTH2_PROVIDER": o.oauth2Provider,
|
||||
"OAUTH2_REDIRECT_URL": o.oauth2RedirectURL,
|
||||
"OAUTH2_USER_CREATION": o.oauth2UserCreationAllowed,
|
||||
"DISABLE_LOCAL_AUTH": o.disableLocalAuth,
|
||||
"POCKET_CONSUMER_KEY": redactSecretValue(o.pocketConsumerKey, redactSecret),
|
||||
"POLLING_FREQUENCY": o.pollingFrequency,
|
||||
"FORCE_REFRESH_INTERVAL": o.forceRefreshInterval,
|
||||
"POLLING_PARSING_ERROR_LIMIT": o.pollingParsingErrorLimit,
|
||||
"POLLING_SCHEDULER": o.pollingScheduler,
|
||||
"PROXY_HTTP_CLIENT_TIMEOUT": o.proxyHTTPClientTimeout,
|
||||
"PROXY_MEDIA_TYPES": o.proxyMediaTypes,
|
||||
"PROXY_OPTION": o.proxyOption,
|
||||
"PROXY_PRIVATE_KEY": redactSecretValue(string(o.proxyPrivateKey), redactSecret),
|
||||
"PROXY_URL": o.proxyUrl,
|
||||
"MEDIA_PROXY_HTTP_CLIENT_TIMEOUT": o.mediaProxyHTTPClientTimeout,
|
||||
"MEDIA_PROXY_RESOURCE_TYPES": o.mediaProxyResourceTypes,
|
||||
"MEDIA_PROXY_MODE": o.mediaProxyMode,
|
||||
"MEDIA_PROXY_PRIVATE_KEY": redactSecretValue(string(o.mediaProxyPrivateKey), redactSecret),
|
||||
"MEDIA_PROXY_CUSTOM_URL": o.mediaProxyCustomURL,
|
||||
"ROOT_URL": o.rootURL,
|
||||
"RUN_MIGRATIONS": o.runMigrations,
|
||||
"SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": o.schedulerEntryFrequencyMaxInterval,
|
||||
|
||||
+42
-13
@@ -10,6 +10,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -56,7 +57,7 @@ func (p *Parser) parseFileContent(r io.Reader) (lines []string) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
|
||||
if !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
@@ -87,6 +88,7 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
p.opts.logFormat = parsedValue
|
||||
}
|
||||
case "DEBUG":
|
||||
slog.Warn("The DEBUG environment variable is deprecated, use LOG_LEVEL instead")
|
||||
parsedValue := parseBool(value, defaultDebug)
|
||||
if parsedValue {
|
||||
p.opts.logLevel = "debug"
|
||||
@@ -112,6 +114,8 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
p.opts.databaseMinConns = parseInt(value, defaultDatabaseMinConns)
|
||||
case "DATABASE_CONNECTION_LIFETIME":
|
||||
p.opts.databaseConnectionLifetime = parseInt(value, defaultDatabaseConnectionLifetime)
|
||||
case "FILTER_ENTRY_MAX_AGE_DAYS":
|
||||
p.opts.filterEntryMaxAgeDays = parseInt(value, defaultFilterEntryMaxAgeDays)
|
||||
case "RUN_MIGRATIONS":
|
||||
p.opts.runMigrations = parseBool(value, defaultRunMigrations)
|
||||
case "DISABLE_HSTS":
|
||||
@@ -158,20 +162,41 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
p.opts.schedulerRoundRobinMinInterval = parseInt(value, defaultSchedulerRoundRobinMinInterval)
|
||||
case "POLLING_PARSING_ERROR_LIMIT":
|
||||
p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit)
|
||||
// kept for compatibility purpose
|
||||
case "PROXY_IMAGES":
|
||||
p.opts.proxyOption = parseString(value, defaultProxyOption)
|
||||
slog.Warn("The PROXY_IMAGES environment variable is deprecated, use MEDIA_PROXY_MODE instead")
|
||||
p.opts.mediaProxyMode = parseString(value, defaultMediaProxyMode)
|
||||
case "PROXY_HTTP_CLIENT_TIMEOUT":
|
||||
p.opts.proxyHTTPClientTimeout = parseInt(value, defaultProxyHTTPClientTimeout)
|
||||
slog.Warn("The PROXY_HTTP_CLIENT_TIMEOUT environment variable is deprecated, use MEDIA_PROXY_HTTP_CLIENT_TIMEOUT instead")
|
||||
p.opts.mediaProxyHTTPClientTimeout = parseInt(value, defaultMediaProxyHTTPClientTimeout)
|
||||
case "MEDIA_PROXY_HTTP_CLIENT_TIMEOUT":
|
||||
p.opts.mediaProxyHTTPClientTimeout = parseInt(value, defaultMediaProxyHTTPClientTimeout)
|
||||
case "PROXY_OPTION":
|
||||
p.opts.proxyOption = parseString(value, defaultProxyOption)
|
||||
slog.Warn("The PROXY_OPTION environment variable is deprecated, use MEDIA_PROXY_MODE instead")
|
||||
p.opts.mediaProxyMode = parseString(value, defaultMediaProxyMode)
|
||||
case "MEDIA_PROXY_MODE":
|
||||
p.opts.mediaProxyMode = parseString(value, defaultMediaProxyMode)
|
||||
case "PROXY_MEDIA_TYPES":
|
||||
p.opts.proxyMediaTypes = parseStringList(value, []string{defaultProxyMediaTypes})
|
||||
// kept for compatibility purpose
|
||||
slog.Warn("The PROXY_MEDIA_TYPES environment variable is deprecated, use MEDIA_PROXY_RESOURCE_TYPES instead")
|
||||
p.opts.mediaProxyResourceTypes = parseStringList(value, []string{defaultMediaResourceTypes})
|
||||
case "MEDIA_PROXY_RESOURCE_TYPES":
|
||||
p.opts.mediaProxyResourceTypes = parseStringList(value, []string{defaultMediaResourceTypes})
|
||||
case "PROXY_IMAGE_URL":
|
||||
p.opts.proxyUrl = parseString(value, defaultProxyUrl)
|
||||
slog.Warn("The PROXY_IMAGE_URL environment variable is deprecated, use MEDIA_PROXY_CUSTOM_URL instead")
|
||||
p.opts.mediaProxyCustomURL = parseString(value, defaultMediaProxyURL)
|
||||
case "PROXY_URL":
|
||||
p.opts.proxyUrl = parseString(value, defaultProxyUrl)
|
||||
slog.Warn("The PROXY_URL environment variable is deprecated, use MEDIA_PROXY_CUSTOM_URL instead")
|
||||
p.opts.mediaProxyCustomURL = parseString(value, defaultMediaProxyURL)
|
||||
case "PROXY_PRIVATE_KEY":
|
||||
slog.Warn("The PROXY_PRIVATE_KEY environment variable is deprecated, use MEDIA_PROXY_PRIVATE_KEY instead")
|
||||
randomKey := make([]byte, 16)
|
||||
rand.Read(randomKey)
|
||||
p.opts.mediaProxyPrivateKey = parseBytes(value, randomKey)
|
||||
case "MEDIA_PROXY_PRIVATE_KEY":
|
||||
randomKey := make([]byte, 16)
|
||||
rand.Read(randomKey)
|
||||
p.opts.mediaProxyPrivateKey = parseBytes(value, randomKey)
|
||||
case "MEDIA_PROXY_CUSTOM_URL":
|
||||
p.opts.mediaProxyCustomURL = parseString(value, defaultMediaProxyURL)
|
||||
case "CREATE_ADMIN":
|
||||
p.opts.createAdmin = parseBool(value, defaultCreateAdmin)
|
||||
case "ADMIN_USERNAME":
|
||||
@@ -200,8 +225,12 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
p.opts.oauth2RedirectURL = parseString(value, defaultOAuth2RedirectURL)
|
||||
case "OAUTH2_OIDC_DISCOVERY_ENDPOINT":
|
||||
p.opts.oidcDiscoveryEndpoint = parseString(value, defaultOAuth2OidcDiscoveryEndpoint)
|
||||
case "OAUTH2_OIDC_PROVIDER_NAME":
|
||||
p.opts.oidcProviderName = parseString(value, defaultOauth2OidcProviderName)
|
||||
case "OAUTH2_PROVIDER":
|
||||
p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider)
|
||||
case "DISABLE_LOCAL_AUTH":
|
||||
p.opts.disableLocalAuth = parseBool(value, defaultDisableLocalAuth)
|
||||
case "HTTP_CLIENT_TIMEOUT":
|
||||
p.opts.httpClientTimeout = parseInt(value, defaultHTTPClientTimeout)
|
||||
case "HTTP_CLIENT_MAX_BODY_SIZE":
|
||||
@@ -234,6 +263,10 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
p.opts.metricsPassword = parseString(value, defaultMetricsPassword)
|
||||
case "METRICS_PASSWORD_FILE":
|
||||
p.opts.metricsPassword = readSecretFile(value, defaultMetricsPassword)
|
||||
case "FETCH_BILIBILI_WATCH_TIME":
|
||||
p.opts.fetchBilibiliWatchTime = parseBool(value, defaultFetchBilibiliWatchTime)
|
||||
case "FETCH_NEBULA_WATCH_TIME":
|
||||
p.opts.fetchNebulaWatchTime = parseBool(value, defaultFetchNebulaWatchTime)
|
||||
case "FETCH_ODYSEE_WATCH_TIME":
|
||||
p.opts.fetchOdyseeWatchTime = parseBool(value, defaultFetchOdyseeWatchTime)
|
||||
case "FETCH_YOUTUBE_WATCH_TIME":
|
||||
@@ -244,10 +277,6 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
p.opts.watchdog = parseBool(value, defaultWatchdog)
|
||||
case "INVIDIOUS_INSTANCE":
|
||||
p.opts.invidiousInstance = parseString(value, defaultInvidiousInstance)
|
||||
case "PROXY_PRIVATE_KEY":
|
||||
randomKey := make([]byte, 16)
|
||||
rand.Read(randomKey)
|
||||
p.opts.proxyPrivateKey = parseBytes(value, randomKey)
|
||||
case "WEBAUTHN":
|
||||
p.opts.webAuthn = parseBool(value, defaultWebAuthn)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
@@ -16,8 +17,7 @@ import (
|
||||
|
||||
// HashFromBytes returns a SHA-256 checksum of the input.
|
||||
func HashFromBytes(value []byte) string {
|
||||
sum := sha256.Sum256(value)
|
||||
return fmt.Sprintf("%x", sum)
|
||||
return fmt.Sprintf("%x", sha256.Sum256(value))
|
||||
}
|
||||
|
||||
// Hash returns a SHA-256 checksum of a string.
|
||||
@@ -55,3 +55,12 @@ func GenerateSHA256Hmac(secret string, data []byte) string {
|
||||
h.Write(data)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func GenerateUUID() string {
|
||||
b := GenerateRandomBytes(16)
|
||||
return fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
|
||||
}
|
||||
|
||||
func ConstantTimeCmp(a, b string) bool {
|
||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func Migrate(db *sql.DB) error {
|
||||
var currentVersion int
|
||||
db.QueryRow(`SELECT version FROM schema_version`).Scan(¤tVersion)
|
||||
|
||||
slog.Debug("Running database migrations",
|
||||
slog.Info("Running database migrations",
|
||||
slog.Int("current_version", currentVersion),
|
||||
slog.Int("latest_version", schemaVersion),
|
||||
)
|
||||
|
||||
@@ -855,4 +855,109 @@ 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 readeck_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN readeck_only_url bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN readeck_url text default '';
|
||||
ALTER TABLE integrations ADD COLUMN readeck_api_key text default '';
|
||||
ALTER TABLE integrations ADD COLUMN readeck_labels text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN disable_http2 bool default 'f'`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE users ADD COLUMN media_playback_rate numeric default 1;`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
// the WHERE part speed-up the request a lot
|
||||
sql := `UPDATE entries SET tags = array_remove(tags, '') WHERE '' = ANY(tags);`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
// Entry URLs can exceeds btree maximum size
|
||||
// Checking entry existence is now using entries_feed_id_status_hash_idx index
|
||||
_, err = tx.Exec(`DROP INDEX entries_feed_url_idx`)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN raindrop_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN raindrop_token text default '';
|
||||
ALTER TABLE integrations ADD COLUMN raindrop_collection_id text default '';
|
||||
ALTER TABLE integrations ADD COLUMN raindrop_tags text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE feeds ADD COLUMN description text default ''`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE users
|
||||
ADD COLUMN block_filter_entry_rules text not null default '',
|
||||
ADD COLUMN keep_filter_entry_rules text not null default ''
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN betula_url text default '';
|
||||
ALTER TABLE integrations ADD COLUMN betula_token text default '';
|
||||
ALTER TABLE integrations ADD COLUMN betula_enabled bool default 'f';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN ntfy_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN ntfy_url text default '';
|
||||
ALTER TABLE integrations ADD COLUMN ntfy_topic text default '';
|
||||
ALTER TABLE integrations ADD COLUMN ntfy_api_token text default '';
|
||||
ALTER TABLE integrations ADD COLUMN ntfy_username text default '';
|
||||
ALTER TABLE integrations ADD COLUMN ntfy_password text default '';
|
||||
ALTER TABLE integrations ADD COLUMN ntfy_icon_url text default '';
|
||||
|
||||
ALTER TABLE feeds ADD COLUMN ntfy_enabled bool default 'f';
|
||||
ALTER TABLE feeds ADD COLUMN ntfy_priority int default '3';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE users ADD COLUMN mark_read_on_media_player_completion bool default 'f';`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE users ADD COLUMN custom_js text not null default '';`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `ALTER TABLE users ADD COLUMN external_font_hosts text not null default '';`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx) (err error) {
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN cubox_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN cubox_api_link text default '';
|
||||
`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/integration"
|
||||
"miniflux.app/v2/internal/mediaproxy"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/proxy"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -247,7 +247,6 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
|
||||
builder := h.store.NewEntryQueryBuilder(userID)
|
||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||
builder.WithLimit(50)
|
||||
builder.WithSorting("id", model.DefaultSortingDirection)
|
||||
|
||||
switch {
|
||||
case request.HasQueryParam(r, "since_id"):
|
||||
@@ -258,6 +257,7 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
|
||||
slog.Int64("since_id", sinceID),
|
||||
)
|
||||
builder.AfterEntryID(sinceID)
|
||||
builder.WithSorting("id", "ASC")
|
||||
}
|
||||
case request.HasQueryParam(r, "max_id"):
|
||||
maxID := request.QueryInt64Param(r, "max_id", 0)
|
||||
@@ -324,7 +324,7 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
|
||||
FeedID: entry.FeedID,
|
||||
Title: entry.Title,
|
||||
Author: entry.Author,
|
||||
HTML: proxy.AbsoluteProxyRewriter(h.router, r.Host, entry.Content),
|
||||
HTML: mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content),
|
||||
URL: entry.URL,
|
||||
IsSaved: isSaved,
|
||||
IsRead: isRead,
|
||||
|
||||
@@ -18,13 +18,12 @@ import (
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/http/route"
|
||||
"miniflux.app/v2/internal/integration"
|
||||
"miniflux.app/v2/internal/mediaproxy"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/proxy"
|
||||
"miniflux.app/v2/internal/reader/fetcher"
|
||||
mff "miniflux.app/v2/internal/reader/handler"
|
||||
mfs "miniflux.app/v2/internal/reader/subscription"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
"miniflux.app/v2/internal/validator"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -265,9 +264,10 @@ func getStreamFilterModifiers(r *http.Request) (RequestModifiers, error) {
|
||||
}
|
||||
|
||||
func getStream(streamID string, userID int64) (Stream, error) {
|
||||
if strings.HasPrefix(streamID, FeedPrefix) {
|
||||
switch {
|
||||
case strings.HasPrefix(streamID, FeedPrefix):
|
||||
return Stream{Type: FeedStream, ID: strings.TrimPrefix(streamID, FeedPrefix)}, nil
|
||||
} else if strings.HasPrefix(streamID, fmt.Sprintf(UserStreamPrefix, userID)) || strings.HasPrefix(streamID, StreamPrefix) {
|
||||
case strings.HasPrefix(streamID, fmt.Sprintf(UserStreamPrefix, userID)) || strings.HasPrefix(streamID, StreamPrefix):
|
||||
id := strings.TrimPrefix(streamID, fmt.Sprintf(UserStreamPrefix, userID))
|
||||
id = strings.TrimPrefix(id, StreamPrefix)
|
||||
switch id {
|
||||
@@ -288,15 +288,15 @@ func getStream(streamID string, userID int64) (Stream, error) {
|
||||
default:
|
||||
return Stream{NoStream, ""}, fmt.Errorf("googlereader: unknown stream with id: %s", id)
|
||||
}
|
||||
} else if strings.HasPrefix(streamID, fmt.Sprintf(UserLabelPrefix, userID)) || strings.HasPrefix(streamID, LabelPrefix) {
|
||||
case strings.HasPrefix(streamID, fmt.Sprintf(UserLabelPrefix, userID)) || strings.HasPrefix(streamID, LabelPrefix):
|
||||
id := strings.TrimPrefix(streamID, fmt.Sprintf(UserLabelPrefix, userID))
|
||||
id = strings.TrimPrefix(id, LabelPrefix)
|
||||
return Stream{LabelStream, id}, nil
|
||||
} else if streamID == "" {
|
||||
case streamID == "":
|
||||
return Stream{NoStream, ""}, nil
|
||||
default:
|
||||
return Stream{NoStream, ""}, fmt.Errorf("googlereader: unknown stream type: %s", streamID)
|
||||
}
|
||||
|
||||
return Stream{NoStream, ""}, fmt.Errorf("googlereader: unknown stream type: %s", streamID)
|
||||
}
|
||||
|
||||
func getStreams(streamIDs []string, userID int64) ([]Stream, error) {
|
||||
@@ -382,7 +382,7 @@ func getItemIDs(r *http.Request) ([]int64, error) {
|
||||
return itemIDs, nil
|
||||
}
|
||||
|
||||
func checkOutputFormat(w http.ResponseWriter, r *http.Request) error {
|
||||
func checkOutputFormat(r *http.Request) error {
|
||||
var output string
|
||||
if r.Method == http.MethodPost {
|
||||
err := r.ParseForm()
|
||||
@@ -736,11 +736,12 @@ func getFeed(stream Stream, store *storage.Storage, userID int64) (*model.Feed,
|
||||
}
|
||||
|
||||
func getOrCreateCategory(category Stream, store *storage.Storage, userID int64) (*model.Category, error) {
|
||||
if category.ID == "" {
|
||||
switch {
|
||||
case category.ID == "":
|
||||
return store.FirstCategory(userID)
|
||||
} else if store.CategoryTitleExists(userID, category.ID) {
|
||||
case store.CategoryTitleExists(userID, category.ID):
|
||||
return store.CategoryByTitle(userID, category.ID)
|
||||
} else {
|
||||
default:
|
||||
catRequest := model.CategoryRequest{
|
||||
Title: category.ID,
|
||||
}
|
||||
@@ -764,7 +765,7 @@ func subscribe(newFeed Stream, category Stream, title string, store *storage.Sto
|
||||
}
|
||||
|
||||
created, localizedError := mff.CreateFeed(store, userID, &feedRequest)
|
||||
if err != nil {
|
||||
if localizedError != nil {
|
||||
return nil, localizedError.Error()
|
||||
}
|
||||
|
||||
@@ -908,8 +909,8 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque
|
||||
slog.Int64("user_id", userID),
|
||||
)
|
||||
|
||||
if err := checkOutputFormat(w, r); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
if err := checkOutputFormat(r); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -960,7 +961,7 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
json.ServerError(w, r, fmt.Errorf("googlereader: no items returned from the database"))
|
||||
json.BadRequest(w, r, fmt.Errorf("googlereader: no items returned from the database for item IDs: %v", itemIDs))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -984,7 +985,7 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
contentItems := make([]contentItem, len(entries))
|
||||
for i, entry := range entries {
|
||||
enclosures := make([]contentItemEnclosure, len(entry.Enclosures))
|
||||
enclosures := make([]contentItemEnclosure, 0, len(entry.Enclosures))
|
||||
for _, enclosure := range entry.Enclosures {
|
||||
enclosures = append(enclosures, contentItemEnclosure{URL: enclosure.URL, Type: enclosure.MimeType})
|
||||
}
|
||||
@@ -1001,28 +1002,18 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque
|
||||
categories = append(categories, userStarred)
|
||||
}
|
||||
|
||||
entry.Content = proxy.AbsoluteProxyRewriter(h.router, r.Host, entry.Content)
|
||||
proxyOption := config.Opts.ProxyOption()
|
||||
entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content)
|
||||
|
||||
for i := range entry.Enclosures {
|
||||
if proxyOption == "all" || proxyOption != "none" && !urllib.IsHTTPS(entry.Enclosures[i].URL) {
|
||||
for _, mediaType := range config.Opts.ProxyMediaTypes() {
|
||||
if strings.HasPrefix(entry.Enclosures[i].MimeType, mediaType+"/") {
|
||||
entry.Enclosures[i].URL = proxy.AbsoluteProxifyURL(h.router, r.Host, entry.Enclosures[i].URL)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
entry.Enclosures.ProxifyEnclosureURL(h.router)
|
||||
|
||||
contentItems[i] = contentItem{
|
||||
ID: fmt.Sprintf(EntryIDLong, entry.ID),
|
||||
Title: entry.Title,
|
||||
Author: entry.Author,
|
||||
TimestampUsec: fmt.Sprintf("%d", entry.Date.UnixNano()/(int64(time.Microsecond)/int64(time.Nanosecond))),
|
||||
CrawlTimeMsec: fmt.Sprintf("%d", entry.Date.UnixNano()/(int64(time.Microsecond)/int64(time.Nanosecond))),
|
||||
TimestampUsec: fmt.Sprintf("%d", entry.Date.UnixMicro()),
|
||||
CrawlTimeMsec: fmt.Sprintf("%d", entry.CreatedAt.UnixMilli()),
|
||||
Published: entry.Date.Unix(),
|
||||
Updated: entry.Date.Unix(),
|
||||
Updated: entry.ChangedAt.Unix(),
|
||||
Categories: categories,
|
||||
Canonical: []contentHREF{
|
||||
{
|
||||
@@ -1170,7 +1161,7 @@ func (h *handler) tagListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
|
||||
if err := checkOutputFormat(w, r); err != nil {
|
||||
if err := checkOutputFormat(r); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
@@ -1205,8 +1196,8 @@ func (h *handler) subscriptionListHandler(w http.ResponseWriter, r *http.Request
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
|
||||
if err := checkOutputFormat(w, r); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
if err := checkOutputFormat(r); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1224,7 +1215,7 @@ func (h *handler) subscriptionListHandler(w http.ResponseWriter, r *http.Request
|
||||
URL: feed.FeedURL,
|
||||
Categories: []subscriptionCategory{{fmt.Sprintf(UserLabelPrefix, userID) + feed.Category.Title, feed.Category.Title, "folder"}},
|
||||
HTMLURL: feed.SiteURL,
|
||||
IconURL: "", //TODO Icons are only base64 encode in DB yet
|
||||
IconURL: "", // TODO: Icons are base64 encoded in the DB.
|
||||
})
|
||||
}
|
||||
json.OK(w, r, result)
|
||||
@@ -1251,8 +1242,8 @@ func (h *handler) userInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
|
||||
if err := checkOutputFormat(w, r); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
if err := checkOutputFormat(r); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1276,8 +1267,8 @@ func (h *handler) streamItemIDsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
slog.Int64("user_id", userID),
|
||||
)
|
||||
|
||||
if err := checkOutputFormat(w, r); err != nil {
|
||||
json.ServerError(w, r, fmt.Errorf("googlereader: output only as json supported"))
|
||||
if err := checkOutputFormat(r); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1477,8 +1468,7 @@ func (h *handler) handleFeedStreamHandler(w http.ResponseWriter, r *http.Request
|
||||
|
||||
if len(rm.ExcludeTargets) > 0 {
|
||||
for _, s := range rm.ExcludeTargets {
|
||||
switch s.Type {
|
||||
case ReadStream:
|
||||
if s.Type == ReadStream {
|
||||
builder.WithoutStatus(model.EntryStatusRead)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,14 @@ package cookie // import "miniflux.app/v2/internal/http/cookie"
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
)
|
||||
|
||||
// Cookie names.
|
||||
const (
|
||||
CookieAppSessionID = "MinifluxAppSessionID"
|
||||
CookieUserSessionID = "MinifluxUserSessionID"
|
||||
|
||||
// Cookie duration in days.
|
||||
cookieDuration = 30
|
||||
)
|
||||
|
||||
// New creates a new cookie.
|
||||
@@ -25,7 +24,7 @@ func New(name, value string, isHTTPS bool, path string) *http.Cookie {
|
||||
Path: basePath(path),
|
||||
Secure: isHTTPS,
|
||||
HttpOnly: true,
|
||||
Expires: time.Now().Add(cookieDuration * 24 * time.Hour),
|
||||
Expires: time.Now().Add(time.Duration(config.Opts.CleanupRemoveSessionsDays()) * 24 * time.Hour),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,14 +37,10 @@ const (
|
||||
|
||||
func WebAuthnSessionData(r *http.Request) *model.WebAuthnSession {
|
||||
if v := r.Context().Value(WebAuthnDataContextKey); v != nil {
|
||||
value, valid := v.(model.WebAuthnSession)
|
||||
if !valid {
|
||||
return nil
|
||||
if value, valid := v.(model.WebAuthnSession); valid {
|
||||
return &value
|
||||
}
|
||||
|
||||
return &value
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -151,39 +147,27 @@ func ClientIP(r *http.Request) string {
|
||||
|
||||
func getContextStringValue(r *http.Request, key ContextKey) string {
|
||||
if v := r.Context().Value(key); v != nil {
|
||||
value, valid := v.(string)
|
||||
if !valid {
|
||||
return ""
|
||||
if value, valid := v.(string); valid {
|
||||
return value
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func getContextBoolValue(r *http.Request, key ContextKey) bool {
|
||||
if v := r.Context().Value(key); v != nil {
|
||||
value, valid := v.(bool)
|
||||
if !valid {
|
||||
return false
|
||||
if value, valid := v.(bool); valid {
|
||||
return value
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getContextInt64Value(r *http.Request, key ContextKey) int64 {
|
||||
if v := r.Context().Value(key); v != nil {
|
||||
value, valid := v.(int64)
|
||||
if !valid {
|
||||
return 0
|
||||
if value, valid := v.(int64); valid {
|
||||
return value
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
)
|
||||
|
||||
const compressionThreshold = 1024
|
||||
@@ -96,7 +98,6 @@ func (b *Builder) Write() {
|
||||
}
|
||||
|
||||
func (b *Builder) writeHeaders() {
|
||||
b.headers["X-XSS-Protection"] = "1; mode=block"
|
||||
b.headers["X-Content-Type-Options"] = "nosniff"
|
||||
b.headers["X-Frame-Options"] = "DENY"
|
||||
b.headers["Referrer-Policy"] = "no-referrer"
|
||||
@@ -111,8 +112,15 @@ func (b *Builder) writeHeaders() {
|
||||
func (b *Builder) compress(data []byte) {
|
||||
if b.enableCompression && len(data) > compressionThreshold {
|
||||
acceptEncoding := b.r.Header.Get("Accept-Encoding")
|
||||
|
||||
switch {
|
||||
case strings.Contains(acceptEncoding, "br"):
|
||||
b.headers["Content-Encoding"] = "br"
|
||||
b.writeHeaders()
|
||||
|
||||
brotliWriter := brotli.NewWriterV2(b.w, brotli.DefaultCompression)
|
||||
defer brotliWriter.Close()
|
||||
brotliWriter.Write(data)
|
||||
return
|
||||
case strings.Contains(acceptEncoding, "gzip"):
|
||||
b.headers["Content-Encoding"] = "gzip"
|
||||
b.writeHeaders()
|
||||
|
||||
@@ -28,7 +28,6 @@ func TestResponseHasCommonHeaders(t *testing.T) {
|
||||
resp := w.Result()
|
||||
|
||||
headers := map[string]string{
|
||||
"X-XSS-Protection": "1; mode=block",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Frame-Options": "DENY",
|
||||
}
|
||||
@@ -229,7 +228,7 @@ func TestBuildResponseWithCachingAndEtag(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildResponseWithGzipCompression(t *testing.T) {
|
||||
func TestBuildResponseWithBrotliCompression(t *testing.T) {
|
||||
body := strings.Repeat("a", compressionThreshold+1)
|
||||
r, err := http.NewRequest("GET", "/", nil)
|
||||
r.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
||||
@@ -246,6 +245,30 @@ func TestBuildResponseWithGzipCompression(t *testing.T) {
|
||||
handler.ServeHTTP(w, r)
|
||||
resp := w.Result()
|
||||
|
||||
expected := "br"
|
||||
actual := resp.Header.Get("Content-Encoding")
|
||||
if actual != expected {
|
||||
t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildResponseWithGzipCompression(t *testing.T) {
|
||||
body := strings.Repeat("a", compressionThreshold+1)
|
||||
r, err := http.NewRequest("GET", "/", nil)
|
||||
r.Header.Set("Accept-Encoding", "gzip, deflate")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
New(w, r).WithBody(body).Write()
|
||||
})
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
resp := w.Result()
|
||||
|
||||
expected := "gzip"
|
||||
actual := resp.Header.Get("Content-Encoding")
|
||||
if actual != expected {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package betula
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
url string
|
||||
token string
|
||||
}
|
||||
|
||||
func NewClient(url, token string) *Client {
|
||||
return &Client{url: url, token: token}
|
||||
}
|
||||
|
||||
func (c *Client) CreateBookmark(entryURL, entryTitle string, tags []string) error {
|
||||
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.url, "/save-link")
|
||||
if err != nil {
|
||||
return fmt.Errorf("betula: unable to generate save-link endpoint: %v", err)
|
||||
}
|
||||
|
||||
values := url.Values{}
|
||||
values.Add("url", entryURL)
|
||||
values.Add("title", entryTitle)
|
||||
values.Add("tags", strings.Join(tags, ","))
|
||||
|
||||
request, err := http.NewRequest(http.MethodPost, apiEndpoint+"?"+values.Encode(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("betula: unable to create request: %v", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
request.AddCookie(&http.Cookie{Name: "betula-token", Value: c.token})
|
||||
|
||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("betula: unable to send request: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
return fmt.Errorf("betula: unable to create bookmark: url=%s status=%d", apiEndpoint, response.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Cubox API documentation: https://help.cubox.cc/save/api/
|
||||
|
||||
package cubox // import "miniflux.app/v2/internal/integration/cubox"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
apiLink string
|
||||
}
|
||||
|
||||
func NewClient(apiLink string) *Client {
|
||||
return &Client{apiLink: apiLink}
|
||||
}
|
||||
|
||||
func (c *Client) SaveLink(entryURL string) error {
|
||||
if c.apiLink == "" {
|
||||
return errors.New("cubox: missing API link")
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(&card{
|
||||
Type: "url",
|
||||
Content: entryURL,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cubox: unable to encode request body: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultClientTimeout)
|
||||
defer cancel()
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiLink, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cubox: unable to create request: %w", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cubox: unable to send request: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != 200 {
|
||||
return fmt.Errorf("cubox: unable to save link: status=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type card struct {
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/integration/apprise"
|
||||
"miniflux.app/v2/internal/integration/betula"
|
||||
"miniflux.app/v2/internal/integration/cubox"
|
||||
"miniflux.app/v2/internal/integration/espial"
|
||||
"miniflux.app/v2/internal/integration/instapaper"
|
||||
"miniflux.app/v2/internal/integration/linkace"
|
||||
@@ -15,10 +17,13 @@ import (
|
||||
"miniflux.app/v2/internal/integration/linkwarden"
|
||||
"miniflux.app/v2/internal/integration/matrixbot"
|
||||
"miniflux.app/v2/internal/integration/notion"
|
||||
"miniflux.app/v2/internal/integration/ntfy"
|
||||
"miniflux.app/v2/internal/integration/nunuxkeeper"
|
||||
"miniflux.app/v2/internal/integration/omnivore"
|
||||
"miniflux.app/v2/internal/integration/pinboard"
|
||||
"miniflux.app/v2/internal/integration/pocket"
|
||||
"miniflux.app/v2/internal/integration/raindrop"
|
||||
"miniflux.app/v2/internal/integration/readeck"
|
||||
"miniflux.app/v2/internal/integration/readwise"
|
||||
"miniflux.app/v2/internal/integration/shaarli"
|
||||
"miniflux.app/v2/internal/integration/shiori"
|
||||
@@ -30,6 +35,30 @@ import (
|
||||
|
||||
// SendEntry sends the entry to third-party providers when the user click on "Save".
|
||||
func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
|
||||
if userIntegrations.BetulaEnabled {
|
||||
slog.Debug("Sending entry to Betula",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int64("entry_id", entry.ID),
|
||||
slog.String("entry_url", entry.URL),
|
||||
)
|
||||
|
||||
client := betula.NewClient(userIntegrations.BetulaURL, userIntegrations.BetulaToken)
|
||||
err := client.CreateBookmark(
|
||||
entry.URL,
|
||||
entry.Title,
|
||||
entry.Tags,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
slog.Error("Unable to send entry to Betula",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int64("entry_id", entry.ID),
|
||||
slog.String("entry_url", entry.URL),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if userIntegrations.PinboardEnabled {
|
||||
slog.Debug("Sending entry to Pinboard",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
@@ -250,6 +279,29 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
|
||||
}
|
||||
}
|
||||
|
||||
if userIntegrations.ReadeckEnabled {
|
||||
slog.Debug("Sending entry to Readeck",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int64("entry_id", entry.ID),
|
||||
slog.String("entry_url", entry.URL),
|
||||
)
|
||||
|
||||
client := readeck.NewClient(
|
||||
userIntegrations.ReadeckURL,
|
||||
userIntegrations.ReadeckAPIKey,
|
||||
userIntegrations.ReadeckLabels,
|
||||
userIntegrations.ReadeckOnlyURL,
|
||||
)
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if userIntegrations.ReadwiseEnabled {
|
||||
slog.Debug("Sending entry to Readwise",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
@@ -271,6 +323,25 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
|
||||
}
|
||||
}
|
||||
|
||||
if userIntegrations.CuboxEnabled {
|
||||
slog.Debug("Sending entry to Cubox",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int64("entry_id", entry.ID),
|
||||
slog.String("entry_url", entry.URL),
|
||||
)
|
||||
|
||||
client := cubox.NewClient(userIntegrations.CuboxAPILink)
|
||||
|
||||
if err := client.SaveLink(entry.URL); err != nil {
|
||||
slog.Error("Unable to send entry to Cubox",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int64("entry_id", entry.ID),
|
||||
slog.String("entry_url", entry.URL),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if userIntegrations.ShioriEnabled {
|
||||
slog.Debug("Sending entry to Shiori",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
@@ -335,6 +406,7 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if userIntegrations.OmnivoreEnabled {
|
||||
slog.Debug("Sending entry to Omnivore",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
@@ -352,6 +424,24 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if userIntegrations.RaindropEnabled {
|
||||
slog.Debug("Sending entry to Raindrop",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int64("entry_id", entry.ID),
|
||||
slog.String("entry_url", entry.URL),
|
||||
)
|
||||
|
||||
client := raindrop.NewClient(userIntegrations.RaindropToken, userIntegrations.RaindropCollectionID, userIntegrations.RaindropTags)
|
||||
if err := client.CreateRaindrop(entry.URL, entry.Title); err != nil {
|
||||
slog.Error("Unable to send entry to Raindrop",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int64("entry_id", entry.ID),
|
||||
slog.String("entry_url", entry.URL),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PushEntries pushes a list of entries to activated third-party providers during feed refreshes.
|
||||
@@ -401,6 +491,28 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
|
||||
}
|
||||
}
|
||||
|
||||
if userIntegrations.NtfyEnabled && feed.NtfyEnabled {
|
||||
slog.Debug("Sending new entries to Ntfy",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int("nb_entries", len(entries)),
|
||||
slog.Int64("feed_id", feed.ID),
|
||||
)
|
||||
|
||||
client := ntfy.NewClient(
|
||||
userIntegrations.NtfyURL,
|
||||
userIntegrations.NtfyTopic,
|
||||
userIntegrations.NtfyAPIToken,
|
||||
userIntegrations.NtfyUsername,
|
||||
userIntegrations.NtfyPassword,
|
||||
userIntegrations.NtfyIconURL,
|
||||
feed.NtfyPriority,
|
||||
)
|
||||
|
||||
if err := client.SendMessages(feed, entries); err != nil {
|
||||
slog.Warn("Unable to send new entries to Ntfy", slog.Any("error", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Integrations that only support sending individual entries
|
||||
if userIntegrations.TelegramBotEnabled || userIntegrations.AppriseEnabled {
|
||||
for _, entry := range entries {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"miniflux.app/v2/internal/model"
|
||||
)
|
||||
|
||||
// PushEntry pushes entries to matrix chat using integration settings provided
|
||||
// PushEntries pushes entries to matrix chat using integration settings provided
|
||||
func PushEntries(feed *model.Feed, entries model.Entries, matrixBaseURL, matrixUsername, matrixPassword, matrixRoomID string) error {
|
||||
client := NewClient(matrixBaseURL)
|
||||
discovery, err := client.DiscoverEndpoints()
|
||||
@@ -28,7 +28,7 @@ func PushEntries(feed *model.Feed, entries model.Entries, matrixBaseURL, matrixU
|
||||
|
||||
for _, entry := range entries {
|
||||
textMessages = append(textMessages, fmt.Sprintf(`[%s] %s - %s`, feed.Title, entry.Title, entry.URL))
|
||||
formattedTextMessages = append(formattedTextMessages, fmt.Sprintf(`<li><strong>%s</strong>: <a href="%s">%s</a></li>`, feed.Title, entry.URL, entry.Title))
|
||||
formattedTextMessages = append(formattedTextMessages, fmt.Sprintf(`<li><strong>%s</strong>: <a href=%q>%s</a></li>`, feed.Title, entry.URL, entry.Title))
|
||||
}
|
||||
|
||||
_, err = client.SendFormattedTextMessage(
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ntfy // import "miniflux.app/v2/internal/integration/ntfy"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultClientTimeout = 10 * time.Second
|
||||
defaultNtfyURL = "https://ntfy.sh"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL string
|
||||
ntfyPriority int
|
||||
}
|
||||
|
||||
func NewClient(ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL string, ntfyPriority int) *Client {
|
||||
if ntfyURL == "" {
|
||||
ntfyURL = defaultNtfyURL
|
||||
}
|
||||
return &Client{ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL, ntfyPriority}
|
||||
}
|
||||
|
||||
func (c *Client) SendMessages(feed *model.Feed, entries model.Entries) error {
|
||||
for _, entry := range entries {
|
||||
ntfyMessage := &ntfyMessage{
|
||||
Topic: c.ntfyTopic,
|
||||
Message: entry.Title,
|
||||
Title: feed.Title,
|
||||
Priority: c.ntfyPriority,
|
||||
Click: entry.URL,
|
||||
}
|
||||
|
||||
if c.ntfyIconURL != "" {
|
||||
ntfyMessage.Icon = c.ntfyIconURL
|
||||
}
|
||||
|
||||
slog.Debug("Sending Ntfy message",
|
||||
slog.String("url", c.ntfyURL),
|
||||
slog.String("topic", c.ntfyTopic),
|
||||
slog.Int("priority", ntfyMessage.Priority),
|
||||
slog.String("message", ntfyMessage.Message),
|
||||
slog.String("entry_url", entry.URL),
|
||||
)
|
||||
|
||||
if err := c.makeRequest(ntfyMessage); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) makeRequest(payload any) error {
|
||||
requestBody, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntfy: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodPost, c.ntfyURL, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntfy: unable to create request: %v", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
|
||||
// See https://docs.ntfy.sh/publish/#access-tokens
|
||||
if c.ntfyApiToken != "" {
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.ntfyApiToken))
|
||||
}
|
||||
|
||||
// See https://docs.ntfy.sh/publish/#username-password
|
||||
if c.ntfyUsername != "" && c.ntfyPassword != "" {
|
||||
request.SetBasicAuth(c.ntfyUsername, c.ntfyPassword)
|
||||
}
|
||||
|
||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntfy: unable to send request: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
return fmt.Errorf("ntfy: incorrect response status code %d for url %s", response.StatusCode, c.ntfyURL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// See https://docs.ntfy.sh/publish/#publish-as-json
|
||||
type ntfyMessage struct {
|
||||
Topic string `json:"topic"`
|
||||
Message string `json:"message"`
|
||||
Title string `json:"title"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Icon string `json:"icon,omitempty"` // https://docs.ntfy.sh/publish/#icons
|
||||
Click string `json:"click,omitempty"`
|
||||
Actions []ntfyAction `json:"actions,omitempty"`
|
||||
}
|
||||
|
||||
// See https://docs.ntfy.sh/publish/#action-buttons
|
||||
type ntfyAction struct {
|
||||
Action string `json:"action"`
|
||||
Label string `json:"label"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
@@ -11,8 +11,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"miniflux.app/v2/internal/crypto"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
@@ -79,7 +78,7 @@ func (c *client) SaveUrl(url string) error {
|
||||
"query": mutation,
|
||||
"variables": map[string]interface{}{
|
||||
"input": map[string]interface{}{
|
||||
"clientRequestId": uuid.New().String(),
|
||||
"clientRequestId": crypto.GenerateUUID(),
|
||||
"source": "api",
|
||||
"url": url,
|
||||
},
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
package pinboard // import "miniflux.app/v2/internal/integration/pinboard"
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -12,6 +14,9 @@ import (
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
var errPostNotFound = fmt.Errorf("pinboard: post not found")
|
||||
var errMissingCredentials = fmt.Errorf("pinboard: missing auth token")
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
@@ -24,20 +29,27 @@ func NewClient(authToken string) *Client {
|
||||
|
||||
func (c *Client) CreateBookmark(entryURL, entryTitle, pinboardTags string, markAsUnread bool) error {
|
||||
if c.authToken == "" {
|
||||
return fmt.Errorf("pinboard: missing auth token")
|
||||
return errMissingCredentials
|
||||
}
|
||||
|
||||
toRead := "no"
|
||||
// We check if the url is already bookmarked to avoid overriding existing data.
|
||||
post, err := c.getBookmark(entryURL)
|
||||
|
||||
if err != nil && errors.Is(err, errPostNotFound) {
|
||||
post = NewPost(entryURL, entryTitle)
|
||||
} else if err != nil {
|
||||
// In case of any other error, we return immediately to avoid overriding existing data.
|
||||
return err
|
||||
}
|
||||
|
||||
post.addTag(pinboardTags)
|
||||
if markAsUnread {
|
||||
toRead = "yes"
|
||||
post.SetToread()
|
||||
}
|
||||
|
||||
values := url.Values{}
|
||||
values.Add("auth_token", c.authToken)
|
||||
values.Add("url", entryURL)
|
||||
values.Add("description", entryTitle)
|
||||
values.Add("tags", pinboardTags)
|
||||
values.Add("toread", toRead)
|
||||
post.AddValues(values)
|
||||
|
||||
apiEndpoint := "https://api.pinboard.in/v1/posts/add?" + values.Encode()
|
||||
request, err := http.NewRequest(http.MethodGet, apiEndpoint, nil)
|
||||
@@ -61,3 +73,46 @@ func (c *Client) CreateBookmark(entryURL, entryTitle, pinboardTags string, markA
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBookmark fetches a bookmark from Pinboard. https://www.pinboard.in/api/#posts_get
|
||||
func (c *Client) getBookmark(entryURL string) (*Post, error) {
|
||||
if c.authToken == "" {
|
||||
return nil, errMissingCredentials
|
||||
}
|
||||
|
||||
values := url.Values{}
|
||||
values.Add("auth_token", c.authToken)
|
||||
values.Add("url", entryURL)
|
||||
|
||||
apiEndpoint := "https://api.pinboard.in/v1/posts/get?" + values.Encode()
|
||||
request, err := http.NewRequest(http.MethodGet, apiEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pinboard: unable to create request: %v", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
|
||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pinboard: unable fetch bookmark: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("pinboard: unable to fetch bookmark, status=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
var results posts
|
||||
err = xml.NewDecoder(response.Body).Decode(&results)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pinboard: unable to decode XML: %v", err)
|
||||
}
|
||||
|
||||
if len(results.Posts) == 0 {
|
||||
return nil, errPostNotFound
|
||||
}
|
||||
|
||||
return &results.Posts[0], nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pinboard // import "miniflux.app/v2/internal/integration/pinboard"
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Post a Pinboard bookmark. "inspiration" from https://github.com/drags/pinboard/blob/master/posts.go#L32-L42
|
||||
type Post struct {
|
||||
XMLName xml.Name `xml:"post"`
|
||||
Url string `xml:"href,attr"`
|
||||
Description string `xml:"description,attr"`
|
||||
Tags string `xml:"tag,attr"`
|
||||
Extended string `xml:"extended,attr"`
|
||||
Date time.Time `xml:"time,attr"`
|
||||
Shared string `xml:"shared,attr"`
|
||||
Toread string `xml:"toread,attr"`
|
||||
}
|
||||
|
||||
// Posts A result of a Pinboard API call
|
||||
type posts struct {
|
||||
XMLName xml.Name `xml:"posts"`
|
||||
Posts []Post `xml:"post"`
|
||||
}
|
||||
|
||||
func NewPost(url string, description string) *Post {
|
||||
return &Post{
|
||||
Url: url,
|
||||
Description: description,
|
||||
Date: time.Now(),
|
||||
Toread: "no",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Post) addTag(tag string) {
|
||||
if !strings.Contains(p.Tags, tag) {
|
||||
p.Tags += " " + tag
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Post) SetToread() {
|
||||
p.Toread = "yes"
|
||||
}
|
||||
|
||||
func (p *Post) AddValues(values url.Values) {
|
||||
values.Add("url", p.Url)
|
||||
values.Add("description", p.Description)
|
||||
values.Add("tags", p.Tags)
|
||||
if p.Toread != "" {
|
||||
values.Add("toread", p.Toread)
|
||||
}
|
||||
if p.Shared != "" {
|
||||
values.Add("shared", p.Shared)
|
||||
}
|
||||
values.Add("dt", p.Date.Format(time.RFC3339))
|
||||
values.Add("extended", p.Extended)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package raindrop // import "miniflux.app/v2/internal/integration/raindrop"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
token string
|
||||
collectionID string
|
||||
tags []string
|
||||
}
|
||||
|
||||
func NewClient(token, collectionID, tags string) *Client {
|
||||
return &Client{token: token, collectionID: collectionID, tags: strings.Split(tags, ",")}
|
||||
}
|
||||
|
||||
// https://developer.raindrop.io/v1/raindrops/single#create-raindrop
|
||||
func (c *Client) CreateRaindrop(entryURL, entryTitle string) error {
|
||||
if c.token == "" {
|
||||
return fmt.Errorf("raindrop: missing token")
|
||||
}
|
||||
|
||||
var request *http.Request
|
||||
requestBodyJson, err := json.Marshal(&raindrop{
|
||||
Link: entryURL,
|
||||
Title: entryTitle,
|
||||
Collection: collection{Id: c.collectionID},
|
||||
Tags: c.tags,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("raindrop: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
request, err = http.NewRequest(http.MethodPost, "https://api.raindrop.io/rest/v1/raindrop", bytes.NewReader(requestBodyJson))
|
||||
if err != nil {
|
||||
return fmt.Errorf("raindrop: unable to create request: %v", err)
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
request.Header.Set("Authorization", "Bearer "+c.token)
|
||||
|
||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("raindrop: unable to send request: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
return fmt.Errorf("raindrop: unable to create bookmark: status=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type raindrop struct {
|
||||
Link string `json:"link"`
|
||||
Title string `json:"title"`
|
||||
Collection collection `json:"collection,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type collection struct {
|
||||
Id string `json:"$id"`
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package readeck // import "miniflux.app/v2/internal/integration/readeck"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
labels string
|
||||
onlyURL bool
|
||||
}
|
||||
|
||||
func NewClient(baseURL, apiKey, labels string, onlyURL bool) *Client {
|
||||
return &Client{baseURL: baseURL, apiKey: apiKey, labels: labels, onlyURL: onlyURL}
|
||||
}
|
||||
|
||||
func (c *Client) CreateBookmark(entryURL, entryTitle string, entryContent string) error {
|
||||
if c.baseURL == "" || c.apiKey == "" {
|
||||
return fmt.Errorf("readeck: missing base URL or API key")
|
||||
}
|
||||
|
||||
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/bookmarks/")
|
||||
if err != nil {
|
||||
return fmt.Errorf(`readeck: invalid API endpoint: %v`, err)
|
||||
}
|
||||
|
||||
labelsSplitFn := func(c rune) bool {
|
||||
return c == ',' || c == ' '
|
||||
}
|
||||
labelsSplit := strings.FieldsFunc(c.labels, labelsSplitFn)
|
||||
|
||||
var request *http.Request
|
||||
if c.onlyURL {
|
||||
requestBodyJson, err := json.Marshal(&readeckBookmark{
|
||||
Url: entryURL,
|
||||
Title: entryTitle,
|
||||
Labels: labelsSplit,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("readeck: unable to encode request body: %v", err)
|
||||
}
|
||||
request, err = http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBodyJson))
|
||||
if err != nil {
|
||||
return fmt.Errorf("readeck: unable to create request: %v", err)
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
requestBody := new(bytes.Buffer)
|
||||
multipartWriter := multipart.NewWriter(requestBody)
|
||||
|
||||
urlPart, err := multipartWriter.CreateFormField("url")
|
||||
if err != nil {
|
||||
return fmt.Errorf("readeck: unable to encode request body (entry url): %v", err)
|
||||
}
|
||||
urlPart.Write([]byte(entryURL))
|
||||
|
||||
titlePart, err := multipartWriter.CreateFormField("title")
|
||||
if err != nil {
|
||||
return fmt.Errorf("readeck: unable to encode request body (entry title): %v", err)
|
||||
}
|
||||
titlePart.Write([]byte(entryTitle))
|
||||
|
||||
featurePart, err := multipartWriter.CreateFormField("feature_find_main")
|
||||
if err != nil {
|
||||
return fmt.Errorf("readeck: unable to encode request body (feature_find_main flag): %v", err)
|
||||
}
|
||||
featurePart.Write([]byte("false")) // false to disable readability
|
||||
|
||||
for _, label := range labelsSplit {
|
||||
labelPart, err := multipartWriter.CreateFormField("labels")
|
||||
if err != nil {
|
||||
return fmt.Errorf("readeck: unable to encode request body (entry labels): %v", err)
|
||||
}
|
||||
labelPart.Write([]byte(label))
|
||||
}
|
||||
|
||||
contentBodyHeader, err := json.Marshal(&partContentHeader{
|
||||
Url: entryURL,
|
||||
ContentHeader: contentHeader{ContentType: "text/html; charset=utf-8"},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("readeck: unable to encode request body (entry content header): %v", err)
|
||||
}
|
||||
|
||||
contentPart, err := multipartWriter.CreateFormFile("resource", "blob")
|
||||
if err != nil {
|
||||
return fmt.Errorf("readeck: unable to encode request body (entry content): %v", err)
|
||||
}
|
||||
contentPart.Write(contentBodyHeader)
|
||||
contentPart.Write([]byte("\n"))
|
||||
contentPart.Write([]byte(entryContent))
|
||||
|
||||
err = multipartWriter.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("readeck: unable to encode request body: %v", err)
|
||||
}
|
||||
request, err = http.NewRequest(http.MethodPost, apiEndpoint, requestBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("readeck: unable to create request: %v", err)
|
||||
}
|
||||
request.Header.Set("Content-Type", multipartWriter.FormDataContentType())
|
||||
}
|
||||
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
request.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
|
||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("readeck: unable to send request: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
return fmt.Errorf("readeck: unable to create bookmark: url=%s status=%d", apiEndpoint, response.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type readeckBookmark struct {
|
||||
Url string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
type contentHeader struct {
|
||||
ContentType string `json:"content-type"`
|
||||
}
|
||||
|
||||
type partContentHeader struct {
|
||||
Url string `json:"url"`
|
||||
ContentHeader contentHeader `json:"headers"`
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package rssbridge // import "miniflux.app/integration/rssbridge"
|
||||
package rssbridge // import "miniflux.app/v2/internal/integration/rssbridge"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
@@ -74,14 +73,15 @@ func (c *Client) CreateLink(entryURL, entryTitle string) error {
|
||||
}
|
||||
|
||||
func (c *Client) generateBearerToken() string {
|
||||
header := strings.TrimRight(base64.URLEncoding.EncodeToString([]byte(`{"typ":"JWT", "alg":"HS256"}`)), "=")
|
||||
payload := strings.TrimRight(base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf(`{"iat": %d}`, time.Now().Unix()))), "=")
|
||||
header := base64.RawURLEncoding.EncodeToString([]byte(`{"typ":"JWT","alg":"HS512"}`))
|
||||
payload := base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf(`{"iat":%d}`, time.Now().Unix())))
|
||||
data := header + "." + payload
|
||||
|
||||
mac := hmac.New(sha512.New, []byte(c.apiSecret))
|
||||
mac.Write([]byte(header + "." + payload))
|
||||
signature := strings.TrimRight(base64.URLEncoding.EncodeToString(mac.Sum(nil)), "=")
|
||||
mac.Write([]byte(data))
|
||||
signature := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
|
||||
return header + "." + payload + "." + signature
|
||||
return data + "." + signature
|
||||
}
|
||||
|
||||
type addLinkRequest struct {
|
||||
|
||||
@@ -31,7 +31,7 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
return fmt.Errorf("shiori: missing base URL, username or password")
|
||||
}
|
||||
|
||||
sessionID, err := c.authenticate()
|
||||
token, err := c.authenticate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("shiori: unable to authenticate: %v", err)
|
||||
}
|
||||
@@ -44,7 +44,11 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
requestBody, err := json.Marshal(&addBookmarkRequest{
|
||||
URL: entryURL,
|
||||
Title: entryTitle,
|
||||
Excerpt: "",
|
||||
CreateArchive: true,
|
||||
CreateEbook: false,
|
||||
Public: 0,
|
||||
Tags: make([]string, 0),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -58,7 +62,7 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
request.Header.Set("X-Session-Id", sessionID)
|
||||
request.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
||||
|
||||
@@ -75,13 +79,13 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) authenticate() (sessionID string, err error) {
|
||||
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/login")
|
||||
func (c *Client) authenticate() (token string, err error) {
|
||||
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/v1/auth/login")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("shiori: invalid API endpoint: %v", err)
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(&authRequest{Username: c.username, Password: c.password})
|
||||
requestBody, err := json.Marshal(&authRequest{Username: c.username, Password: c.password, RememberMe: false})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("shiori: unable to encode request body: %v", err)
|
||||
}
|
||||
@@ -111,21 +115,31 @@ func (c *Client) authenticate() (sessionID string, err error) {
|
||||
if err := json.NewDecoder(response.Body).Decode(&authResponse); err != nil {
|
||||
return "", fmt.Errorf("shiori: unable to decode response: %v", err)
|
||||
}
|
||||
|
||||
return authResponse.SessionID, nil
|
||||
return authResponse.Message.Token, nil
|
||||
}
|
||||
|
||||
type authRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
RememberMe bool `json:"remember_me"`
|
||||
}
|
||||
|
||||
type authResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Message authResponseMessage `json:"message"`
|
||||
}
|
||||
|
||||
type authResponseMessage struct {
|
||||
SessionID string `json:"session"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type addBookmarkRequest struct {
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
CreateArchive bool `json:"createArchive"`
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
CreateArchive bool `json:"create_archive"`
|
||||
CreateEbook bool `json:"create_ebook"`
|
||||
Public int `json:"public"`
|
||||
Excerpt string `json:"excerpt"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
|
||||
func PushEntry(feed *model.Feed, entry *model.Entry, botToken, chatID string, topicID *int64, disableWebPagePreview, disableNotification bool, disableButtons bool) error {
|
||||
formattedText := fmt.Sprintf(
|
||||
`<a href=%q>%s</a>`,
|
||||
`<b>%s</b> - <a href=%q>%s</a>`,
|
||||
feed.Title,
|
||||
entry.URL,
|
||||
entry.Title,
|
||||
)
|
||||
|
||||
@@ -57,6 +57,7 @@ func (c *Client) SendSaveEntryWebhookEvent(entry *model.Entry) error {
|
||||
ID: entry.Feed.ID,
|
||||
UserID: entry.Feed.UserID,
|
||||
CategoryID: entry.Feed.Category.ID,
|
||||
Category: &WebhookCategory{ID: entry.Feed.Category.ID, Title: entry.Feed.Category.Title},
|
||||
FeedURL: entry.Feed.FeedURL,
|
||||
SiteURL: entry.Feed.SiteURL,
|
||||
Title: entry.Feed.Title,
|
||||
@@ -94,13 +95,13 @@ func (c *Client) SendNewEntriesWebhookEvent(feed *model.Feed, entries model.Entr
|
||||
Tags: entry.Tags,
|
||||
})
|
||||
}
|
||||
|
||||
return c.makeRequest(NewEntriesEventType, &WebhookNewEntriesEvent{
|
||||
EventType: NewEntriesEventType,
|
||||
Feed: &WebhookFeed{
|
||||
ID: feed.ID,
|
||||
UserID: feed.UserID,
|
||||
CategoryID: feed.Category.ID,
|
||||
Category: &WebhookCategory{ID: feed.Category.ID, Title: feed.Category.Title},
|
||||
FeedURL: feed.FeedURL,
|
||||
SiteURL: feed.SiteURL,
|
||||
Title: feed.Title,
|
||||
@@ -145,13 +146,19 @@ func (c *Client) makeRequest(eventType string, payload any) error {
|
||||
}
|
||||
|
||||
type WebhookFeed struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
FeedURL string `json:"feed_url"`
|
||||
SiteURL string `json:"site_url"`
|
||||
Title string `json:"title"`
|
||||
CheckedAt time.Time `json:"checked_at"`
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
Category *WebhookCategory `json:"category,omitempty"`
|
||||
FeedURL string `json:"feed_url"`
|
||||
SiteURL string `json:"site_url"`
|
||||
Title string `json:"title"`
|
||||
CheckedAt time.Time `json:"checked_at"`
|
||||
}
|
||||
|
||||
type WebhookCategory struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
type WebhookEntry struct {
|
||||
|
||||
@@ -20,7 +20,7 @@ var translationFiles embed.FS
|
||||
// LoadCatalogMessages loads and parses all translations encoded in JSON.
|
||||
func LoadCatalogMessages() error {
|
||||
var err error
|
||||
defaultCatalog = make(catalog)
|
||||
defaultCatalog = make(catalog, len(AvailableLanguages()))
|
||||
|
||||
for language := range AvailableLanguages() {
|
||||
defaultCatalog[language], err = loadTranslationFile(language)
|
||||
|
||||
@@ -53,11 +53,11 @@ func TestAllKeysHaveValue(t *testing.T) {
|
||||
switch value := v.(type) {
|
||||
case string:
|
||||
if value == "" {
|
||||
t.Errorf(`The key %q for the language %q have an empty string as value`, k, language)
|
||||
t.Errorf(`The key %q for the language %q has an empty string as value`, k, language)
|
||||
}
|
||||
case []string:
|
||||
case []any:
|
||||
if len(value) == 0 {
|
||||
t.Errorf(`The key %q for the language %q have an empty list as value`, k, language)
|
||||
t.Errorf(`The key %q for the language %q has an empty list as value`, k, language)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,3 +88,20 @@ func TestMissingTranslations(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslationFilePluralForms(t *testing.T) {
|
||||
for language := range AvailableLanguages() {
|
||||
messages, err := loadTranslationFile(language)
|
||||
if err != nil {
|
||||
t.Fatalf(`Unable to load translation messages for language %q`, language)
|
||||
}
|
||||
|
||||
for k, v := range messages {
|
||||
if value, ok := v.([]any); ok {
|
||||
if len(value) != numberOfPluralFormsPerLanguage[language] {
|
||||
t.Errorf(`The key %q for the language %q does not have the expected number of plurals, got %d instead of %d`, k, language, len(value), numberOfPluralFormsPerLanguage[language])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,27 @@
|
||||
|
||||
package locale // import "miniflux.app/v2/internal/locale"
|
||||
|
||||
var numberOfPluralFormsPerLanguage = map[string]int{
|
||||
"en_US": 2,
|
||||
"es_ES": 2,
|
||||
"fr_FR": 2,
|
||||
"de_DE": 2,
|
||||
"pl_PL": 3,
|
||||
"pt_BR": 2,
|
||||
"zh_CN": 1,
|
||||
"zh_TW": 1,
|
||||
"nl_NL": 2,
|
||||
"ru_RU": 3,
|
||||
"it_IT": 2,
|
||||
"ja_JP": 1,
|
||||
"tr_TR": 2,
|
||||
"el_EL": 2,
|
||||
"fi_FI": 2,
|
||||
"hi_IN": 2,
|
||||
"uk_UA": 3,
|
||||
"id_ID": 1,
|
||||
}
|
||||
|
||||
// AvailableLanguages returns the list of available languages.
|
||||
func AvailableLanguages() map[string]string {
|
||||
return map[string]string{
|
||||
|
||||
+37
-33
@@ -3,69 +3,65 @@
|
||||
|
||||
package locale // import "miniflux.app/v2/internal/locale"
|
||||
|
||||
type pluralFormFunc func(n int) int
|
||||
|
||||
// See https://localization-guide.readthedocs.io/en/latest/l10n/pluralforms.html
|
||||
// And http://www.unicode.org/cldr/charts/29/supplemental/language_plural_rules.html
|
||||
var pluralForms = map[string]pluralFormFunc{
|
||||
var pluralForms = map[string](func(n int) int){
|
||||
// nplurals=2; plural=(n != 1);
|
||||
"default": func(n int) int {
|
||||
if n != 1 {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
},
|
||||
// nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5);
|
||||
"ar_AR": func(n int) int {
|
||||
if n == 0 {
|
||||
switch {
|
||||
case n == 0:
|
||||
return 0
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
case n == 1:
|
||||
return 1
|
||||
}
|
||||
|
||||
if n == 2 {
|
||||
case n == 2:
|
||||
return 2
|
||||
}
|
||||
|
||||
if n%100 >= 3 && n%100 <= 10 {
|
||||
case n%100 >= 3 && n%100 <= 10:
|
||||
return 3
|
||||
}
|
||||
|
||||
if n%100 >= 11 {
|
||||
case n%100 >= 11:
|
||||
return 4
|
||||
}
|
||||
|
||||
return 5
|
||||
},
|
||||
// nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;
|
||||
"cs_CZ": func(n int) int {
|
||||
if n == 1 {
|
||||
switch {
|
||||
case n == 1:
|
||||
return 0
|
||||
}
|
||||
|
||||
if n >= 2 && n <= 4 {
|
||||
case n >= 2 && n <= 4:
|
||||
return 1
|
||||
}
|
||||
|
||||
return 2
|
||||
},
|
||||
// nplurals=2; plural=(n > 1);
|
||||
"fr_FR": func(n int) int {
|
||||
if n > 1 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
},
|
||||
// nplurals=1; plural=0;
|
||||
"id_ID": func(n int) int {
|
||||
return 0
|
||||
},
|
||||
// nplurals=1; plural=0;
|
||||
"ja_JP": func(n int) int {
|
||||
return 0
|
||||
},
|
||||
// nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
|
||||
"pl_PL": func(n int) int {
|
||||
if n == 1 {
|
||||
switch {
|
||||
case n == 1:
|
||||
return 0
|
||||
}
|
||||
|
||||
if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) {
|
||||
case n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20):
|
||||
return 1
|
||||
}
|
||||
|
||||
return 2
|
||||
},
|
||||
// nplurals=2; plural=(n > 1);
|
||||
@@ -76,23 +72,31 @@ var pluralForms = map[string]pluralFormFunc{
|
||||
return 0
|
||||
},
|
||||
"ru_RU": pluralFormRuSrUa,
|
||||
// nplurals=2; plural=(n > 1);
|
||||
"tr_TR": func(n int) int {
|
||||
if n > 1 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
},
|
||||
"uk_UA": pluralFormRuSrUa,
|
||||
"sr_RS": pluralFormRuSrUa,
|
||||
// nplurals=1; plural=0;
|
||||
"zh_CN": func(n int) int {
|
||||
return 0
|
||||
},
|
||||
"zh_TW": func(n int) int {
|
||||
return 0
|
||||
},
|
||||
}
|
||||
|
||||
// nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
|
||||
func pluralFormRuSrUa(n int) int {
|
||||
if n%10 == 1 && n%100 != 11 {
|
||||
switch {
|
||||
case n%10 == 1 && n%100 != 11:
|
||||
return 0
|
||||
}
|
||||
|
||||
if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) {
|
||||
case n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20):
|
||||
return 1
|
||||
}
|
||||
|
||||
return 2
|
||||
}
|
||||
|
||||
@@ -25,6 +25,20 @@ func TestPluralRules(t *testing.T) {
|
||||
2: 1,
|
||||
5: 2,
|
||||
},
|
||||
"fr_FR": {
|
||||
1: 0,
|
||||
2: 1,
|
||||
5: 1,
|
||||
},
|
||||
"id_ID": {
|
||||
1: 0,
|
||||
5: 0,
|
||||
},
|
||||
"ja_JP": {
|
||||
1: 0,
|
||||
2: 0,
|
||||
5: 0,
|
||||
},
|
||||
"pl_PL": {
|
||||
1: 0,
|
||||
2: 1,
|
||||
@@ -45,10 +59,24 @@ func TestPluralRules(t *testing.T) {
|
||||
2: 1,
|
||||
5: 2,
|
||||
},
|
||||
"tr_TR": {
|
||||
1: 0,
|
||||
2: 1,
|
||||
5: 1,
|
||||
},
|
||||
"uk_UA": {
|
||||
1: 0,
|
||||
2: 1,
|
||||
5: 2,
|
||||
},
|
||||
"zh_CN": {
|
||||
1: 0,
|
||||
5: 0,
|
||||
},
|
||||
"zh_TW": {
|
||||
1: 0,
|
||||
5: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for rule, values := range scenarios {
|
||||
|
||||
@@ -10,6 +10,15 @@ type Printer struct {
|
||||
language string
|
||||
}
|
||||
|
||||
func (p *Printer) Print(key string) string {
|
||||
if str, ok := defaultCatalog[p.language][key]; ok {
|
||||
if translation, ok := str.(string); ok {
|
||||
return translation
|
||||
}
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// Printf is like fmt.Printf, but using language-specific formatting.
|
||||
func (p *Printer) Printf(key string, args ...interface{}) string {
|
||||
var translation string
|
||||
|
||||
@@ -35,11 +35,13 @@
|
||||
"menu.about": "Über",
|
||||
"menu.export": "Exportieren",
|
||||
"menu.import": "Importieren",
|
||||
"menu.search": "Suche",
|
||||
"menu.create_category": "Kategorie anlegen",
|
||||
"menu.mark_page_as_read": "Diese Seite als gelesen markieren",
|
||||
"menu.mark_all_as_read": "Alle als gelesen markieren",
|
||||
"menu.show_all_entries": "Zeige alle Artikel",
|
||||
"menu.show_only_unread_entries": "Nur ungelesene Artikel anzeigen",
|
||||
"menu.show_only_starred_entries": "Nur markierte Artikel anzeigen",
|
||||
"menu.refresh_feed": "Aktualisieren",
|
||||
"menu.refresh_all_feeds": "Alle Abonnements im Hintergrund aktualisieren",
|
||||
"menu.edit_feed": "Bearbeiten",
|
||||
@@ -54,7 +56,9 @@
|
||||
"search.label": "Suche",
|
||||
"search.placeholder": "Suche...",
|
||||
"search.submit": "Search",
|
||||
"pagination.last": "Last",
|
||||
"pagination.next": "Nächste",
|
||||
"pagination.first": "First",
|
||||
"pagination.previous": "Vorherige",
|
||||
"entry.status.unread": "Ungelesen",
|
||||
"entry.status.read": "Gelesen",
|
||||
@@ -177,6 +181,8 @@
|
||||
"page.keyboard_shortcuts.go_to_feed": "Zum Abonnement gehen",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "Zur vorherigen Seite gehen",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "Zur nächsten Seite gehen",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "Gehen Sie zum untersten Element",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "Zum obersten Artikel gehen",
|
||||
"page.keyboard_shortcuts.open_item": "Gewählten Artikel öffnen",
|
||||
"page.keyboard_shortcuts.open_original": "Original-Artikel öffnen",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "Öffne den Original-Link in der aktuellen Registerkarte",
|
||||
@@ -205,8 +211,8 @@
|
||||
"page.settings.title": "Einstellungen",
|
||||
"page.settings.link_google_account": "Google-Konto verknüpfen",
|
||||
"page.settings.unlink_google_account": "Verknüpfung mit Google-Konto entfernen",
|
||||
"page.settings.link_oidc_account": "OpenID-Connect-Konto verknüpfen",
|
||||
"page.settings.unlink_oidc_account": "Verknüpfung mit OpenID-Connect-Konto entfernen",
|
||||
"page.settings.link_oidc_account": "%s-Konto verknüpfen",
|
||||
"page.settings.unlink_oidc_account": "Verknüpfung mit %s-Konto entfernen",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "Aktionen",
|
||||
"page.settings.webauthn.passkey_name": "Name des Passkeys",
|
||||
@@ -214,13 +220,14 @@
|
||||
"page.settings.webauthn.last_seen_on": "Zuletzt genutzt",
|
||||
"page.settings.webauthn.register": "Hauptschlüssel registrieren",
|
||||
"page.settings.webauthn.register.error": "Hauptschlüssel kann nicht registriert werden",
|
||||
"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.settings.webauthn.delete": [
|
||||
"Entfernen Sie %d Hauptschlüssel",
|
||||
"%d Hauptschlüssel entfernen"
|
||||
],
|
||||
"page.login.title": "Anmeldung",
|
||||
"page.login.google_signin": "Anmeldung mit Google",
|
||||
"page.login.oidc_signin": "Anmeldung mit OpenID Connect",
|
||||
"page.login.oidc_signin": "Anmeldung mit %s",
|
||||
"page.login.webauthn_login": "Melden Sie sich mit dem Passkey an",
|
||||
"page.login.webauthn_login.error": "Anmeldung mit Passkey nicht möglich",
|
||||
"page.integrations.title": "Dienste",
|
||||
@@ -255,6 +262,7 @@
|
||||
"alert.no_bookmark": "Es existiert derzeit kein Lesezeichen.",
|
||||
"alert.no_category": "Es ist keine Kategorie vorhanden.",
|
||||
"alert.no_category_entry": "Es befindet sich kein Artikel in dieser Kategorie.",
|
||||
"alert.no_tag_entry": "Es gibt keine Artikel, die diesem Tag entsprechen.",
|
||||
"alert.no_feed_entry": "Es existiert kein Artikel für dieses Abonnement.",
|
||||
"alert.no_feed": "Es sind keine Abonnements vorhanden.",
|
||||
"alert.no_feed_in_category": "Für diese Kategorie gibt es kein Abonnement.",
|
||||
@@ -296,6 +304,14 @@
|
||||
"error.password_min_length": "Wenigstens 6 Zeichen müssen genutzt werden.",
|
||||
"error.settings_mandatory_fields": "Die Felder für Benutzername, Thema, Sprache und Zeitzone sind obligatorisch.",
|
||||
"error.settings_reading_speed_is_positive": "Die Lesegeschwindigkeiten müssen positive ganze Zahlen sein.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.entries_per_page_invalid": "Die Anzahl der Einträge pro Seite ist ungültig.",
|
||||
"error.feed_mandatory_fields": "Die URL und die Kategorie sind obligatorisch.",
|
||||
"error.feed_already_exists": "Dieser Feed existiert bereits.",
|
||||
@@ -313,6 +329,7 @@
|
||||
"form.feed.label.title": "Titel",
|
||||
"form.feed.label.site_url": "URL der Webseite",
|
||||
"form.feed.label.feed_url": "URL des Abonnements",
|
||||
"form.feed.label.description": "Beschreibung",
|
||||
"form.feed.label.category": "Kategorie",
|
||||
"form.feed.label.crawler": "Originalinhalt herunterladen",
|
||||
"form.feed.label.feed_username": "Benutzername des Abonnements",
|
||||
@@ -327,10 +344,20 @@
|
||||
"form.feed.label.urlrewrite_rules": "Umschreibregeln für URL",
|
||||
"form.feed.label.ignore_http_cache": "Ignoriere HTTP-Cache",
|
||||
"form.feed.label.allow_self_signed_certificates": "Erlaube selbstsignierte oder ungültige Zertifikate",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.fetch_via_proxy": "Über Proxy abrufen",
|
||||
"form.feed.label.disabled": "Dieses Abonnement nicht aktualisieren",
|
||||
"form.feed.label.no_media_player": "Kein Media-Player (Audio/Video)",
|
||||
"form.feed.label.hide_globally": "Einträge in der globalen Ungelesen-Liste ausblenden",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.feed.fieldset.general": "Allgemein",
|
||||
"form.feed.fieldset.rules": "Regeln",
|
||||
"form.feed.fieldset.network_settings": "Netzwerkeinstellungen",
|
||||
@@ -367,15 +394,26 @@
|
||||
"form.prefs.label.gesture_nav": "Geste zum Navigieren zwischen Einträgen",
|
||||
"form.prefs.label.show_reading_time": "Geschätzte Lesezeit für Artikel anzeigen",
|
||||
"form.prefs.label.custom_css": "Benutzerdefiniertes CSS",
|
||||
"form.prefs.label.custom_js": "Benutzerdefiniertes JavaScript",
|
||||
"form.prefs.label.entry_order": "Artikel-Sortierspalte",
|
||||
"form.prefs.label.default_home_page": "Standard-Startseite",
|
||||
"form.prefs.label.categories_sorting_order": "Kategorie-Sortierung",
|
||||
"form.prefs.label.mark_read_on_view": "Einträge automatisch als gelesen markieren, wenn sie angezeigt werden",
|
||||
"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_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.fieldset.application_settings": "Anwendungseinstellungen",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentifizierungseinstellungen",
|
||||
"form.prefs.fieldset.reader_settings": "Reader-Einstellungen",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.label.external_font_hosts": "Externe Schriftarten-Hosts",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"form.import.label.file": "OPML Datei",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.fever_activate": "Fever API aktivieren",
|
||||
"form.integration.fever_username": "Fever Benutzername",
|
||||
"form.integration.fever_password": "Fever Passwort",
|
||||
@@ -447,6 +485,15 @@
|
||||
"form.integration.matrix_bot_password": "Passwort für Matrix-Benutzer",
|
||||
"form.integration.matrix_bot_url": "URL des Matrix-Servers",
|
||||
"form.integration.matrix_bot_chat_id": "ID des Matrix-Raums",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.readeck_activate": "Artikel in Readeck speichern",
|
||||
"form.integration.readeck_endpoint": "Readeck API-Endpunkt",
|
||||
"form.integration.readeck_api_key": "Readeck API-Schlüssel",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "Nur URL senden (anstelle des vollständigen Inhalts)",
|
||||
"form.integration.shiori_activate": "Artikel in Shiori speichern",
|
||||
"form.integration.shiori_endpoint": "Shiori API-Endpunkt",
|
||||
"form.integration.shiori_username": "Shiori Benutzername",
|
||||
@@ -459,6 +506,13 @@
|
||||
"form.integration.webhook_secret": "Webhook Geheimnis",
|
||||
"form.integration.rssbridge_activate": "Beim Hinzufügen von Abonnements RSS-Bridge prüfen.",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_topic": "Ntfy topic",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.api_key.label.description": "API-Schlüsselbezeichnung",
|
||||
"form.submit.loading": "Lade...",
|
||||
"form.submit.saving": "Speichern...",
|
||||
@@ -498,7 +552,7 @@
|
||||
"error.http_body_read": "Der HTTP-Inhalt kann nicht gelesen werden: %v",
|
||||
"error.http_empty_response_body": "Der Inhalt der HTTP-Antwort ist leer.",
|
||||
"error.http_empty_response": "Die HTTP-Antwort ist leer. Vielleicht versucht die Webseite, sich vor Bots zu schützen?",
|
||||
"error.tls_error": "TLS-Fehler: %v. Wenn Sie mögen, können Sie versuchen die TLS-Verifizierung in den Einstellungen des Abonnements zu deaktivieren.",
|
||||
"error.tls_error": "TLS-Fehler: %q. Wenn Sie mögen, können Sie versuchen die TLS-Verifizierung in den Einstellungen des Abonnements zu deaktivieren.",
|
||||
"error.network_operation": "Miniflux kann die Webseite aufgrund eines Netzwerk-Fehlers nicht erreichen: %v",
|
||||
"error.network_timeout": "Die Webseite ist zu langsam und die Anfrage ist abgelaufen: %v.",
|
||||
"error.http_client_error": "HTTP-Client-Fehler: %v.",
|
||||
@@ -517,5 +571,16 @@
|
||||
"error.unable_to_parse_feed": "Dieses Abonnement kann nicht gelesen werden: %v.",
|
||||
"error.feed_not_found": "Dieses Abonnement existiert nicht oder gehört nicht zu diesem Benutzer.",
|
||||
"error.unable_to_detect_rssbridge": "Abonnement kann nicht durch RSS-Bridge erkannt werden: %v.",
|
||||
"error.feed_format_not_detected": "Das Format des Abonnements kann nicht erkannt werden: %v."
|
||||
"error.feed_format_not_detected": "Das Format des Abonnements kann nicht erkannt werden: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Wiedergabegeschwindigkeit von Audio/Video",
|
||||
"error.settings_media_playback_rate_range": "Die Wiedergabegeschwindigkeit liegt außerhalb des Bereichs",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
||||
@@ -35,11 +35,13 @@
|
||||
"menu.about": "Περί",
|
||||
"menu.export": "Εξαγωγή",
|
||||
"menu.import": "Εισαγωγή",
|
||||
"menu.search": "Αναζήτηση",
|
||||
"menu.create_category": "Δημιουργήστε μια κατηγορία",
|
||||
"menu.mark_page_as_read": "Σημείωση αυτής της σελίδας ως αναγνωσμένη",
|
||||
"menu.mark_all_as_read": "Σημείωση όλων ως αναγνωσμένα",
|
||||
"menu.show_all_entries": "Εμφάνιση όλων των καταχωρήσεων",
|
||||
"menu.show_only_unread_entries": "Εμφάνιση μόνο μη αναγνωσμένων καταχωρήσεων",
|
||||
"menu.show_only_starred_entries": "Εμφάνιση μόνο αγαπημένων καταχωρήσεων",
|
||||
"menu.refresh_feed": "Ανανέωση",
|
||||
"menu.refresh_all_feeds": "Ανανέωση όλων των ροών στο παρασκήνιο",
|
||||
"menu.edit_feed": "Επεξεργασία",
|
||||
@@ -54,7 +56,9 @@
|
||||
"search.label": "Αναζήτηση",
|
||||
"search.placeholder": "Αναζήτηση...",
|
||||
"search.submit": "Search",
|
||||
"pagination.last": "Last",
|
||||
"pagination.next": "Επόμενη",
|
||||
"pagination.first": "First",
|
||||
"pagination.previous": "Προηγούμενη",
|
||||
"entry.status.unread": "Μη αναγνωσμένο",
|
||||
"entry.status.read": "Αναγνωσμένο",
|
||||
@@ -177,6 +181,8 @@
|
||||
"page.keyboard_shortcuts.go_to_feed": "Πηγαίνετε στη ροή",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "Μετάβαση στην προηγούμενη σελίδα",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "Μετάβαση στην επόμενη σελίδα",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "Μετάβαση στο κάτω στοιχείο",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "Μετάβαση στο επάνω στοιχείο",
|
||||
"page.keyboard_shortcuts.open_item": "Άνοιγμα επιλεγμένου στοιχείου",
|
||||
"page.keyboard_shortcuts.open_original": "Άνοιγμα αρχικού συνδέσμου",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "Άνοιγμα αρχικού συνδέσμου στην τρέχουσα καρτέλα",
|
||||
@@ -205,8 +211,8 @@
|
||||
"page.settings.title": "Ρυθμίσεις",
|
||||
"page.settings.link_google_account": "Σύνδεση του λογαριασμό μου Google",
|
||||
"page.settings.unlink_google_account": "Αποσύνδεση του λογαριασμού μου Google",
|
||||
"page.settings.link_oidc_account": "Σύνδεση του λογαριασμού μου OpenID Connect",
|
||||
"page.settings.unlink_oidc_account": "Αποσύνδεση του λογαριασμού μου OpenID Connect",
|
||||
"page.settings.link_oidc_account": "Σύνδεση του λογαριασμού μου %s",
|
||||
"page.settings.unlink_oidc_account": "Αποσύνδεση του λογαριασμού μου %s",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
@@ -214,13 +220,14 @@
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.register": "Εγγραφή κωδικού πρόσβασης",
|
||||
"page.settings.webauthn.register.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.settings.webauthn.delete": [
|
||||
"Αφαιρέστε %d κωδικό πρόσβασης",
|
||||
"Καταργήστε %d κωδικούς πρόσβασης"
|
||||
],
|
||||
"page.login.title": "Είσοδος",
|
||||
"page.login.google_signin": "Συνδεθείτε με τo Google",
|
||||
"page.login.oidc_signin": "Συνδεθείτε με το OpenID Connect",
|
||||
"page.login.oidc_signin": "Συνδεθείτε με το %s",
|
||||
"page.login.webauthn_login": "Είσοδος με κωδικό πρόσβασης",
|
||||
"page.login.webauthn_login.error": "Δεν είναι δυνατή η σύνδεση με κωδικό πρόσβασης",
|
||||
"page.integrations.title": "Ενσωμάτωση",
|
||||
@@ -255,6 +262,7 @@
|
||||
"alert.no_bookmark": "Δεν υπάρχει σελιδοδείκτης αυτή τη στιγμή.",
|
||||
"alert.no_category": "Δεν υπάρχει κατηγορία.",
|
||||
"alert.no_category_entry": "Δεν υπάρχουν άρθρα σε αυτήν την κατηγορία.",
|
||||
"alert.no_tag_entry": "Δεν υπάρχουν αντικείμενα που να ταιριάζουν με αυτή την ετικέτα.",
|
||||
"alert.no_feed_entry": "Δεν υπάρχουν άρθρα για αυτήν τη ροή.",
|
||||
"alert.no_feed": "Δεν έχετε συνδρομές.",
|
||||
"alert.no_feed_in_category": "Δεν υπάρχει συνδρομή για αυτήν την κατηγορία.",
|
||||
@@ -296,6 +304,14 @@
|
||||
"error.password_min_length": "Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον 6 χαρακτήρες.",
|
||||
"error.settings_mandatory_fields": "Τα πεδία όνομα χρήστη, θέμα, Γλώσσα και ζώνη ώρας είναι υποχρεωτικά.",
|
||||
"error.settings_reading_speed_is_positive": "Οι ταχύτητες ανάγνωσης πρέπει να είναι θετικοί ακέραιοι αριθμοί.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.entries_per_page_invalid": "Ο αριθμός των καταχωρήσεων ανά σελίδα δεν είναι έγκυρος.",
|
||||
"error.feed_mandatory_fields": "Η διεύθυνση URL και η κατηγορία είναι υποχρεωτικά.",
|
||||
"error.feed_already_exists": "Αυτή η ροή υπάρχει ήδη.",
|
||||
@@ -315,6 +331,7 @@
|
||||
"form.feed.label.title": "Τίτλος",
|
||||
"form.feed.label.site_url": "Διεύθυνση URL ιστότοπου",
|
||||
"form.feed.label.feed_url": "Διεύθυνση URL ροής",
|
||||
"form.feed.label.description": "Περιγραφή",
|
||||
"form.feed.label.category": "Κατηγορία",
|
||||
"form.feed.label.crawler": "Λήψη αρχικού περιεχομένου",
|
||||
"form.feed.label.feed_username": "Όνομα Χρήστη ροής",
|
||||
@@ -327,6 +344,7 @@
|
||||
"form.feed.label.keeplist_rules": "Κρατήστε Κανόνες",
|
||||
"form.feed.label.ignore_http_cache": "Αγνοήστε την προσωρινή μνήμη HTTP",
|
||||
"form.feed.label.allow_self_signed_certificates": "Να επιτρέπονται αυτο-υπογεγραμμένα ή μη έγκυρα πιστοποιητικά",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.fetch_via_proxy": "Λήψη μέσω διακομιστή μεσολάβησης",
|
||||
"form.feed.label.disabled": "Μη ανανέωση αυτής της ροής",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
@@ -335,6 +353,15 @@
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
"form.feed.fieldset.integration": "Third-Party Services",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.category.label.title": "Τίτλος",
|
||||
"form.category.hide_globally": "Απόκρυψη καταχωρήσεων σε γενική λίστα μη αναγνωσμένων",
|
||||
"form.user.label.username": "Χρήστης",
|
||||
@@ -367,15 +394,26 @@
|
||||
"form.prefs.label.gesture_nav": "Χειρονομία για πλοήγηση μεταξύ των καταχωρήσεων",
|
||||
"form.prefs.label.show_reading_time": "Εμφάνιση εκτιμώμενου χρόνου ανάγνωσης για άρθρα",
|
||||
"form.prefs.label.custom_css": "Προσαρμοσμένο CSS",
|
||||
"form.prefs.label.custom_js": "Προσαρμοσμένο JavaScript",
|
||||
"form.prefs.label.entry_order": "Στήλη ταξινόμησης εισόδου",
|
||||
"form.prefs.label.default_home_page": "Προεπιλεγμένη αρχική σελίδα",
|
||||
"form.prefs.label.categories_sorting_order": "Ταξινόμηση κατηγοριών",
|
||||
"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_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"form.import.label.file": "Αρχείο OPML",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.fever_activate": "Ενεργοποιήστε το Fever API",
|
||||
"form.integration.fever_username": "Όνομα Χρήστη Fever",
|
||||
"form.integration.fever_password": "Κωδικός Πρόσβασης Fever",
|
||||
@@ -447,6 +485,15 @@
|
||||
"form.integration.matrix_bot_password": "Κωδικός πρόσβασης για τον χρήστη Matrix",
|
||||
"form.integration.matrix_bot_url": "URL διακομιστή Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Αναγνωριστικό της αίθουσας Matrix",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.readeck_activate": "Αποθήκευση άρθρων στο Readeck",
|
||||
"form.integration.readeck_endpoint": "Τελικό σημείο Readeck API",
|
||||
"form.integration.readeck_api_key": "Κλειδί API Readeck",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "Αποστολή μόνο URL (αντί για πλήρες περιεχόμενο)",
|
||||
"form.integration.shiori_activate": "Αποθήκευση άρθρων στο Shiori",
|
||||
"form.integration.shiori_endpoint": "Τελικό σημείο Shiori",
|
||||
"form.integration.shiori_username": "Όνομα Χρήστη Shiori",
|
||||
@@ -459,6 +506,13 @@
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_topic": "Ntfy topic",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.api_key.label.description": "Ετικέτα κλειδιού API",
|
||||
"form.submit.loading": "Φόρτωση...",
|
||||
"form.submit.saving": "Αποθήκευση...",
|
||||
@@ -498,7 +552,7 @@
|
||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"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.http_client_error": "HTTP client error: %v.",
|
||||
@@ -517,5 +571,16 @@
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Ταχύτητα αναπαραγωγής του ήχου/βίντεο",
|
||||
"error.settings_media_playback_rate_range": "Η ταχύτητα αναπαραγωγής είναι εκτός εύρους",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
||||
@@ -35,10 +35,12 @@
|
||||
"menu.about": "About",
|
||||
"menu.export": "Export",
|
||||
"menu.import": "Import",
|
||||
"menu.search": "Search",
|
||||
"menu.create_category": "Create a category",
|
||||
"menu.mark_page_as_read": "Mark this page as read",
|
||||
"menu.mark_all_as_read": "Mark all as read",
|
||||
"menu.show_all_entries": "Show all entries",
|
||||
"menu.show_only_starred_entries": "Show only starred entries",
|
||||
"menu.show_only_unread_entries": "Show only unread entries",
|
||||
"menu.refresh_feed": "Refresh",
|
||||
"menu.refresh_all_feeds": "Refresh all feeds in the background",
|
||||
@@ -54,7 +56,9 @@
|
||||
"search.label": "Search",
|
||||
"search.placeholder": "Search…",
|
||||
"search.submit": "Search",
|
||||
"pagination.last": "Last",
|
||||
"pagination.next": "Next",
|
||||
"pagination.first": "First",
|
||||
"pagination.previous": "Previous",
|
||||
"entry.status.unread": "Unread",
|
||||
"entry.status.read": "Read",
|
||||
@@ -175,6 +179,8 @@
|
||||
"page.keyboard_shortcuts.go_to_previous_item": "Go to previous item",
|
||||
"page.keyboard_shortcuts.go_to_next_item": "Go to next item",
|
||||
"page.keyboard_shortcuts.go_to_feed": "Go to feed",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "Go to top item",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "Go to bottom item",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "Go to previous page",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "Go to next page",
|
||||
"page.keyboard_shortcuts.open_item": "Open selected item",
|
||||
@@ -205,8 +211,8 @@
|
||||
"page.settings.title": "Settings",
|
||||
"page.settings.link_google_account": "Link my Google account",
|
||||
"page.settings.unlink_google_account": "Unlink my Google account",
|
||||
"page.settings.link_oidc_account": "Link my OpenID Connect account",
|
||||
"page.settings.unlink_oidc_account": "Unlink my OpenID Connect account",
|
||||
"page.settings.link_oidc_account": "Link my %s account",
|
||||
"page.settings.unlink_oidc_account": "Unlink my %s account",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
@@ -214,15 +220,16 @@
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.register": "Register passkey",
|
||||
"page.settings.webauthn.register.error": "Unable to register passkey",
|
||||
"page.settings.webauthn.delete" : [
|
||||
"page.settings.webauthn.delete": [
|
||||
"Remove %d passkey",
|
||||
"Remove %d passkeys"
|
||||
],
|
||||
"page.login.title": "Sign In",
|
||||
"page.login.google_signin": "Sign in with Google",
|
||||
"page.login.oidc_signin": "Sign in with OpenID Connect",
|
||||
"page.login.oidc_signin": "Sign in with %s",
|
||||
"page.login.webauthn_login": "Login with passkey",
|
||||
"page.login.webauthn_login.error": "Unable to login with 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.integrations.title": "Integrations",
|
||||
"page.integration.miniflux_api": "Miniflux API",
|
||||
"page.integration.miniflux_api_endpoint": "API Endpoint",
|
||||
@@ -255,6 +262,7 @@
|
||||
"alert.no_bookmark": "There are no starred entries.",
|
||||
"alert.no_category": "There is no category.",
|
||||
"alert.no_category_entry": "There are no entries in this category.",
|
||||
"alert.no_tag_entry": "There are no entries matching this tag.",
|
||||
"alert.no_feed_entry": "There are no entries for this feed.",
|
||||
"alert.no_feed": "You don’t have any feeds.",
|
||||
"alert.no_feed_in_category": "There is no feed for this category.",
|
||||
@@ -296,6 +304,14 @@
|
||||
"error.password_min_length": "The password must have at least 6 characters.",
|
||||
"error.settings_mandatory_fields": "The username, theme, language and timezone fields are mandatory.",
|
||||
"error.settings_reading_speed_is_positive": "The reading speeds must be positive integers.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.entries_per_page_invalid": "The number of entries per page is not valid.",
|
||||
"error.feed_mandatory_fields": "The URL and the category are mandatory.",
|
||||
"error.feed_already_exists": "This feed already exists.",
|
||||
@@ -313,6 +329,7 @@
|
||||
"form.feed.label.title": "Title",
|
||||
"form.feed.label.site_url": "Site URL",
|
||||
"form.feed.label.feed_url": "Feed URL",
|
||||
"form.feed.label.description": "Description",
|
||||
"form.feed.label.category": "Category",
|
||||
"form.feed.label.crawler": "Fetch original content",
|
||||
"form.feed.label.feed_username": "Feed Username",
|
||||
@@ -327,10 +344,18 @@
|
||||
"form.feed.label.urlrewrite_rules": "URL Rewrite Rules",
|
||||
"form.feed.label.ignore_http_cache": "Ignore HTTP cache",
|
||||
"form.feed.label.allow_self_signed_certificates": "Allow self-signed or invalid certificates",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.fetch_via_proxy": "Fetch via proxy",
|
||||
"form.feed.label.disabled": "Do not refresh this feed",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.hide_globally": "Hide entries in global unread list",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
@@ -367,15 +392,26 @@
|
||||
"form.prefs.label.gesture_nav": "Gesture to navigate between entries",
|
||||
"form.prefs.label.show_reading_time": "Show estimated reading time for entries",
|
||||
"form.prefs.label.custom_css": "Custom CSS",
|
||||
"form.prefs.label.custom_js": "Custom JavaScript",
|
||||
"form.prefs.label.entry_order": "Entry sorting column",
|
||||
"form.prefs.label.default_home_page": "Default home page",
|
||||
"form.prefs.label.categories_sorting_order": "Categories sorting",
|
||||
"form.prefs.label.mark_read_on_view": "Automatically mark entries as read when viewed",
|
||||
"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_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"form.import.label.file": "OPML file",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.fever_activate": "Activate Fever API",
|
||||
"form.integration.fever_username": "Fever Username",
|
||||
"form.integration.fever_password": "Fever Password",
|
||||
@@ -447,6 +483,15 @@
|
||||
"form.integration.matrix_bot_password": "Password for Matrix user",
|
||||
"form.integration.matrix_bot_url": "Matrix server URL",
|
||||
"form.integration.matrix_bot_chat_id": "ID of Matrix Room",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.readeck_activate": "Save entries to readeck",
|
||||
"form.integration.readeck_endpoint": "Readeck API Endpoint",
|
||||
"form.integration.readeck_api_key": "Readeck API key",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "Send only URL (instead of full content)",
|
||||
"form.integration.shiori_activate": "Save articles to Shiori",
|
||||
"form.integration.shiori_endpoint": "Shiori API Endpoint",
|
||||
"form.integration.shiori_username": "Shiori Username",
|
||||
@@ -459,6 +504,15 @@
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_topic": "Ntfy topic",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.api_key.label.description": "API Key Label",
|
||||
"form.submit.loading": "Loading…",
|
||||
"form.submit.saving": "Saving…",
|
||||
@@ -498,7 +552,7 @@
|
||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"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.http_client_error": "HTTP client error: %v.",
|
||||
@@ -517,5 +571,16 @@
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Playback speed of the audio/video",
|
||||
"error.settings_media_playback_rate_range": "Playback speed is out of range",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"skip_to_content": "Skip to content",
|
||||
"skip_to_content": "Saltar al contenido",
|
||||
"confirm.question": "¿Estás seguro?",
|
||||
"confirm.question.refresh": "¿Quieres forzar la actualización?",
|
||||
"confirm.yes": "sí",
|
||||
@@ -9,8 +9,8 @@
|
||||
"action.save": "Guardar",
|
||||
"action.or": "o",
|
||||
"action.cancel": "Cancelar",
|
||||
"action.remove": "Quitar",
|
||||
"action.remove_feed": "Quitar esta fuente",
|
||||
"action.remove": "Eliminar",
|
||||
"action.remove_feed": "Eliminar esta fuente",
|
||||
"action.update": "Actualizar",
|
||||
"action.edit": "Editar",
|
||||
"action.download": "Descargar",
|
||||
@@ -19,8 +19,8 @@
|
||||
"action.home_screen": "Añadir a la pantalla principal",
|
||||
"tooltip.keyboard_shortcuts": "Atajo de teclado: %s",
|
||||
"tooltip.logged_user": "Registrado como %s",
|
||||
"menu.title": "Menu",
|
||||
"menu.home_page": "Home page",
|
||||
"menu.title": "Menú",
|
||||
"menu.home_page": "Página de inicio",
|
||||
"menu.unread": "No leídos",
|
||||
"menu.starred": "Marcadores",
|
||||
"menu.history": "Historial",
|
||||
@@ -35,13 +35,15 @@
|
||||
"menu.about": "Acerca de",
|
||||
"menu.export": "Exportar",
|
||||
"menu.import": "Importar",
|
||||
"menu.search": "Buscar",
|
||||
"menu.create_category": "Crear una categoría",
|
||||
"menu.mark_page_as_read": "Marcar esta página como leída",
|
||||
"menu.mark_all_as_read": "Marcar todos como leídos",
|
||||
"menu.show_all_entries": "Mostrar todos los artículos",
|
||||
"menu.show_only_unread_entries": "Mostrar solo los artículos no leídos",
|
||||
"menu.show_only_starred_entries": "Mostrar solo los artículos marcados con una estrella",
|
||||
"menu.refresh_feed": "Refrescar",
|
||||
"menu.refresh_all_feeds": "Refrescar todas las fuentes en el fondo",
|
||||
"menu.refresh_all_feeds": "Refrescar todas las fuentes en segundo plano",
|
||||
"menu.edit_feed": "Editar",
|
||||
"menu.edit_category": "Editar",
|
||||
"menu.add_feed": "Agregar fuente",
|
||||
@@ -53,8 +55,10 @@
|
||||
"menu.shared_entries": "Artículos compartidos",
|
||||
"search.label": "Buscar",
|
||||
"search.placeholder": "Búsqueda...",
|
||||
"search.submit": "Search",
|
||||
"search.submit": "Buscar",
|
||||
"pagination.last": "Último",
|
||||
"pagination.next": "Siguiente",
|
||||
"pagination.first": "Primero",
|
||||
"pagination.previous": "Anterior",
|
||||
"entry.status.unread": "No leído",
|
||||
"entry.status.read": "Leído",
|
||||
@@ -89,22 +93,22 @@
|
||||
"entry.tags.label": "Etiquetas:",
|
||||
"page.shared_entries.title": "Artículos compartidos",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries"
|
||||
"%d artículo compartido",
|
||||
"%d artículos compartidos"
|
||||
],
|
||||
"page.unread.title": "No leídos",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries"
|
||||
"%d artículo no leído",
|
||||
"%d artículos no leídos"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total"
|
||||
"%d artículo en total",
|
||||
"%d artículos en total"
|
||||
],
|
||||
"page.starred.title": "Marcadores",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries"
|
||||
"%d artículo marcado",
|
||||
"%d artículos marcados"
|
||||
],
|
||||
"page.categories.title": "Categorías",
|
||||
"page.categories.no_feed": "Sin fuente.",
|
||||
@@ -115,17 +119,17 @@
|
||||
"Hay %d fuentes."
|
||||
],
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories"
|
||||
"%d categoría",
|
||||
"%d categorías"
|
||||
],
|
||||
"page.new_category.title": "Nueva categoría",
|
||||
"page.new_user.title": "Nuevo usuario",
|
||||
"page.edit_category.title": "Editar categoría: %s",
|
||||
"page.edit_user.title": "Editar usuario: %s",
|
||||
"page.feeds.title": "Fuentes",
|
||||
"page.category_label": "Category: %s",
|
||||
"page.category_label": "Categoría: %s",
|
||||
"page.feeds.last_check": "Última verificación:",
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.next_check": "Próxima verificación:",
|
||||
"page.feeds.read_counter": "Número de artículos leídos",
|
||||
"page.feeds.error_count": [
|
||||
"%d error",
|
||||
@@ -133,15 +137,15 @@
|
||||
],
|
||||
"page.history.title": "Historial",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries"
|
||||
"%d artículo leído",
|
||||
"%d artículos leídos"
|
||||
],
|
||||
"page.import.title": "Importar",
|
||||
"page.search.title": "Resultados de la búsqueda",
|
||||
"page.about.title": "Acerca de",
|
||||
"page.about.credits": "Créditos",
|
||||
"page.about.version": "Versión:",
|
||||
"page.about.build_date": "Fecha de construcción:",
|
||||
"page.about.build_date": "Fecha de compilación:",
|
||||
"page.about.author": "Autor:",
|
||||
"page.about.license": "Licencia:",
|
||||
"page.about.global_config_options": "Opciones de configuración global",
|
||||
@@ -177,6 +181,8 @@
|
||||
"page.keyboard_shortcuts.go_to_feed": "Ir a la fuente",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "Ir al página anterior",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "Ir al página siguiente",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "Ir al elemento inferior",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "Ir al elemento superior",
|
||||
"page.keyboard_shortcuts.open_item": "Abrir el elemento seleccionado",
|
||||
"page.keyboard_shortcuts.open_original": "Abrir el enlace original",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "Abrir enlace original en la pestaña actual",
|
||||
@@ -184,7 +190,7 @@
|
||||
"page.keyboard_shortcuts.open_comments_same_window": "Abrir enlace de comentarios en la pestaña actual",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "Marcar como leído o no leído, enfoque siguiente",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Marcar como leído o no leído, foco anterior",
|
||||
"page.keyboard_shortcuts.refresh_all_feeds": "Refrescar todas las fuentes en el fondo",
|
||||
"page.keyboard_shortcuts.refresh_all_feeds": "Refrescar todas las fuentes en segundo plano",
|
||||
"page.keyboard_shortcuts.mark_page_as_read": "Marcar página actual como leída",
|
||||
"page.keyboard_shortcuts.download_content": "Descargar el contento original",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Agregar o quitar marcador",
|
||||
@@ -205,24 +211,25 @@
|
||||
"page.settings.title": "Ajustes",
|
||||
"page.settings.link_google_account": "Vincular mi cuenta de Google",
|
||||
"page.settings.unlink_google_account": "Desvincular mi cuenta de Google",
|
||||
"page.settings.link_oidc_account": "Vincular mi cuenta de OpenID Connect",
|
||||
"page.settings.unlink_oidc_account": "Desvincular mi cuenta de OpenID Connect",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
"page.settings.webauthn.added_on": "Added On",
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.link_oidc_account": "Vincular mi cuenta de %s",
|
||||
"page.settings.unlink_oidc_account": "Desvincular mi cuenta de %s",
|
||||
"page.settings.webauthn.passkeys": "Claves de acceso",
|
||||
"page.settings.webauthn.actions": "Accioness",
|
||||
"page.settings.webauthn.passkey_name": "Nombre de clave de acceso",
|
||||
"page.settings.webauthn.added_on": "Añadido",
|
||||
"page.settings.webauthn.last_seen_on": "Usado por última vez",
|
||||
"page.settings.webauthn.register": "Registrar clave de acceso",
|
||||
"page.settings.webauthn.register.error": "No se puede registrar la clave de paso",
|
||||
"page.settings.webauthn.register.error": "No se puede registrar la clave de acceso",
|
||||
"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.settings.webauthn.delete": [
|
||||
"Eliminar %d clave de paso",
|
||||
"Eliminar %d claves de paso"
|
||||
"Eliminar %d clave de acceso",
|
||||
"Eliminar %d claves de acceso"
|
||||
],
|
||||
"page.login.title": "Iniciar sesión",
|
||||
"page.login.google_signin": "Iniciar sesión con tu cuenta de Google",
|
||||
"page.login.oidc_signin": "Iniciar sesión con tu cuenta de OpenID Connect",
|
||||
"page.login.oidc_signin": "Iniciar sesión con tu cuenta de %s",
|
||||
"page.login.webauthn_login": "Iniciar sesión con clave de acceso",
|
||||
"page.login.webauthn_login.error": "No se puede iniciar sesión con la clave de paso",
|
||||
"page.login.webauthn_login.error": "No se puede iniciar sesión con la clave de acceso",
|
||||
"page.integrations.title": "Integraciones",
|
||||
"page.integration.miniflux_api": "API de Miniflux",
|
||||
"page.integration.miniflux_api_endpoint": "Extremo de API",
|
||||
@@ -250,11 +257,12 @@
|
||||
"page.offline.title": "Modo offline",
|
||||
"page.offline.message": "Estas desconectado",
|
||||
"page.offline.refresh_page": "Intenta actualizar la página",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"page.webauthn_rename.title": "Renombrar clave de acceso",
|
||||
"alert.no_shared_entry": "No hay artículos compartidos.",
|
||||
"alert.no_bookmark": "No hay marcador en este momento.",
|
||||
"alert.no_category": "No hay categoría.",
|
||||
"alert.no_category_entry": "No hay artículos en esta categoría.",
|
||||
"alert.no_tag_entry": "No hay artículos con esta etiqueta.",
|
||||
"alert.no_feed_entry": "No hay artículos para esta fuente.",
|
||||
"alert.no_feed": "No tienes fuentes.",
|
||||
"alert.no_feed_in_category": "No hay fuentes para esta categoría.",
|
||||
@@ -289,6 +297,14 @@
|
||||
"error.password_min_length": "La contraseña debería tener al menos 6 caracteres.",
|
||||
"error.settings_mandatory_fields": "Los campos de nombre de usuario, tema, idioma y zona horaria son obligatorios.",
|
||||
"error.settings_reading_speed_is_positive": "Las velocidades de lectura deben ser números enteros positivos.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Regla de bloqueo no válida: a la regla #%d le falta un nombre de campo válido (Opciones: %s)",
|
||||
"error.settings_block_rule_separator_required": "Regla de bloqueo no válida: el patrón de la regla #%d debe estar separado por un '='",
|
||||
"error.settings_block_rule_regex_required": "Regla de bloqueo no válida: no se ha proporcionado el patrón de la regla #%d",
|
||||
"error.settings_block_rule_invalid_regex": "Regla de bloqueo no válida: el patrón de la regla #%d no es una expresión regular válida",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Regla de mantenimiento no válida: a la regla #%d le falta un nombre de campo válido (Opciones: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Regla de mantenimiento no válida: el patrón de la regla #%d debe estar separado por un '='",
|
||||
"error.settings_keep_rule_regex_required": "Regla de conservación no válida: no se ha proporcionado la regla #%d patrón",
|
||||
"error.settings_keep_rule_invalid_regex": "Regla de mantenimiento no válida: el patrón de la regla #%d no es una expresión regular válida",
|
||||
"error.entries_per_page_invalid": "El número de artículos por página no es válido.",
|
||||
"error.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.",
|
||||
"error.feed_already_exists": "Este feed ya existe.",
|
||||
@@ -313,6 +329,7 @@
|
||||
"form.feed.label.title": "Título",
|
||||
"form.feed.label.site_url": "URL del sitio",
|
||||
"form.feed.label.feed_url": "URL de la fuente",
|
||||
"form.feed.label.description": "Descripción",
|
||||
"form.feed.label.category": "Categoría",
|
||||
"form.feed.label.crawler": "Obtener rastreador original",
|
||||
"form.feed.label.feed_username": "Nombre de usuario de la fuente",
|
||||
@@ -321,20 +338,30 @@
|
||||
"form.feed.label.cookie": "Configurar las cookies",
|
||||
"form.feed.label.scraper_rules": "Reglas de extracción de información",
|
||||
"form.feed.label.rewrite_rules": "Reglas de reescribir",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.apprise_service_urls": "Lista separada por comas de las URL del servicio Apprise",
|
||||
"form.feed.label.blocklist_rules": "Reglas de Filtrado (Bloquear)",
|
||||
"form.feed.label.keeplist_rules": "Reglas de Filtrado (Permitir)",
|
||||
"form.feed.label.urlrewrite_rules": "Reglas de Filtrado (Reescritura)",
|
||||
"form.feed.label.ignore_http_cache": "Ignorar caché HTTP",
|
||||
"form.feed.label.allow_self_signed_certificates": "Permitir certificados autofirmados o no válidos",
|
||||
"form.feed.label.disable_http2": "Deshabilite HTTP/2 para evitar huellas digitales",
|
||||
"form.feed.label.fetch_via_proxy": "Buscar a través de proxy",
|
||||
"form.feed.label.disabled": "No actualice este feed",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.no_media_player": "Sin reproductor multimedia (audio/video)",
|
||||
"form.feed.label.hide_globally": "Ocultar artículos en la lista global de no leídos",
|
||||
"form.feed.label.ntfy_activate": "Enviar entradas a ntfy",
|
||||
"form.feed.label.ntfy_priority": "Prioridad Ntfy",
|
||||
"form.feed.label.ntfy_max_priority": "Prioridad máxima a Ntfy",
|
||||
"form.feed.label.ntfy_high_priority": "Prioridad alta a Ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Prioridad predeterminada a Ntfy",
|
||||
"form.feed.label.ntfy_low_priority": "Prioridad baja a Ntfy",
|
||||
"form.feed.label.ntfy_min_priority": "Prioridad mínima a Ntfy",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
"form.feed.fieldset.integration": "Third-Party Services",
|
||||
"form.feed.fieldset.rules": "Reglas",
|
||||
"form.feed.fieldset.network_settings": "Ajustes de red",
|
||||
"form.feed.fieldset.integration": "Servicios de terceros",
|
||||
"form.category.label.title": "Título",
|
||||
"form.category.hide_globally": "Ocultar artículos en la lista global de no leídos",
|
||||
"form.user.label.username": "Nombre de usuario",
|
||||
@@ -367,15 +394,26 @@
|
||||
"form.prefs.label.gesture_nav": "Gesto para navegar entre entradas",
|
||||
"form.prefs.label.show_reading_time": "Mostrar el tiempo estimado de lectura de los artículos",
|
||||
"form.prefs.label.custom_css": "CSS personalizado",
|
||||
"form.prefs.label.custom_js": "JavaScript personalizado",
|
||||
"form.prefs.label.entry_order": "Columna de clasificación de artículos",
|
||||
"form.prefs.label.default_home_page": "Página de inicio por defecto",
|
||||
"form.prefs.label.categories_sorting_order": "Clasificación por categorías",
|
||||
"form.prefs.label.mark_read_on_view": "Marcar automáticamente las entradas como leídas cuando se vean",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Marcar las entradas como leídas cuando se vean. Para audio/video, marcar como leído al 90%% de finalización",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Marcar como leído solo cuando la reproducción de audio/video alcance el 90%% de finalización",
|
||||
"form.prefs.label.mark_read_manually": "Marcar entradas como leídas manualmente",
|
||||
"form.prefs.fieldset.application_settings": "Ajustes de la aplicación",
|
||||
"form.prefs.fieldset.authentication_settings": "Ajustes de la autentificación",
|
||||
"form.prefs.fieldset.reader_settings": "Ajustes del lector",
|
||||
"form.prefs.fieldset.global_feed_settings": "Ajustes globales del feed",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"form.import.label.file": "Archivo OPML",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.betula_activate": "Guardar artículos en Betula",
|
||||
"form.integration.betula_url": "URL del servidor Betula",
|
||||
"form.integration.betula_token": "Token de Betula",
|
||||
"form.integration.fever_activate": "Activar API de Fever",
|
||||
"form.integration.fever_username": "Nombre de usuario de Fever",
|
||||
"form.integration.fever_password": "Contraseña de Fever",
|
||||
@@ -402,12 +440,12 @@
|
||||
"form.integration.wallabag_client_secret": "Secreto de cliente de Wallabag",
|
||||
"form.integration.wallabag_username": "Nombre de usuario de Wallabag",
|
||||
"form.integration.wallabag_password": "Contraseña de Wallabag",
|
||||
"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.apprise_activate": "Push entries to Apprise",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
|
||||
"form.integration.notion_activate": "Guardar entradas en Notion",
|
||||
"form.integration.notion_page_id": "ID de página de Notion",
|
||||
"form.integration.notion_token": "Token secreto de Notion",
|
||||
"form.integration.apprise_activate": "Enviar artículos a Apprise",
|
||||
"form.integration.apprise_url": "URL de la API de Apprise",
|
||||
"form.integration.apprise_services_url": "Lista separada por comas de las URL del servicio Apprise",
|
||||
"form.integration.nunux_keeper_activate": "Enviar artículos a Nunux Keeper",
|
||||
"form.integration.nunux_keeper_endpoint": "Acceso API de Nunux Keeper",
|
||||
"form.integration.nunux_keeper_api_key": "Clave de API de Nunux Keeper",
|
||||
@@ -418,26 +456,26 @@
|
||||
"form.integration.espial_endpoint": "Acceso API de Espial",
|
||||
"form.integration.espial_api_key": "Clave de API de Espial",
|
||||
"form.integration.espial_tags": "Etiquetas de Espial",
|
||||
"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.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.telegram_bot_activate": "Envíe nuevos artículos al chat de Telegram",
|
||||
"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_bot_disable_web_page_preview": "Disable web page preview",
|
||||
"form.integration.telegram_bot_disable_notification": "Disable notification",
|
||||
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Deshabilitar la vista previa de la página web",
|
||||
"form.integration.telegram_bot_disable_notification": "Deshabilitar notificación",
|
||||
"form.integration.telegram_bot_disable_buttons": "Deshabilitar botones",
|
||||
"form.integration.linkace_activate": "Guardar artículos en LinkAce",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_tags": "LinkAce Tags",
|
||||
"form.integration.linkace_is_private": "Mark link as private",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
"form.integration.linkace_api_key": "Clave API de LinkAce",
|
||||
"form.integration.linkace_tags": "Etiquetas de LinkAce",
|
||||
"form.integration.linkace_is_private": "Marcar enlace como privado",
|
||||
"form.integration.linkace_check_disabled": "Deshabilitar la comprobación de enlace",
|
||||
"form.integration.linkding_activate": "Enviar artículos a Linkding",
|
||||
"form.integration.linkding_endpoint": "Acceso API de Linkding",
|
||||
"form.integration.linkding_api_key": "Clave de API de Linkding",
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkding_tags": "Etiquetas de Linkding",
|
||||
"form.integration.linkding_bookmark": "Marcar marcador como no leído",
|
||||
"form.integration.linkwarden_activate": "Enviar artículos a Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Acceso API de Linkwarden",
|
||||
@@ -447,18 +485,34 @@
|
||||
"form.integration.matrix_bot_password": "Contraseña para el usuario de Matrix",
|
||||
"form.integration.matrix_bot_url": "URL del servidor de Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID de la sala de Matrix",
|
||||
"form.integration.raindrop_activate": "Guardar artículos en Raindrop",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.raindrop_collection_id": "Colección ID",
|
||||
"form.integration.raindrop_tags": "Etiquetas (separadas por comas)",
|
||||
"form.integration.readeck_activate": "Enviar artículos a Readeck",
|
||||
"form.integration.readeck_endpoint": "Acceso API de Readeck",
|
||||
"form.integration.readeck_api_key": "Clave de 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.shiori_activate": "Guardar artículos a Shiori",
|
||||
"form.integration.shiori_endpoint": "Extremo de API de Shiori",
|
||||
"form.integration.shiori_username": "Nombre de usuario de Shiori",
|
||||
"form.integration.shiori_password": "Contraseña de Shiori",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Guardar artículos en Shaarli",
|
||||
"form.integration.shaarli_endpoint": "URL de Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Secreto API de Shaarli",
|
||||
"form.integration.webhook_activate": "Habilitar Webhook",
|
||||
"form.integration.webhook_url": "URL de Webhook",
|
||||
"form.integration.webhook_secret": "Secreto de Webhook",
|
||||
"form.integration.rssbridge_activate": "Vericar RSS-Bridge al agregar suscripciones",
|
||||
"form.integration.rssbridge_url": "URL del servidro RSS-Bridge",
|
||||
"form.integration.ntfy_activate": "Enviar artículos a ntfy",
|
||||
"form.integration.ntfy_topic": "Tema Ntfy",
|
||||
"form.integration.ntfy_url": "URL de Ntfy (opcional, la predeterminada es ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Token de API de Ntfy (opcional)",
|
||||
"form.integration.ntfy_username": "Nombre de usuario de Ntfy (opcional)",
|
||||
"form.integration.ntfy_password": "Contraseña de Ntfy (opcional)",
|
||||
"form.integration.ntfy_icon_url": "URL del icono de Ntfy (opcional)",
|
||||
"form.api_key.label.description": "Etiqueta de clave API",
|
||||
"form.submit.loading": "Cargando...",
|
||||
"form.submit.saving": "Guardando...",
|
||||
@@ -490,32 +544,43 @@
|
||||
"hace %d años"
|
||||
],
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again.",
|
||||
"You have triggered too many feed refreshes. Please wait %d minutes before trying again."
|
||||
"Has activado demasiadas actualizaciones del feed. Espere %d minuto antes de volver a intentarlo.",
|
||||
"Has activado demasiadas actualizaciones del feed. Espere %d minutos antes de volver a intentarlo."
|
||||
],
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"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_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||
"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.http_client_error": "HTTP client error: %v.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||
"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_bad_gateway": "The website is not available at the moment due to a bad gateway error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"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_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_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.database_error": "Database error: %v.",
|
||||
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||
"error.duplicated_feed": "This feed already exists.",
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||
"alert.background_feed_refresh": "Todos los feeds se actualizan en segundo plano. Puede continuar usando Miniflux mientras se ejecuta este proceso.",
|
||||
"error.http_response_too_large": "La respuesta HTTP es demasiado grande. Puede aumentar el límite de tamaño de respuesta HTTP en la configuración global (requiere reiniciar el servidor).",
|
||||
"error.http_body_read": "Imposible leer el cuerpo HTTP: %v.",
|
||||
"error.http_empty_response_body": "El cuerpo de la respuesta HTTP está vacío.",
|
||||
"error.http_empty_response": "La respuesta HTTP está vacía. ¿Quizás este sitio web tiene un mecanismo de protección contra bots?",
|
||||
"error.tls_error": "Error de TLS: %q. Puede desactivar la verificación TLS en la configuración del feed si lo desea.",
|
||||
"error.network_operation": "Miniflux no puede acceder a este sitio web debido a un error de red: %v.",
|
||||
"error.network_timeout": "Este sitio web es demasiado lento y se agotó el tiempo de espera de la solicitud: %v",
|
||||
"error.http_client_error": "Error cliente HTTP: %v.",
|
||||
"error.http_not_authorized": "El acceso a este sitio web no está autorizado. Podría ser un nombre de usuario o contraseña incorrectos.",
|
||||
"error.http_too_many_requests": "Miniflux generó demasiadas solicitudes a este sitio web. Por favor, inténtalo de nuevo más tarde o cambia la configuración de la aplicación.",
|
||||
"error.http_forbidden": "El acceso a este sitio web está prohibido. ¿Quizás este sitio web tiene un mecanismo de protección contra bots?",
|
||||
"error.http_resource_not_found": "No se encuentra el recurso solicitado. Por favor, verifique la URL.",
|
||||
"error.http_internal_server_error": "El sitio web no está disponible en estos momentos debido a un error del servidor. El problema no está en el lado de Miniflux. Por favor, inténtalo de nuevo más tarde.",
|
||||
"error.http_bad_gateway": "El sitio web no está disponible en este momento debido a un error en la puerta de enlace. El problema no está en el lado de Miniflux. Por favor, inténtalo de nuevo más tarde.",
|
||||
"error.http_service_unavailable": "El sitio web no está disponible en estos momentos debido a un error interno del servidor. El problema no está en el lado de Miniflux. Por favor, inténtalo de nuevo más tarde.",
|
||||
"error.http_gateway_timeout": "El sitio web no está disponible en este momento debido a un error de tiempo de espera de la puerta de enlace. El problema no está en el lado de Miniflux. Por favor, inténtalo de nuevo más tarde.",
|
||||
"error.http_unexpected_status_code": "El sitio web no está disponible en este momento debido a un código de estado HTTP inesperado: %d. El problema no está en el lado de Miniflux. Por favor, inténtalo de nuevo más tarde.",
|
||||
"error.database_error": "Error en la base de datos: %v.",
|
||||
"error.category_not_found": "Esta categoría no existe o no pertenece a este usuario.",
|
||||
"error.duplicated_feed": "Este feed ya existe.",
|
||||
"error.unable_to_parse_feed": "No se puede analizar este feed: %v.",
|
||||
"error.feed_not_found": "Este feed no existe o no pertenece a este usuario.",
|
||||
"error.unable_to_detect_rssbridge": "No se puede detectar la fuente usando RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "No se puede detectar el formato del feed: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Velocidad de reproducción del audio/vídeo",
|
||||
"error.settings_media_playback_rate_range": "La velocidad de reproducción está fuera de rango",
|
||||
"enclosure_media_controls.seek" : "Buscar:",
|
||||
"enclosure_media_controls.seek.title" : "Buscar %s segundos",
|
||||
"enclosure_media_controls.speed" : "Velocidad:",
|
||||
"enclosure_media_controls.speed.faster" : "Más rápido",
|
||||
"enclosure_media_controls.speed.faster.title" : "Más rápido a %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Despacio",
|
||||
"enclosure_media_controls.speed.slower.title" : "Más despacio a %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Restablecer",
|
||||
"enclosure_media_controls.speed.reset.title" : "Restablecer la velocidad a 1x"
|
||||
}
|
||||
|
||||
@@ -35,11 +35,13 @@
|
||||
"menu.about": "Tietoja",
|
||||
"menu.export": "Vie",
|
||||
"menu.import": "Tuo",
|
||||
"menu.search": "Haku",
|
||||
"menu.create_category": "Luo kategoria",
|
||||
"menu.mark_page_as_read": "Merkitse tämä sivu luetuksi",
|
||||
"menu.mark_all_as_read": "Merkitse kaikki luetuksi",
|
||||
"menu.show_all_entries": "Näytä kaikki artikkelit",
|
||||
"menu.show_only_unread_entries": "Näytä vain lukemattomat artikkelit",
|
||||
"menu.show_only_starred_entries": "Show only starred entries",
|
||||
"menu.refresh_feed": "Päivitä",
|
||||
"menu.refresh_all_feeds": "Päivitä kaikki syötteet taustalla",
|
||||
"menu.edit_feed": "Muokkaa",
|
||||
@@ -54,7 +56,9 @@
|
||||
"search.label": "Haku",
|
||||
"search.placeholder": "Hae...",
|
||||
"search.submit": "Search",
|
||||
"pagination.last": "Last",
|
||||
"pagination.next": "Seuraava",
|
||||
"pagination.first": "First",
|
||||
"pagination.previous": "Edellinen",
|
||||
"entry.status.unread": "Lukematon",
|
||||
"entry.status.read": "Luettu",
|
||||
@@ -177,6 +181,8 @@
|
||||
"page.keyboard_shortcuts.go_to_feed": "Siirry syötteeseen",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "Siirry edelliselle sivulle",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "Siirry seuraavalle sivulle",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "Siirry alimpaan kohtaan",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "Siirry alkuun",
|
||||
"page.keyboard_shortcuts.open_item": "Avaa valittu kohde",
|
||||
"page.keyboard_shortcuts.open_original": "Avaa alkuperäinen linkki",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "Avaa alkuperäinen linkki nykyisessä välilehdessä",
|
||||
@@ -205,8 +211,8 @@
|
||||
"page.settings.title": "Asetukset",
|
||||
"page.settings.link_google_account": "Linkitä Google-tilini",
|
||||
"page.settings.unlink_google_account": "Poista Google-tilini linkitys",
|
||||
"page.settings.link_oidc_account": "Linkitä OpenID Connect -tilini",
|
||||
"page.settings.unlink_oidc_account": "Poista OpenID Connect -tilini linkitys",
|
||||
"page.settings.link_oidc_account": "Linkitä %s -tilini",
|
||||
"page.settings.unlink_oidc_account": "Poista %s -tilini linkitys",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
@@ -214,13 +220,14 @@
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.register": "Rekisteröi salasana",
|
||||
"page.settings.webauthn.register.error": "Salasanaa ei voi rekisteröidä",
|
||||
"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.settings.webauthn.delete": [
|
||||
"Poista %d salasana",
|
||||
"Poista %d salasanaa"
|
||||
],
|
||||
"page.login.title": "Kirjaudu sisään",
|
||||
"page.login.google_signin": "Kirjaudu sisään Googlella",
|
||||
"page.login.oidc_signin": "Kirjaudu sisään OpenID Connectilla",
|
||||
"page.login.oidc_signin": "Kirjaudu sisään %silla",
|
||||
"page.login.webauthn_login": "Kirjaudu sisään salasanalla",
|
||||
"page.login.webauthn_login.error": "Ei voida kirjautua sisään salasanalla",
|
||||
"page.integrations.title": "Integraatiot",
|
||||
@@ -255,6 +262,7 @@
|
||||
"alert.no_bookmark": "Tällä hetkellä ei ole kirjanmerkkiä.",
|
||||
"alert.no_category": "Ei ole kategoriaa.",
|
||||
"alert.no_category_entry": "Tässä kategoriassa ei ole artikkeleita.",
|
||||
"alert.no_tag_entry": "Tätä tunnistetta vastaavia merkintöjä ei ole.",
|
||||
"alert.no_feed_entry": "Tässä syötteessä ei ole artikkeleita.",
|
||||
"alert.no_feed": "Sinulla ei ole tilauksia.",
|
||||
"alert.no_feed_in_category": "Tälle kategorialle ei ole tilausta.",
|
||||
@@ -296,6 +304,14 @@
|
||||
"error.password_min_length": "Salasanassa on oltava vähintään 6 merkkiä.",
|
||||
"error.settings_mandatory_fields": "Käyttäjätunnus, teema, kieli ja aikavyöhyke ovat pakollisia.",
|
||||
"error.settings_reading_speed_is_positive": "Lukunopeuksien on oltava positiivisia kokonaislukuja.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.entries_per_page_invalid": "Artikkelien määrä sivulla ei kelpaa.",
|
||||
"error.feed_mandatory_fields": "URL-osoite ja kategoria ovat pakollisia.",
|
||||
"error.feed_already_exists": "Tämä syöte on jo olemassa.",
|
||||
@@ -315,6 +331,7 @@
|
||||
"form.feed.label.title": "Otsikko",
|
||||
"form.feed.label.site_url": "Sivuston URL-osoite",
|
||||
"form.feed.label.feed_url": "Syötteen URL-osoite",
|
||||
"form.feed.label.description": "Kuvaus",
|
||||
"form.feed.label.category": "Kategoria",
|
||||
"form.feed.label.crawler": "Nouda alkuperäinen sisältö",
|
||||
"form.feed.label.feed_username": "Syötteen käyttäjätunnus",
|
||||
@@ -327,10 +344,20 @@
|
||||
"form.feed.label.keeplist_rules": "Keep-säännöt",
|
||||
"form.feed.label.ignore_http_cache": "Ohita HTTP-välimuisti",
|
||||
"form.feed.label.allow_self_signed_certificates": "Salli itseallekirjoitetut tai virheelliset varmenteet",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.fetch_via_proxy": "Nouda välityspalvelimen kautta",
|
||||
"form.feed.label.disabled": "Älä päivitä tätä syötettä",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.hide_globally": "Piilota artikkelit lukemattomien listassa",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
@@ -367,15 +394,26 @@
|
||||
"form.prefs.label.gesture_nav": "Ele siirtyäksesi merkintöjen välillä",
|
||||
"form.prefs.label.show_reading_time": "Näytä artikkeleiden arvioitu lukuaika",
|
||||
"form.prefs.label.custom_css": "Mukautettu CSS",
|
||||
"form.prefs.label.custom_js": "Mukautettu JavaScript",
|
||||
"form.prefs.label.entry_order": "Lajittele sarakkeen mukaan",
|
||||
"form.prefs.label.default_home_page": "Oletusarvoinen etusivu",
|
||||
"form.prefs.label.categories_sorting_order": "Kategorioiden lajittelu",
|
||||
"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_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"form.import.label.file": "OPML-tiedosto",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.fever_activate": "Ota Fever API käyttöön",
|
||||
"form.integration.fever_username": "Fever-käyttäjätunnus",
|
||||
"form.integration.fever_password": "Fever-salasana",
|
||||
@@ -447,6 +485,15 @@
|
||||
"form.integration.matrix_bot_password": "Matrix-käyttäjän salasana",
|
||||
"form.integration.matrix_bot_url": "Matrix-palvelimen URL-osoite",
|
||||
"form.integration.matrix_bot_chat_id": "Matrix-huoneen tunnus",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.readeck_activate": "Tallenna artikkelit Readeckiin",
|
||||
"form.integration.readeck_endpoint": "Readeck API-päätepiste",
|
||||
"form.integration.readeck_api_key": "Readeck API-avain",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "Lähetä vain URL-osoite (koko sisällön sijaan)",
|
||||
"form.integration.shiori_activate": "Save articles to Shiori",
|
||||
"form.integration.shiori_endpoint": "Shiori API Endpoint",
|
||||
"form.integration.shiori_username": "Shiori Username",
|
||||
@@ -459,6 +506,13 @@
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_topic": "Ntfy topic",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.api_key.label.description": "API Key Label",
|
||||
"form.submit.loading": "Ladataan...",
|
||||
"form.submit.saving": "Tallennetaan...",
|
||||
@@ -498,7 +552,7 @@
|
||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"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.http_client_error": "HTTP client error: %v.",
|
||||
@@ -517,5 +571,16 @@
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Äänen/videon toistonopeus",
|
||||
"error.settings_media_playback_rate_range": "Toistonopeus on alueen ulkopuolella",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
||||
@@ -35,11 +35,13 @@
|
||||
"menu.about": "À propos",
|
||||
"menu.export": "Export",
|
||||
"menu.import": "Import",
|
||||
"menu.search": "Recherche",
|
||||
"menu.create_category": "Créer une catégorie",
|
||||
"menu.mark_page_as_read": "Marquer cette page comme lu",
|
||||
"menu.mark_all_as_read": "Tout marquer comme lu",
|
||||
"menu.show_all_entries": "Afficher tous les articles",
|
||||
"menu.show_only_unread_entries": "Afficher uniquement les articles non lus",
|
||||
"menu.show_only_starred_entries": "Afficher uniquement les favoris",
|
||||
"menu.refresh_feed": "Actualiser",
|
||||
"menu.refresh_all_feeds": "Actualiser les abonnements en arrière-plan",
|
||||
"menu.edit_feed": "Modifier",
|
||||
@@ -54,7 +56,9 @@
|
||||
"search.label": "Recherche",
|
||||
"search.placeholder": "Recherche...",
|
||||
"search.submit": "Rechercher",
|
||||
"pagination.last": "Dernière page",
|
||||
"pagination.next": "Suivant",
|
||||
"pagination.first": "Première page",
|
||||
"pagination.previous": "Précédent",
|
||||
"entry.status.unread": "Non lu",
|
||||
"entry.status.read": "Lu",
|
||||
@@ -177,6 +181,8 @@
|
||||
"page.keyboard_shortcuts.go_to_feed": "Voir abonnement",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "Page précédente",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "Page suivante",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "Aller à l'élément du bas",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "Aller à l'élément supérieur",
|
||||
"page.keyboard_shortcuts.open_item": "Ouvrir élément sélectionné",
|
||||
"page.keyboard_shortcuts.open_original": "Ouvrir le lien original",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "Ouvrir le lien original dans l'onglet en cours",
|
||||
@@ -205,8 +211,8 @@
|
||||
"page.settings.title": "Réglages",
|
||||
"page.settings.link_google_account": "Associer mon compte Google",
|
||||
"page.settings.unlink_google_account": "Dissocier mon compte Google",
|
||||
"page.settings.link_oidc_account": "Associer mon compte OpenID Connect",
|
||||
"page.settings.unlink_oidc_account": "Dissocier mon compte OpenID Connect",
|
||||
"page.settings.link_oidc_account": "Associer mon compte %s",
|
||||
"page.settings.unlink_oidc_account": "Dissocier mon compte %s",
|
||||
"page.settings.webauthn.passkeys": "Clés d’accès",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.passkey_name": "Nom de la clé d’accès",
|
||||
@@ -214,13 +220,14 @@
|
||||
"page.settings.webauthn.last_seen_on": "Dernière utilisation",
|
||||
"page.settings.webauthn.register": "Enregister une nouvelle clé d’accès",
|
||||
"page.settings.webauthn.register.error": "Impossible d'enregistrer la clé d’accès",
|
||||
"page.settings.webauthn.delete" : [
|
||||
"page.login.webauthn_login.help": "Veuillez saisir votre nom d'utilisateur si vous utilisez une clé de sécurité. Cela n'est pas nécessaire si vous utilisez une clé d'accès (Passkey).",
|
||||
"page.settings.webauthn.delete": [
|
||||
"Supprimer %d clé d’accès",
|
||||
"Supprimer %d clés d’accès"
|
||||
],
|
||||
"page.login.title": "Connexion",
|
||||
"page.login.google_signin": "Se connecter avec Google",
|
||||
"page.login.oidc_signin": "Se connecter avec OpenID Connect",
|
||||
"page.login.oidc_signin": "Se connecter avec %s",
|
||||
"page.login.webauthn_login": "Se connecter avec une clé d’accès",
|
||||
"page.login.webauthn_login.error": "Impossible de se connecter avec la clé d’accès",
|
||||
"page.integrations.title": "Intégrations",
|
||||
@@ -255,6 +262,7 @@
|
||||
"alert.no_bookmark": "Il n'y a aucun favoris pour le moment.",
|
||||
"alert.no_category": "Il n'y a aucune catégorie.",
|
||||
"alert.no_category_entry": "Il n'y a aucun article dans cette catégorie.",
|
||||
"alert.no_tag_entry": "Il n'y a aucun article correspondant à ce tag.",
|
||||
"alert.no_feed_entry": "Il n'y a aucun article pour cet abonnement.",
|
||||
"alert.no_feed": "Vous n'avez aucun abonnement.",
|
||||
"alert.no_feed_in_category": "Il n'y a pas d'abonnement pour cette catégorie.",
|
||||
@@ -289,6 +297,14 @@
|
||||
"error.password_min_length": "Vous devez utiliser au moins 6 caractères pour le mot de passe.",
|
||||
"error.settings_mandatory_fields": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.",
|
||||
"error.settings_reading_speed_is_positive": "Les vitesses de lecture doivent être des entiers positifs.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Règle de blocage invalide : la règle n°%d ne contient pas un nom de champ valide (Options : %s)",
|
||||
"error.settings_block_rule_separator_required": "Règle de blocage invalide : le motif de la règle n°%d doit être séparé par un '='",
|
||||
"error.settings_block_rule_regex_required": "Règle de blocage invalide : le motif de la règle n°%d n'est pas fourni",
|
||||
"error.settings_block_rule_invalid_regex": "Règle de blocage invalide : le motif de la règle n°%d n'est pas une expression régulière valide",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Règle de conservation invalide : la règle n°%d ne contient pas un nom de champ valide (Options : %s)",
|
||||
"error.settings_keep_rule_separator_required": "Règle de conservation invalide : le motif de la règle n°%d doit être séparé par un '='",
|
||||
"error.settings_keep_rule_regex_required": "Règle de conservation invalide : le motif de la règle n°%d n'est pas fourni",
|
||||
"error.settings_keep_rule_invalid_regex": "Règle de conservation invalide : le motif de la règle n°%d n'est pas une expression régulière valide",
|
||||
"error.entries_per_page_invalid": "Le nombre d'entrées par page n'est pas valide.",
|
||||
"error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.",
|
||||
"error.feed_already_exists": "Ce flux existe déjà.",
|
||||
@@ -313,6 +329,7 @@
|
||||
"form.feed.label.title": "Titre",
|
||||
"form.feed.label.site_url": "URL du site web",
|
||||
"form.feed.label.feed_url": "URL du flux",
|
||||
"form.feed.label.description": "Description",
|
||||
"form.feed.label.category": "Catégorie",
|
||||
"form.feed.label.crawler": "Récupérer le contenu original",
|
||||
"form.feed.label.feed_username": "Nom d'utilisateur du flux",
|
||||
@@ -327,10 +344,20 @@
|
||||
"form.feed.label.urlrewrite_rules": "Règles de réécriture d'URL",
|
||||
"form.feed.label.ignore_http_cache": "Ignorer le cache HTTP",
|
||||
"form.feed.label.allow_self_signed_certificates": "Autoriser les certificats auto-signés ou non valides",
|
||||
"form.feed.label.disable_http2": "Désactiver HTTP/2",
|
||||
"form.feed.label.fetch_via_proxy": "Récupérer via proxy",
|
||||
"form.feed.label.disabled": "Ne pas actualiser ce flux",
|
||||
"form.feed.label.no_media_player": "Pas de lecteur multimedia (audio/vidéo)",
|
||||
"form.feed.label.hide_globally": "Masquer les entrées dans la liste globale non lue",
|
||||
"form.feed.label.ntfy_activate": "Activer les notifications",
|
||||
"form.feed.label.ntfy_priority": "Priorité de notification",
|
||||
"form.feed.label.ntfy_max_priority": "Priorité maximale de notification",
|
||||
"form.feed.label.ntfy_high_priority": "Priorité élevée de notification",
|
||||
"form.feed.label.ntfy_default_priority": "Priorité par défaut de notification",
|
||||
"form.feed.label.ntfy_low_priority": "Priorité basse de notification",
|
||||
"form.feed.label.ntfy_min_priority": "Priorité minimale de notification",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.feed.fieldset.general": "Général",
|
||||
"form.feed.fieldset.rules": "Règles",
|
||||
"form.feed.fieldset.network_settings": "Paramètres réseau",
|
||||
@@ -367,15 +394,26 @@
|
||||
"form.prefs.label.gesture_nav": "Geste pour naviguer entre les entrées",
|
||||
"form.prefs.label.show_reading_time": "Afficher le temps de lecture estimé des articles",
|
||||
"form.prefs.label.custom_css": "Feuille de style personnalisée",
|
||||
"form.prefs.label.custom_js": "Code JavaScript personnalisé",
|
||||
"form.prefs.label.entry_order": "Colonne de tri des entrées",
|
||||
"form.prefs.label.default_home_page": "Page d'accueil par défaut",
|
||||
"form.prefs.label.categories_sorting_order": "Colonne de tri des catégories",
|
||||
"form.prefs.label.mark_read_on_view": "Marquer automatiquement les entrées comme lues lorsqu'elles sont consultées",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Marquer automatiquement les entrées comme lues lorsqu'elles sont consultées. Pour l'audio/vidéo, marquer comme lues après 90%%",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Marqué les entrées comme lues uniquement après 90%% de lecture de l'audio/vidéo",
|
||||
"form.prefs.label.mark_read_manually": "Marqué les entrées comme lues manuellement",
|
||||
"form.prefs.fieldset.application_settings": "Paramètres de l'application",
|
||||
"form.prefs.fieldset.authentication_settings": "Paramètres d'authentification",
|
||||
"form.prefs.fieldset.reader_settings": "Paramètres du lecteur",
|
||||
"form.prefs.fieldset.global_feed_settings": "Paramètres globaux des abonnements",
|
||||
"form.prefs.label.external_font_hosts": "Polices externes autorisées",
|
||||
"form.prefs.help.external_font_hosts": "Liste de domaine externes autorisés, séparés par des espaces. Par exemple : « fonts.gstatic.com fonts.googleapis.com ».",
|
||||
"error.settings_invalid_domain_list": "Liste de domaines invalide. Veuillez fournir une liste de domaines séparés par des espaces.",
|
||||
"form.import.label.file": "Fichier OPML",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.betula_activate": "Sauvegarder les entrées vers Betula",
|
||||
"form.integration.betula_url": "URL du serveur Betula",
|
||||
"form.integration.betula_token": "Jeton de sécurité de l'API de Betula",
|
||||
"form.integration.fever_activate": "Activer l'API de Fever",
|
||||
"form.integration.fever_username": "Nom d'utilisateur pour l'API de Fever",
|
||||
"form.integration.fever_password": "Mot de passe pour l'API de Fever",
|
||||
@@ -418,7 +456,7 @@
|
||||
"form.integration.espial_endpoint": "URL de l'API de Espial",
|
||||
"form.integration.espial_api_key": "Clé d'API de Espial",
|
||||
"form.integration.espial_tags": "Libellés de Espial",
|
||||
"form.integration.readwise_activate": "Enregistrer les entrées dans Readwise Reader",
|
||||
"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",
|
||||
"form.integration.telegram_bot_activate": "Envoyer les nouveaux articles vers Telegram",
|
||||
@@ -427,17 +465,17 @@
|
||||
"form.integration.telegram_topic_id": "Identifiant du sujet (Topic ID)",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Désactiver l'aperçu de la page Web",
|
||||
"form.integration.telegram_bot_disable_notification": "Désactiver les notifications",
|
||||
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_tags": "LinkAce Tags",
|
||||
"form.integration.linkace_is_private": "Mark link as private",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
"form.integration.telegram_bot_disable_buttons": "Désactiver les boutons",
|
||||
"form.integration.linkace_activate": "Enregistrer les entrées vers LinkAce",
|
||||
"form.integration.linkace_endpoint": "Point de terminaison de l'API LinkAce",
|
||||
"form.integration.linkace_api_key": "Clé d'API LinkAce",
|
||||
"form.integration.linkace_tags": "Étiquettes LinkAce",
|
||||
"form.integration.linkace_is_private": "Marquer le lien comme privé",
|
||||
"form.integration.linkace_check_disabled": "Désactiver la vérification des liens",
|
||||
"form.integration.linkding_activate": "Sauvegarder les articles vers Linkding",
|
||||
"form.integration.linkding_endpoint": "URL de l'API de Linkding",
|
||||
"form.integration.linkding_api_key": "Clé d'API de Linkding",
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkding_tags": "Libellés",
|
||||
"form.integration.linkding_bookmark": "Marquer le lien comme non lu",
|
||||
"form.integration.linkwarden_activate": "Sauvegarder les articles vers Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "URL de l'API de Linkwarden",
|
||||
@@ -447,6 +485,15 @@
|
||||
"form.integration.matrix_bot_password": "Mot de passe de l'utilisateur Matrix",
|
||||
"form.integration.matrix_bot_url": "URL du serveur Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Identifiant de la salle Matrix",
|
||||
"form.integration.raindrop_activate": "Enregistrer les entrées vers Raindrop",
|
||||
"form.integration.raindrop_token": "Jeton d'accès de Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Identifiant de la collection",
|
||||
"form.integration.raindrop_tags": "Libellés (séparées par des virgules)",
|
||||
"form.integration.readeck_activate": "Sauvegarder les articles vers Readeck",
|
||||
"form.integration.readeck_endpoint": "URL de l'API de Readeck",
|
||||
"form.integration.readeck_api_key": "Clé d'API de Readeck",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "Envoyer uniquement l'URL (au lieu du contenu complet)",
|
||||
"form.integration.shiori_activate": "Sauvegarder les articles vers Shiori",
|
||||
"form.integration.shiori_endpoint": "URL de l'API de Shiori",
|
||||
"form.integration.shiori_username": "Nom d'utilisateur de Shiori",
|
||||
@@ -459,6 +506,13 @@
|
||||
"form.integration.webhook_secret": "Secret du webhook",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.ntfy_activate": "Envoyer les entrées vers ntfy",
|
||||
"form.integration.ntfy_topic": "Sujet Ntfy",
|
||||
"form.integration.ntfy_url": "URL de Ntfy (optionnel, ntfy.sh par défaut)",
|
||||
"form.integration.ntfy_api_token": "Jeton d'API Ntfy (optionnel)",
|
||||
"form.integration.ntfy_username": "Nom d'utilisateur Ntfy (optionnel)",
|
||||
"form.integration.ntfy_password": "Mot de passe Ntfy (facultatif)",
|
||||
"form.integration.ntfy_icon_url": "URL de l'icône Ntfy (facultatif)",
|
||||
"form.api_key.label.description": "Libellé de la clé d'API",
|
||||
"form.submit.loading": "Chargement...",
|
||||
"form.submit.saving": "Sauvegarde en cours...",
|
||||
@@ -498,7 +552,7 @@
|
||||
"error.http_body_read": "Impossible de lire le corps de la réponse HTTP : %v.",
|
||||
"error.http_empty_response_body": "Le corps de la réponse HTTP est vide.",
|
||||
"error.http_empty_response": "La réponse HTTP est vide. Peut-être que ce site web bloque Miniflux avec une protection anti-bot ?",
|
||||
"error.tls_error": "Erreur TLS : %v. Vous pouvez désactiver la vérification TLS dans les paramètres de l'abonnement.",
|
||||
"error.tls_error": "Erreur TLS : %q. Vous pouvez désactiver la vérification TLS dans les paramètres de l'abonnement.",
|
||||
"error.network_operation": "Miniflux n'est pas en mesure de se connecter à ce site web à cause d'un problème réseau : %v.",
|
||||
"error.network_timeout": "Ce site web est trop lent à répondre : %v.",
|
||||
"error.http_client_error": "Erreur du client HTTP : %v.",
|
||||
@@ -517,5 +571,16 @@
|
||||
"error.unable_to_parse_feed": "Impossible d'analyser ce flux : %v.",
|
||||
"error.feed_not_found": "Impossible de trouver ce flux.",
|
||||
"error.unable_to_detect_rssbridge": "Impossible de détecter un flux RSS en utilisant RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Impossible de détecter le format du flux : %v."
|
||||
"error.feed_format_not_detected": "Impossible de détecter le format du flux : %v.",
|
||||
"form.prefs.label.media_playback_rate": "Vitesse de lecture de l'audio/vidéo",
|
||||
"error.settings_media_playback_rate_range": "La vitesse de lecture est hors limites",
|
||||
"enclosure_media_controls.seek" : "Avancer/Reculer :",
|
||||
"enclosure_media_controls.seek.title" : "Avancer/Reculer de %s seconds",
|
||||
"enclosure_media_controls.speed" : "Vitesse :",
|
||||
"enclosure_media_controls.speed.faster" : "Accélérer",
|
||||
"enclosure_media_controls.speed.faster.title" : "Accélérer de %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Ralentir",
|
||||
"enclosure_media_controls.speed.slower.title" : "Ralentir de %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Réinitialiser",
|
||||
"enclosure_media_controls.speed.reset.title" : "Réinitialiser la vitesse de lecture à 1x"
|
||||
}
|
||||
|
||||
@@ -35,11 +35,13 @@
|
||||
"menu.about": "के बारे में",
|
||||
"menu.export": "निर्यात करे",
|
||||
"menu.import": "आयात करे",
|
||||
"menu.search": "खोज",
|
||||
"menu.create_category": "श्रेणी बनाए",
|
||||
"menu.mark_page_as_read": "इस पृष्ठ को पढ़ा हुआ चिह्नित करें",
|
||||
"menu.mark_all_as_read": "सभी को पढ़ा हुआ मार्क करें",
|
||||
"menu.show_all_entries": "सभी प्रविष्टियाँ दिखाए",
|
||||
"menu.show_only_unread_entries": "सभी अपठित प्रविष्टियाँ दिखाए",
|
||||
"menu.show_only_starred_entries": "Show only starred entries",
|
||||
"menu.refresh_feed": "ताज़ा करें",
|
||||
"menu.refresh_all_feeds": "पृष्ठभूमि में सभी फ़ीड को ताज़ा करें",
|
||||
"menu.edit_feed": "फ़ीड संपाद करे",
|
||||
@@ -54,7 +56,9 @@
|
||||
"search.label": "खोजे",
|
||||
"search.placeholder": "खोजे...",
|
||||
"search.submit": "Search",
|
||||
"pagination.last": "Last",
|
||||
"pagination.next": "अगला",
|
||||
"pagination.first": "First",
|
||||
"pagination.previous": "पिछला",
|
||||
"entry.status.unread": "अपठित",
|
||||
"entry.status.read": "पढ़े",
|
||||
@@ -177,6 +181,8 @@
|
||||
"page.keyboard_shortcuts.go_to_feed": "फ़ीड पर जाएं",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "पिछले पृष्ठ पर जाएं",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "अगले पेज पर जाएं",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "निचले आइटम पर जाएँ",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "शीर्ष आइटम पर जाएँ",
|
||||
"page.keyboard_shortcuts.open_item": "चयनित आइटम खोलें",
|
||||
"page.keyboard_shortcuts.open_original": "मूल लिंक खोलें",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "वर्तमान टैब में मूल लिंक खोलें",
|
||||
@@ -205,8 +211,8 @@
|
||||
"page.settings.title": "समायोजन",
|
||||
"page.settings.link_google_account": "मेरा गूगल खाता जोरीय",
|
||||
"page.settings.unlink_google_account": "मेरा गूगल खाता हटाय",
|
||||
"page.settings.link_oidc_account": "मेरा ओपन-ईद खाता जोरीय",
|
||||
"page.settings.unlink_oidc_account": "मेरा ओपन-ईद खाता हटाय",
|
||||
"page.settings.link_oidc_account": "मेरा ओपन-ईद खाता जोरीय (%s)",
|
||||
"page.settings.unlink_oidc_account": "मेरा ओपन-ईद खाता हटाय (%s)",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
@@ -214,13 +220,14 @@
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.register": "रजिस्टर पासकी",
|
||||
"page.settings.webauthn.register.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.settings.webauthn.delete": [
|
||||
"%d पासकुंजी निकालें",
|
||||
"%d पासकी हटाएं"
|
||||
],
|
||||
"page.login.title": "साइन इन करें",
|
||||
"page.login.google_signin": "गूगल के साथ साइन इन करें",
|
||||
"page.login.oidc_signin": "ओपन-ईद के साथ साइन इन करें",
|
||||
"page.login.oidc_signin": "ओपन-ईद के साथ साइन इन करें (%s)",
|
||||
"page.login.webauthn_login": "पासकी से लॉगिन करें",
|
||||
"page.login.webauthn_login.error": "पासकी से लॉगिन करने में असमर्थ",
|
||||
"page.integrations.title": "एकीकरण",
|
||||
@@ -255,6 +262,7 @@
|
||||
"alert.no_bookmark": "इस समय कोई बुकमार्क नहीं है",
|
||||
"alert.no_category": "कोई श्रेणी नहीं है।",
|
||||
"alert.no_category_entry": "इस श्रेणी में कोई विषय-वस्तु नहीं है।",
|
||||
"alert.no_tag_entry": "इस टैग से मेल खाती कोई प्रविष्टियाँ नहीं हैं।",
|
||||
"alert.no_feed_entry": "इस फ़ीड के लिए कोई विषय-वस्तु नहीं है।",
|
||||
"alert.no_feed": "आपके पास कोई सदस्यता नहीं है।",
|
||||
"alert.no_feed_in_category": "इस श्रेणी के लिए कोई सदस्यता नहीं है।",
|
||||
@@ -296,6 +304,14 @@
|
||||
"error.password_min_length": "पासवर्ड में कम से कम 6 अक्षर होने चाहिए।",
|
||||
"error.settings_mandatory_fields": "उपयोगकर्ता नाम, विषयवस्तु, भाषा और समयक्षेत्र फ़ील्ड अनिवार्य हैं।",
|
||||
"error.settings_reading_speed_is_positive": "पढ़ने की गति सकारात्मक पूर्णांक होनी चाहिए।",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.entries_per_page_invalid": "प्रति पृष्ठ प्रविष्टियों की संख्या मान्य नहीं है।",
|
||||
"error.feed_mandatory_fields": "URL और श्रेणी अनिवार्य हैं।",
|
||||
"error.feed_already_exists": "यह फ़ीड पहले से मौजूद है.",
|
||||
@@ -313,6 +329,7 @@
|
||||
"form.feed.label.title": "शीर्षक",
|
||||
"form.feed.label.site_url": "साइट यूआरएल",
|
||||
"form.feed.label.feed_url": "फ़ीड यूआरएल",
|
||||
"form.feed.label.description": "विवरण",
|
||||
"form.feed.label.category": "श्रेणी",
|
||||
"form.feed.label.crawler": "मूल सामग्री प्राप्त करें",
|
||||
"form.feed.label.feed_username": "फ़ीड उपयोगकर्ता नाम",
|
||||
@@ -327,10 +344,20 @@
|
||||
"form.feed.label.urlrewrite_rules": " यूआरएल पुनर्लेखन नियम",
|
||||
"form.feed.label.ignore_http_cache": "एचटीटीपी कैश पर ध्यान न दें",
|
||||
"form.feed.label.allow_self_signed_certificates": "स्व-हस्ताक्षरित या अमान्य प्रमाणपत्रों की अनुमति दें",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.fetch_via_proxy": "प्रॉक्सी के माध्यम से प्राप्त करें",
|
||||
"form.feed.label.disabled": "इस फ़ीड को रीफ़्रेश न करें",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.hide_globally": "वैश्विक अपठित सूची में प्रविष्टियां छिपाएं",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
@@ -367,15 +394,26 @@
|
||||
"form.prefs.label.gesture_nav": "प्रविष्टियों के बीच नेविगेट करने के लिए इशारा",
|
||||
"form.prefs.label.show_reading_time": "विषय के लिए अनुमानित पढ़ने का समय दिखाएं",
|
||||
"form.prefs.label.custom_css": "कस्टम सीएसएस",
|
||||
"form.prefs.label.custom_js": "कस्टम जेएस",
|
||||
"form.prefs.label.entry_order": "प्रवेश छँटाई कॉलम",
|
||||
"form.prefs.label.default_home_page": "डिफ़ॉल्ट होमपेज़",
|
||||
"form.prefs.label.categories_sorting_order": "श्रेणियाँ छँटाई",
|
||||
"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_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"form.import.label.file": "ओपीएमएल फ़ाइल",
|
||||
"form.import.label.url": "यूआरएल",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.fever_activate": "फीवर एपीआई सक्रिय करें",
|
||||
"form.integration.fever_username": "फीवर उपयोगकर्ता नाम",
|
||||
"form.integration.fever_password": "फीवर पासवर्ड",
|
||||
@@ -447,6 +485,15 @@
|
||||
"form.integration.matrix_bot_password": "मैट्रिक्स उपयोगकर्ता के लिए पासवर्ड",
|
||||
"form.integration.matrix_bot_url": "मैट्रिक्स सर्वर URL",
|
||||
"form.integration.matrix_bot_chat_id": "मैट्रिक्स रूम की आईडी",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.readeck_activate": "Readeck में विषयवस्तु सहेजें",
|
||||
"form.integration.readeck_endpoint": "Readeck·एपीआई·समापन·बिंदु",
|
||||
"form.integration.readeck_api_key": "Readeck एपीआई कुंजी",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "केवल URL भेजें (पूर्ण सामग्री के बजाय)",
|
||||
"form.integration.shiori_activate": "Save articles to Shiori",
|
||||
"form.integration.shiori_endpoint": "Shiori API Endpoint",
|
||||
"form.integration.shiori_username": "Shiori Username",
|
||||
@@ -459,6 +506,13 @@
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_topic": "Ntfy topic",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.api_key.label.description": "एपीआई कुंजी लेबल",
|
||||
"form.submit.loading": "लोड हो रहा है...",
|
||||
"form.submit.saving": "सहेजा जा रहा है...",
|
||||
@@ -498,7 +552,7 @@
|
||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"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.http_client_error": "HTTP client error: %v.",
|
||||
@@ -517,5 +571,16 @@
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||
}
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "ऑडियो/वीडियो की प्लेबैक गति",
|
||||
"error.settings_media_playback_rate_range": "प्लेबैक गति सीमा से बाहर है",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
||||
@@ -35,11 +35,13 @@
|
||||
"menu.about": "Tentang",
|
||||
"menu.export": "Ekspor",
|
||||
"menu.import": "Impor",
|
||||
"menu.search": "Cari",
|
||||
"menu.create_category": "Buat kategori",
|
||||
"menu.mark_page_as_read": "Tandai halaman ini sebagai telah dibaca",
|
||||
"menu.mark_all_as_read": "Tandai semua sebagai telah dibaca",
|
||||
"menu.show_all_entries": "Tampilkan semua entri",
|
||||
"menu.show_only_unread_entries": "Tampilkan hanya entri yang belum dibaca",
|
||||
"menu.show_only_starred_entries": "Show only starred entries",
|
||||
"menu.refresh_feed": "Muat ulang",
|
||||
"menu.refresh_all_feeds": "Muat ulang semua umpan di latar belakang",
|
||||
"menu.edit_feed": "Sunting",
|
||||
@@ -55,6 +57,8 @@
|
||||
"search.placeholder": "Cari...",
|
||||
"search.submit": "Search",
|
||||
"pagination.next": "Berikutnya",
|
||||
"pagination.last": "Last",
|
||||
"pagination.first": "First",
|
||||
"pagination.previous": "Sebelumnya",
|
||||
"entry.status.unread": "Belum dibaca",
|
||||
"entry.status.read": "Telah dibaca",
|
||||
@@ -83,38 +87,33 @@
|
||||
"entry.shared_entry.title": "Buka tautan publik",
|
||||
"entry.shared_entry.label": "Bagikan",
|
||||
"entry.estimated_reading_time": [
|
||||
"%d menit untuk dibaca"
|
||||
"%d menit untuk dibaca"
|
||||
],
|
||||
"entry.tags.label": "Tanda:",
|
||||
"page.shared_entries.title": "Entri yang Dibagikan",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries"
|
||||
"%d shared entry"
|
||||
],
|
||||
"page.unread.title": "Belum Dibaca",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries"
|
||||
"%d unread entry"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total"
|
||||
"%d entry in total"
|
||||
],
|
||||
"page.starred.title": "Markah",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries"
|
||||
"%d starred entry"
|
||||
],
|
||||
"page.categories.title": "Kategori",
|
||||
"page.categories.no_feed": "Tidak ada umpan.",
|
||||
"page.categories.entries": "Artikel",
|
||||
"page.categories.feeds": "Langganan",
|
||||
"page.categories.feed_count": [
|
||||
"Ada %d umpan."
|
||||
"Ada %d umpan."
|
||||
],
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories"
|
||||
"%d category"
|
||||
],
|
||||
"page.new_category.title": "Kategori Baru",
|
||||
"page.new_user.title": "Pengguna Baru",
|
||||
@@ -126,12 +125,11 @@
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.read_counter": "Jumlah entri yang telah dibaca",
|
||||
"page.feeds.error_count": [
|
||||
"%d galat"
|
||||
"%d galat"
|
||||
],
|
||||
"page.history.title": "Riwayat",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries"
|
||||
"%d read entry"
|
||||
],
|
||||
"page.import.title": "Impor",
|
||||
"page.search.title": "Hasil Pencarian",
|
||||
@@ -174,6 +172,8 @@
|
||||
"page.keyboard_shortcuts.go_to_feed": "Ke umpan",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "Ke halaman sebelumnya",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "Ke halaman berikutnya",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "Pergi ke item paling bawah",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "Pergi ke item teratas",
|
||||
"page.keyboard_shortcuts.open_item": "Buka entri yang dipilih",
|
||||
"page.keyboard_shortcuts.open_original": "Buka tautan asli",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "Buka tautan asli di bilah saat ini",
|
||||
@@ -202,8 +202,8 @@
|
||||
"page.settings.title": "Pengaturan",
|
||||
"page.settings.link_google_account": "Tautkan akun Google saya",
|
||||
"page.settings.unlink_google_account": "Putuskan akun Google saya",
|
||||
"page.settings.link_oidc_account": "Tautkan akun OpenID Connect saya",
|
||||
"page.settings.unlink_oidc_account": "Putuskan akun OpenID Connect saya",
|
||||
"page.settings.link_oidc_account": "Tautkan akun %s saya",
|
||||
"page.settings.unlink_oidc_account": "Putuskan akun %s saya",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
@@ -211,13 +211,13 @@
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.register": "Register passkey",
|
||||
"page.settings.webauthn.register.error": "Unable to register 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.settings.webauthn.delete": [
|
||||
"Remove %d passkey",
|
||||
"Remove %d passkeys"
|
||||
"Remove %d passkey"
|
||||
],
|
||||
"page.login.title": "Masuk",
|
||||
"page.login.google_signin": "Masuk dengan Google",
|
||||
"page.login.oidc_signin": "Masuk dengan OpenID Connect",
|
||||
"page.login.oidc_signin": "Masuk dengan %s",
|
||||
"page.login.webauthn_login": "Login with passkey",
|
||||
"page.login.webauthn_login.error": "Unable to login with passkey",
|
||||
"page.integrations.title": "Integrasi",
|
||||
@@ -252,6 +252,7 @@
|
||||
"alert.no_bookmark": "Tidak ada markah.",
|
||||
"alert.no_category": "Tidak ada kategori.",
|
||||
"alert.no_category_entry": "Tidak ada artikel di kategori ini.",
|
||||
"alert.no_tag_entry": "Tidak ada entri yang cocok dengan tag ini.",
|
||||
"alert.no_feed_entry": "Tidak ada artikel di umpan ini.",
|
||||
"alert.no_feed": "Anda tidak memiliki langganan.",
|
||||
"alert.no_feed_in_category": "Tidak ada langganan untuk kategori ini.",
|
||||
@@ -293,6 +294,14 @@
|
||||
"error.password_min_length": "Kata sandi harus memiliki setidaknya 6 karakter.",
|
||||
"error.settings_mandatory_fields": "Harus ada nama pengguna, tema, bahasa, dan zona waktu.",
|
||||
"error.settings_reading_speed_is_positive": "Kecepatan membaca harus integer positif.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.entries_per_page_invalid": "Jumlah entri per halaman tidak valid.",
|
||||
"error.feed_mandatory_fields": "Harus ada URL dan kategorinya.",
|
||||
"error.feed_already_exists": "Umpan ini sudah ada.",
|
||||
@@ -310,6 +319,7 @@
|
||||
"form.feed.label.title": "Judul",
|
||||
"form.feed.label.site_url": "URL Situs",
|
||||
"form.feed.label.feed_url": "URL Umpan",
|
||||
"form.feed.label.description": "Deskripsi",
|
||||
"form.feed.label.category": "Kategori",
|
||||
"form.feed.label.crawler": "Ambil konten asli",
|
||||
"form.feed.label.feed_username": "Nama Pengguna Umpan",
|
||||
@@ -324,10 +334,20 @@
|
||||
"form.feed.label.urlrewrite_rules": "Aturan Tulis Ulang URL",
|
||||
"form.feed.label.ignore_http_cache": "Abaikan Tembolok HTTP",
|
||||
"form.feed.label.allow_self_signed_certificates": "Perbolehkan sertifikat web tidak valid atau sertifikasi sendiri",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.fetch_via_proxy": "Ambil via Proksi",
|
||||
"form.feed.label.disabled": "Jangan perbarui umpan ini",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.hide_globally": "Sembunyikan entri di daftar belum dibaca global",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
@@ -364,15 +384,26 @@
|
||||
"form.prefs.label.gesture_nav": "Isyarat untuk menavigasi antar entri",
|
||||
"form.prefs.label.show_reading_time": "Tampilkan perkiraan waktu baca untuk artikel",
|
||||
"form.prefs.label.custom_css": "Modifikasi CSS",
|
||||
"form.prefs.label.custom_js": "Modifikasi JavaScript",
|
||||
"form.prefs.label.entry_order": "Pengurutan Kolom Entri",
|
||||
"form.prefs.label.default_home_page": "Beranda Baku",
|
||||
"form.prefs.label.categories_sorting_order": "Pengurutan Kategori",
|
||||
"form.prefs.label.mark_read_on_view": "Secara otomatis menandai entri sebagai telah dibaca saat dilihat",
|
||||
"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_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"form.import.label.file": "Berkas OPML",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.fever_activate": "Aktifkan API Fever",
|
||||
"form.integration.fever_username": "Nama Pengguna Fever",
|
||||
"form.integration.fever_password": "Kata Sandi Fever",
|
||||
@@ -444,6 +475,15 @@
|
||||
"form.integration.matrix_bot_password": "Kata Sandi Matrix",
|
||||
"form.integration.matrix_bot_url": "URL Peladen Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID Ruang Matrix",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.readeck_activate": "Simpan artikel ke Readeck",
|
||||
"form.integration.readeck_endpoint": "Titik URL API Readeck",
|
||||
"form.integration.readeck_api_key": "Kunci API Readeck",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "Kirim hanya URL (alih-alih konten penuh)",
|
||||
"form.integration.shiori_activate": "Save articles to Shiori",
|
||||
"form.integration.shiori_endpoint": "Shiori API Endpoint",
|
||||
"form.integration.shiori_username": "Shiori Username",
|
||||
@@ -456,6 +496,13 @@
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_topic": "Ntfy topic",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.api_key.label.description": "Label Kunci API",
|
||||
"form.submit.loading": "Memuat...",
|
||||
"form.submit.saving": "Menyimpan...",
|
||||
@@ -463,33 +510,32 @@
|
||||
"time_elapsed.yesterday": "kemarin",
|
||||
"time_elapsed.now": "baru saja",
|
||||
"time_elapsed.minutes": [
|
||||
"%d menit yang lalu"
|
||||
"%d menit yang lalu"
|
||||
],
|
||||
"time_elapsed.hours": [
|
||||
"%d jam yang lalu"
|
||||
"%d jam yang lalu"
|
||||
],
|
||||
"time_elapsed.days": [
|
||||
"%d hari yang lalu"
|
||||
"%d hari yang lalu"
|
||||
],
|
||||
"time_elapsed.weeks": [
|
||||
"%d pekan yang lalu"
|
||||
"%d pekan yang lalu"
|
||||
],
|
||||
"time_elapsed.months": [
|
||||
"%d bulan yang lalu"
|
||||
"%d bulan yang lalu"
|
||||
],
|
||||
"time_elapsed.years": [
|
||||
"%d tahun yang lalu"
|
||||
"%d tahun yang lalu"
|
||||
],
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again.",
|
||||
"You have triggered too many feed refreshes. Please wait %d minutes before trying again."
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again."
|
||||
],
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"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_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"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.http_client_error": "HTTP client error: %v.",
|
||||
@@ -508,5 +554,16 @@
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Kecepatan pemutaran audio/video",
|
||||
"error.settings_media_playback_rate_range": "Kecepatan pemutaran di luar jangkauan",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
||||
@@ -35,11 +35,13 @@
|
||||
"menu.about": "Informazioni",
|
||||
"menu.export": "Esporta",
|
||||
"menu.import": "Importa",
|
||||
"menu.search": "Cerca",
|
||||
"menu.create_category": "Aggiungi una categoria",
|
||||
"menu.mark_page_as_read": "Segna questa pagina come letta",
|
||||
"menu.mark_all_as_read": "Segna tutti gli articoli come letti",
|
||||
"menu.show_all_entries": "Mostra tutte le voci",
|
||||
"menu.show_only_unread_entries": "Mostra solo voci non lette",
|
||||
"menu.show_only_starred_entries": "Mostra solo voci preferiti",
|
||||
"menu.refresh_feed": "Aggiorna",
|
||||
"menu.refresh_all_feeds": "Aggiorna tutti i feed in background",
|
||||
"menu.edit_feed": "Modifica",
|
||||
@@ -55,6 +57,8 @@
|
||||
"search.placeholder": "Cerca...",
|
||||
"search.submit": "Search",
|
||||
"pagination.next": "Successivo",
|
||||
"pagination.last": "Last",
|
||||
"pagination.first": "First",
|
||||
"pagination.previous": "Precedente",
|
||||
"entry.status.unread": "Da leggere",
|
||||
"entry.status.read": "Letto",
|
||||
@@ -177,6 +181,8 @@
|
||||
"page.keyboard_shortcuts.go_to_feed": "Mostra il feed",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "Mostra la pagina precedente",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "Mostra la pagina successiva",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "Vai all'elemento in fondo",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "Vai all'elemento principale",
|
||||
"page.keyboard_shortcuts.open_item": "Apri l'articolo selezionato",
|
||||
"page.keyboard_shortcuts.open_original": "Apri la pagina web originale",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "Apri il link originale nella scheda corrente",
|
||||
@@ -205,8 +211,8 @@
|
||||
"page.settings.title": "Impostazioni",
|
||||
"page.settings.link_google_account": "Collega il mio account Google",
|
||||
"page.settings.unlink_google_account": "Scollega il mio account Google",
|
||||
"page.settings.link_oidc_account": "Collega il mio account OpenID Connect",
|
||||
"page.settings.unlink_oidc_account": "Scollega il mio account OpenID Connect",
|
||||
"page.settings.link_oidc_account": "Collega il mio account %s",
|
||||
"page.settings.unlink_oidc_account": "Scollega il mio account %s",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
@@ -214,13 +220,14 @@
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.register": "Registra la chiave di accesso",
|
||||
"page.settings.webauthn.register.error": "Impossibile registrare la 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.settings.webauthn.delete": [
|
||||
"Rimuovi %d passkey",
|
||||
"Rimuovi %d passkey"
|
||||
],
|
||||
"page.login.title": "Accedi",
|
||||
"page.login.google_signin": "Accedi tramite Google",
|
||||
"page.login.oidc_signin": "Accedi tramite OpenID Connect",
|
||||
"page.login.oidc_signin": "Accedi tramite %s",
|
||||
"page.login.webauthn_login": "Accedi con passkey",
|
||||
"page.login.webauthn_login.error": "Impossibile accedere con passkey",
|
||||
"page.integrations.title": "Integrazioni",
|
||||
@@ -255,6 +262,7 @@
|
||||
"alert.no_bookmark": "Nessun preferito disponibile.",
|
||||
"alert.no_category": "Nessuna categoria disponibile.",
|
||||
"alert.no_category_entry": "Questa categoria non contiene alcun articolo.",
|
||||
"alert.no_tag_entry": "Non ci sono voci corrispondenti a questo tag.",
|
||||
"alert.no_feed_entry": "Questo feed non contiene alcun articolo.",
|
||||
"alert.no_feed": "Nessun feed disponibile.",
|
||||
"alert.no_feed_in_category": "Non esiste un abbonamento per questa categoria.",
|
||||
@@ -289,6 +297,14 @@
|
||||
"error.password_min_length": "La password deve contenere almeno 6 caratteri.",
|
||||
"error.settings_mandatory_fields": "Il nome utente, il tema, la lingua ed il fuso orario sono campi obbligatori.",
|
||||
"error.settings_reading_speed_is_positive": "Le velocità di lettura devono essere numeri interi positivi.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.entries_per_page_invalid": "Il numero di articoli per pagina non è valido.",
|
||||
"error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.",
|
||||
"error.feed_already_exists": "Questo feed esiste già.",
|
||||
@@ -313,6 +329,7 @@
|
||||
"form.feed.label.title": "Titolo",
|
||||
"form.feed.label.site_url": "URL del sito",
|
||||
"form.feed.label.feed_url": "URL del feed",
|
||||
"form.feed.label.description": "Descrizione",
|
||||
"form.feed.label.category": "Categoria",
|
||||
"form.feed.label.crawler": "Scarica il contenuto integrale",
|
||||
"form.feed.label.feed_username": "Nome utente del feed",
|
||||
@@ -327,10 +344,20 @@
|
||||
"form.feed.label.urlrewrite_rules": "Regole di riscrittura URL",
|
||||
"form.feed.label.ignore_http_cache": "Ignora cache HTTP",
|
||||
"form.feed.label.allow_self_signed_certificates": "Consenti certificati autofirmati o non validi",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.fetch_via_proxy": "Recuperare tramite proxy",
|
||||
"form.feed.label.disabled": "Non aggiornare questo feed",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.hide_globally": "Nascondere le voci nella lista globale dei non letti",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
@@ -367,15 +394,26 @@
|
||||
"form.prefs.label.gesture_nav": "Gesto per navigare tra le voci",
|
||||
"form.prefs.label.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli",
|
||||
"form.prefs.label.custom_css": "CSS personalizzati",
|
||||
"form.prefs.label.custom_js": "JavaScript personalizzati",
|
||||
"form.prefs.label.entry_order": "Colonna di ordinamento delle voci",
|
||||
"form.prefs.label.default_home_page": "Pagina iniziale predefinita",
|
||||
"form.prefs.label.categories_sorting_order": "Ordinamento delle categorie",
|
||||
"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_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"form.import.label.file": "File OPML",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.fever_activate": "Abilita l'API di Fever",
|
||||
"form.integration.fever_username": "Nome utente dell'account Fever",
|
||||
"form.integration.fever_password": "Password dell'account Fever",
|
||||
@@ -447,6 +485,15 @@
|
||||
"form.integration.matrix_bot_password": "Password per l'utente Matrix",
|
||||
"form.integration.matrix_bot_url": "URL del server Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID della stanza Matrix",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.readeck_activate": "Salva gli articoli su Readeck",
|
||||
"form.integration.readeck_endpoint": "Endpoint dell'API di Readeck",
|
||||
"form.integration.readeck_api_key": "API key dell'account Readeck",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "Invia solo URL (invece del contenuto completo)",
|
||||
"form.integration.shiori_activate": "Salva gli articoli su Shiori",
|
||||
"form.integration.shiori_endpoint": "Endpoint dell'API di Shiori",
|
||||
"form.integration.shiori_username": "Nome utente dell'account Shiori",
|
||||
@@ -460,6 +507,13 @@
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.api_key.label.description": "Etichetta chiave API",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_topic": "Ntfy topic",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.submit.loading": "Caricamento in corso...",
|
||||
"form.submit.saving": "Salvataggio in corso...",
|
||||
"time_elapsed.not_yet": "non ancora",
|
||||
@@ -498,7 +552,7 @@
|
||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"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.http_client_error": "HTTP client error: %v.",
|
||||
@@ -517,5 +571,16 @@
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Velocità di riproduzione dell'audio/video",
|
||||
"error.settings_media_playback_rate_range": "La velocità di riproduzione non rientra nell'intervallo",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
||||
@@ -35,11 +35,13 @@
|
||||
"menu.about": "ソフトウェア情報",
|
||||
"menu.export": "エクスポート",
|
||||
"menu.import": "インポート",
|
||||
"menu.search": "検索",
|
||||
"menu.create_category": "カテゴリを作成",
|
||||
"menu.mark_page_as_read": "このページを既読にする",
|
||||
"menu.mark_all_as_read": "すべて既読にする",
|
||||
"menu.show_all_entries": "すべての記事を表示",
|
||||
"menu.show_only_unread_entries": "未読の記事だけを表示",
|
||||
"menu.show_only_starred_entries": "Show only starred entries",
|
||||
"menu.refresh_feed": "更新",
|
||||
"menu.refresh_all_feeds": "すべてのフィードをバックグラウンドで更新",
|
||||
"menu.edit_feed": "編集",
|
||||
@@ -54,7 +56,9 @@
|
||||
"search.label": "検索",
|
||||
"search.placeholder": "…を検索",
|
||||
"search.submit": "Search",
|
||||
"pagination.last": "Last",
|
||||
"pagination.next": "次",
|
||||
"pagination.first": "First",
|
||||
"pagination.previous": "前",
|
||||
"entry.status.unread": "未読にする",
|
||||
"entry.status.read": "既読にする",
|
||||
@@ -83,40 +87,33 @@
|
||||
"entry.shared_entry.title": "公開リンクを開く",
|
||||
"entry.shared_entry.label": "共有する",
|
||||
"entry.estimated_reading_time": [
|
||||
"%d 分で読めます",
|
||||
"%d 分で読めます"
|
||||
],
|
||||
"entry.tags.label": "タグ:",
|
||||
"page.shared_entries.title": "共有エントリ",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries"
|
||||
"%d shared entry"
|
||||
],
|
||||
"page.unread.title": "未読",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries"
|
||||
"%d unread entry"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total"
|
||||
"%d entry in total"
|
||||
],
|
||||
"page.starred.title": "星付き",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries"
|
||||
"%d starred entry"
|
||||
],
|
||||
"page.categories.title": "カテゴリ",
|
||||
"page.categories.no_feed": "フィードはありません。",
|
||||
"page.categories.entries": "記事一覧",
|
||||
"page.categories.feeds": "フィード一覧",
|
||||
"page.categories.feed_count": [
|
||||
"%d 件のフィードがあります。",
|
||||
"%d 件のフィードがあります。"
|
||||
],
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories"
|
||||
"%d category"
|
||||
],
|
||||
"page.new_category.title": "新規カテゴリ",
|
||||
"page.new_user.title": "新規ユーザー",
|
||||
@@ -128,13 +125,11 @@
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.read_counter": "既読記事の数",
|
||||
"page.feeds.error_count": [
|
||||
"%d 個のエラー",
|
||||
"%d 個のエラー"
|
||||
],
|
||||
"page.history.title": "履歴",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries"
|
||||
"%d read entry"
|
||||
],
|
||||
"page.import.title": "インポート",
|
||||
"page.search.title": "検索結果",
|
||||
@@ -177,6 +172,8 @@
|
||||
"page.keyboard_shortcuts.go_to_feed": "フィード",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "前のページ",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "次のページ",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "一番下の項目に移動",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "先頭の項目に移動",
|
||||
"page.keyboard_shortcuts.open_item": "選択されたアイテムを開く",
|
||||
"page.keyboard_shortcuts.open_original": "オリジナルのリンクを開く",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "現在のタブでオリジナルのリンクを開く",
|
||||
@@ -205,8 +202,8 @@
|
||||
"page.settings.title": "設定",
|
||||
"page.settings.link_google_account": "Google アカウントと接続する",
|
||||
"page.settings.unlink_google_account": "Google アカウントと接続を解除する",
|
||||
"page.settings.link_oidc_account": "OpenID Connect アカウントと接続する",
|
||||
"page.settings.unlink_oidc_account": "OpenID Connect アカウントと接続を解除する",
|
||||
"page.settings.link_oidc_account": "%s アカウントと接続する",
|
||||
"page.settings.unlink_oidc_account": "%s アカウントと接続を解除する",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
@@ -214,13 +211,13 @@
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.register": "パスキーを登録する",
|
||||
"page.settings.webauthn.register.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.settings.webauthn.delete": [
|
||||
"%d 個のパスキーを削除",
|
||||
"%d 個のパスキーを削除"
|
||||
],
|
||||
"page.login.title": "ログイン",
|
||||
"page.login.google_signin": "Google アカウントでログイン",
|
||||
"page.login.oidc_signin": "OpenID Connect アカウントでログイン",
|
||||
"page.login.oidc_signin": "%s アカウントでログイン",
|
||||
"page.login.webauthn_login": "パスキーでログイン",
|
||||
"page.login.webauthn_login.error": "パスキーでログインできない",
|
||||
"page.integrations.title": "連携",
|
||||
@@ -255,6 +252,7 @@
|
||||
"alert.no_bookmark": "現在星付きはありません。",
|
||||
"alert.no_category": "カテゴリが存在しません。",
|
||||
"alert.no_category_entry": "このカテゴリには記事がありません。",
|
||||
"alert.no_tag_entry": "このタグに一致するエントリーはありません。",
|
||||
"alert.no_feed_entry": "このフィードには記事がありません。",
|
||||
"alert.no_feed": "何も購読していません。",
|
||||
"alert.no_feed_in_category": "このカテゴリには購読中のフィードがありません。",
|
||||
@@ -296,6 +294,14 @@
|
||||
"error.password_min_length": "パスワードは6文字以上である必要があります。",
|
||||
"error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンのすべてが必要です。",
|
||||
"error.settings_reading_speed_is_positive": "読書速度は正の整数である必要があります。",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.entries_per_page_invalid": "ページあたりの記事数が無効です。",
|
||||
"error.feed_mandatory_fields": "URL と カテゴリが必要です。",
|
||||
"error.feed_already_exists": "このフィードは既に存在します。",
|
||||
@@ -313,6 +319,7 @@
|
||||
"form.feed.label.title": "タイトル",
|
||||
"form.feed.label.site_url": "サイト URL",
|
||||
"form.feed.label.feed_url": "フィード URL",
|
||||
"form.feed.label.description": "説明",
|
||||
"form.feed.label.category": "カテゴリ",
|
||||
"form.feed.label.crawler": "オリジナルの内容を取得",
|
||||
"form.feed.label.feed_username": "フィードのユーザー名",
|
||||
@@ -327,10 +334,20 @@
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.ignore_http_cache": "HTTPキャッシュを無視",
|
||||
"form.feed.label.allow_self_signed_certificates": "自己署名証明書または無効な証明書を許可する",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.fetch_via_proxy": "プロキシ経由で取得",
|
||||
"form.feed.label.disabled": "このフィードを更新しない",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.hide_globally": "未読一覧に記事を表示しない",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
@@ -367,15 +384,26 @@
|
||||
"form.prefs.label.gesture_nav": "エントリ間を移動するジェスチャー",
|
||||
"form.prefs.label.show_reading_time": "記事の推定読書時間を表示する",
|
||||
"form.prefs.label.custom_css": "カスタム CSS",
|
||||
"form.prefs.label.custom_js": "カスタム JavaScript",
|
||||
"form.prefs.label.entry_order": "記事の表示順の基準",
|
||||
"form.prefs.label.default_home_page": "デフォルトのトップページ",
|
||||
"form.prefs.label.categories_sorting_order": "カテゴリの表示順",
|
||||
"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_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"form.import.label.file": "OPML ファイル",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.fever_activate": "Fever API を有効にする",
|
||||
"form.integration.fever_username": "Fever のユーザー名",
|
||||
"form.integration.fever_password": "Fever のパスワード",
|
||||
@@ -447,6 +475,15 @@
|
||||
"form.integration.matrix_bot_password": "Matrixユーザ用パスワード",
|
||||
"form.integration.matrix_bot_url": "MatrixサーバーのURL",
|
||||
"form.integration.matrix_bot_chat_id": "MatrixルームのID",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.readeck_activate": "Readeck に記事を保存する",
|
||||
"form.integration.readeck_endpoint": "Readeck の API Endpoint",
|
||||
"form.integration.readeck_api_key": "Readeck の API key",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "URL のみを送信 (完全なコンテンツではなく)",
|
||||
"form.integration.shiori_activate": "Shiori に記事を保存する",
|
||||
"form.integration.shiori_endpoint": "Shiori の API Endpoint",
|
||||
"form.integration.shiori_username": "Shiori の ユーザー名",
|
||||
@@ -459,6 +496,13 @@
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_topic": "Ntfy topic",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.api_key.label.description": "API キーラベル",
|
||||
"form.submit.loading": "読み込み中…",
|
||||
"form.submit.saving": "保存中…",
|
||||
@@ -466,39 +510,32 @@
|
||||
"time_elapsed.yesterday": "昨日",
|
||||
"time_elapsed.now": "今",
|
||||
"time_elapsed.minutes": [
|
||||
"%d 分前",
|
||||
"%d 分前"
|
||||
],
|
||||
"time_elapsed.hours": [
|
||||
"%d 時間前",
|
||||
"%d 時間前"
|
||||
],
|
||||
"time_elapsed.days": [
|
||||
"%d 日前",
|
||||
"%d 日前"
|
||||
],
|
||||
"time_elapsed.weeks": [
|
||||
"%d 週間前",
|
||||
"%d 週間前"
|
||||
],
|
||||
"time_elapsed.months": [
|
||||
"%d か月前",
|
||||
"%d か月前"
|
||||
],
|
||||
"time_elapsed.years": [
|
||||
"%d 年前",
|
||||
"%d 年前"
|
||||
],
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again.",
|
||||
"You have triggered too many feed refreshes. Please wait %d minutes before trying again."
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again."
|
||||
],
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"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_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"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.http_client_error": "HTTP client error: %v.",
|
||||
@@ -517,5 +554,16 @@
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "オーディオ/ビデオの再生速度",
|
||||
"error.settings_media_playback_rate_range": "再生速度が範囲外",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"skip_to_content": "Skip to content",
|
||||
"skip_to_content": "Ga naar inhoud",
|
||||
"confirm.question": "Weet je het zeker?",
|
||||
"confirm.question.refresh": "Wil je een gedwongen vernieuwing uitvoeren?",
|
||||
"confirm.question.refresh": "Wil je vernieuwen forceren?",
|
||||
"confirm.yes": "ja",
|
||||
"confirm.no": "nee",
|
||||
"confirm.loading": "Bezig...",
|
||||
"action.subscribe": "Abboneren",
|
||||
"action.subscribe": "Abonneren",
|
||||
"action.save": "Opslaan",
|
||||
"action.or": "of",
|
||||
"action.cancel": "annuleren",
|
||||
"action.remove": "Verwijderen",
|
||||
"action.remove_feed": "Verwijder deze feed",
|
||||
"action.update": "Updaten",
|
||||
"action.update": "Bijwerken",
|
||||
"action.edit": "Bewerken",
|
||||
"action.download": "Download",
|
||||
"action.import": "Importeren",
|
||||
@@ -20,7 +20,7 @@
|
||||
"tooltip.keyboard_shortcuts": "Sneltoets: %s",
|
||||
"tooltip.logged_user": "Ingelogd als %s",
|
||||
"menu.title": "Menu",
|
||||
"menu.home_page": "Home page",
|
||||
"menu.home_page": "Startpagina",
|
||||
"menu.unread": "Ongelezen",
|
||||
"menu.starred": "Favorieten",
|
||||
"menu.history": "Geschiedenis",
|
||||
@@ -31,15 +31,17 @@
|
||||
"menu.preferences": "Voorkeuren",
|
||||
"menu.integrations": "Integraties",
|
||||
"menu.sessions": "Sessies",
|
||||
"menu.users": "Users",
|
||||
"menu.users": "Gebruikers",
|
||||
"menu.about": "Over",
|
||||
"menu.export": "Exporteren",
|
||||
"menu.import": "Importeren",
|
||||
"menu.search": "Zoeken",
|
||||
"menu.create_category": "Categorie toevoegen",
|
||||
"menu.mark_page_as_read": "Markeer deze pagina als gelezen",
|
||||
"menu.mark_all_as_read": "Markeer alle items als gelezen",
|
||||
"menu.mark_all_as_read": "Markeer alles als gelezen",
|
||||
"menu.show_all_entries": "Toon alle artikelen",
|
||||
"menu.show_only_unread_entries": "Toon alleen ongelezen artikelen",
|
||||
"menu.show_only_starred_entries": "Toon alleen favorieten",
|
||||
"menu.refresh_feed": "Vernieuwen",
|
||||
"menu.refresh_all_feeds": "Vernieuw alle feeds in de achtergrond",
|
||||
"menu.edit_feed": "Bewerken",
|
||||
@@ -47,37 +49,39 @@
|
||||
"menu.add_feed": "Feed toevoegen",
|
||||
"menu.add_user": "Gebruiker toevoegen",
|
||||
"menu.flush_history": "Verwijder geschiedenis",
|
||||
"menu.feed_entries": "Lidwoord",
|
||||
"menu.feed_entries": "Artikelen",
|
||||
"menu.api_keys": "API-sleutels",
|
||||
"menu.create_api_key": "Maak een nieuwe API-sleutel",
|
||||
"menu.shared_entries": "Gedeelde vermeldingen",
|
||||
"menu.shared_entries": "Gedeelde artikelen",
|
||||
"search.label": "Zoeken",
|
||||
"search.placeholder": "Zoeken...",
|
||||
"search.submit": "Search",
|
||||
"search.submit": "Zoeken",
|
||||
"pagination.last": "Laatste",
|
||||
"pagination.next": "Volgende",
|
||||
"pagination.first": "Eerste",
|
||||
"pagination.previous": "Vorige",
|
||||
"entry.status.unread": "Ongelezen",
|
||||
"entry.status.read": "Gelezen",
|
||||
"entry.status.toast.unread": "Gemarkeerd als ongelezen",
|
||||
"entry.status.toast.read": "Gemarkeerd als gelezen",
|
||||
"entry.status.title": "Verander status van item",
|
||||
"entry.bookmark.toggle.on": "Ster toevoegen",
|
||||
"entry.bookmark.toggle.off": "Ster weghalen",
|
||||
"entry.bookmark.toast.on": "Met ster",
|
||||
"entry.bookmark.toast.off": "Ster verwijderd",
|
||||
"entry.state.saving": "Opslaag...",
|
||||
"entry.status.title": "Verander artikelstatus",
|
||||
"entry.bookmark.toggle.on": "Favoriet",
|
||||
"entry.bookmark.toggle.off": "Favoriet verwijderen",
|
||||
"entry.bookmark.toast.on": "Favoriet toegevoegd",
|
||||
"entry.bookmark.toast.off": "Favoriet verwijderd",
|
||||
"entry.state.saving": "Opslaan...",
|
||||
"entry.state.loading": "Laden...",
|
||||
"entry.save.label": "Opslaan",
|
||||
"entry.save.title": "Artikel opslaan",
|
||||
"entry.save.completed": "Done!",
|
||||
"entry.save.completed": "Klaar!",
|
||||
"entry.save.toast.completed": "Artikel opgeslagen",
|
||||
"entry.scraper.label": "Downloaden",
|
||||
"entry.scraper.title": "Fetch original content",
|
||||
"entry.scraper.title": "Originele inhoud ophalen",
|
||||
"entry.scraper.completed": "Klaar!",
|
||||
"entry.external_link.label": "Externe link",
|
||||
"entry.comments.label": "Comments",
|
||||
"entry.comments.title": "Bekijk de reacties",
|
||||
"entry.share.label": "Deel",
|
||||
"entry.comments.label": "Reacties",
|
||||
"entry.comments.title": "Bekijk reacties",
|
||||
"entry.share.label": "Delen",
|
||||
"entry.share.title": "Deel dit artikel",
|
||||
"entry.unshare.label": "Delen ongedaan maken",
|
||||
"entry.shared_entry.title": "Open de openbare link",
|
||||
@@ -86,115 +90,117 @@
|
||||
"%d minuut leestijd",
|
||||
"%d minuten leestijd"
|
||||
],
|
||||
"entry.tags.label": "Labels:",
|
||||
"page.shared_entries.title": "Gedeelde vermeldingen",
|
||||
"entry.tags.label": "Tags:",
|
||||
"page.shared_entries.title": "Gedeelde artikelen",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries"
|
||||
"%d gedeeld artikel",
|
||||
"%d gedeelde artikelen"
|
||||
],
|
||||
"page.unread.title": "Ongelezen",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries"
|
||||
"%d ongelezen artikel",
|
||||
"%d ongelezen artikelen"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total"
|
||||
"%d artikel totaal",
|
||||
"%d artikelen totaal"
|
||||
],
|
||||
"page.starred.title": "Favorieten",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries"
|
||||
"%d favoriet artikel",
|
||||
"%d favoriete artikelen"
|
||||
],
|
||||
"page.categories.title": "Categorieën",
|
||||
"page.categories.no_feed": "Geen feeds.",
|
||||
"page.categories.entries": "Lidwoord",
|
||||
"page.categories.feeds": "Abonnementen",
|
||||
"page.categories.no_feed": "Geen feed.",
|
||||
"page.categories.entries": "Artikelen",
|
||||
"page.categories.feeds": "Feeds",
|
||||
"page.categories.feed_count": [
|
||||
"Er is %d feed.",
|
||||
"Er zijn %d feeds."
|
||||
],
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories"
|
||||
"%d categorie",
|
||||
"%d categorieën"
|
||||
],
|
||||
"page.new_category.title": "Nieuwe categorie",
|
||||
"page.new_user.title": "Nieuwe gebruiker",
|
||||
"page.edit_category.title": "Bewerken van categorie: %s",
|
||||
"page.edit_category.title": "Bewerk categorie: %s",
|
||||
"page.edit_user.title": "Bewerk gebruiker: %s",
|
||||
"page.feeds.title": "Feeds",
|
||||
"page.category_label": "Category: %s",
|
||||
"page.feeds.last_check": "Laatste update:",
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.read_counter": "Aantal gelezen vermeldingen",
|
||||
"page.category_label": "Categorie: %s",
|
||||
"page.feeds.last_check": "Laatste controle:",
|
||||
"page.feeds.next_check": "Volgende controle:",
|
||||
"page.feeds.read_counter": "Aantal gelezen artikelen",
|
||||
"page.feeds.error_count": [
|
||||
"%d error",
|
||||
"%d errors"
|
||||
"%d fout",
|
||||
"%d fouten"
|
||||
],
|
||||
"page.history.title": "Geschiedenis",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries"
|
||||
"%d gelezen artikel",
|
||||
"%d gelezen artikelen"
|
||||
],
|
||||
"page.import.title": "Importeren",
|
||||
"page.login.title": "Inloggen",
|
||||
"page.search.title": "Zoekresultaten",
|
||||
"page.about.title": "Over",
|
||||
"page.about.credits": "Copyrights",
|
||||
"page.about.credits": "Credits",
|
||||
"page.about.version": "Versie:",
|
||||
"page.about.build_date": "Datum build:",
|
||||
"page.about.build_date": "Compilatiedatum:",
|
||||
"page.about.author": "Auteur:",
|
||||
"page.about.license": "Licentie:",
|
||||
"page.about.global_config_options": "globale configuratie-opties",
|
||||
"page.about.global_config_options": "Globale Configuratie Opties",
|
||||
"page.about.postgres_version": "Postgres versie:",
|
||||
"page.about.go_version": "Go versie:",
|
||||
"page.add_feed.title": "Nieuwe feed",
|
||||
"page.add_feed.no_category": "Er zijn geen categorieën. Je moet op zijn minst één caterogie hebben.",
|
||||
"page.add_feed.no_category": "Er is geen categorie. Je moet minstens één categorie hebben.",
|
||||
"page.add_feed.label.url": "URL",
|
||||
"page.add_feed.submit": "Feed zoeken",
|
||||
"page.add_feed.legend.advanced_options": "Geavanceerde mogelijkheden",
|
||||
"page.add_feed.legend.advanced_options": "Geavanceerde opties",
|
||||
"page.add_feed.choose_feed": "Feed kiezen",
|
||||
"page.edit_feed.title": "Bewerken van feed: %s",
|
||||
"page.edit_feed.last_check": "Laatste update:",
|
||||
"page.edit_feed.last_modified_header": "LastModified-header:",
|
||||
"page.edit_feed.etag_header": "ETAG-header:",
|
||||
"page.edit_feed.title": "Bewerk feed: %s",
|
||||
"page.edit_feed.last_check": "Laatste controle:",
|
||||
"page.edit_feed.last_modified_header": "LastModified header:",
|
||||
"page.edit_feed.etag_header": "ETAG header:",
|
||||
"page.edit_feed.no_header": "Geen",
|
||||
"page.edit_feed.last_parsing_error": "Laatste parse error",
|
||||
"page.edit_feed.last_parsing_error": "Laatste analysefout",
|
||||
"page.entry.attachments": "Bijlagen",
|
||||
"page.keyboard_shortcuts.title": "Sneltoetsen",
|
||||
"page.keyboard_shortcuts.subtitle.sections": "Naviguatie tussen menu's",
|
||||
"page.keyboard_shortcuts.subtitle.items": "Navigatie tussen items",
|
||||
"page.keyboard_shortcuts.subtitle.pages": "Naviguatie tussen pagina's",
|
||||
"page.keyboard_shortcuts.subtitle.actions": "Actions",
|
||||
"page.keyboard_shortcuts.subtitle.sections": "Navigeren door menu's",
|
||||
"page.keyboard_shortcuts.subtitle.items": "Navigeren door artikelen",
|
||||
"page.keyboard_shortcuts.subtitle.pages": "Navigeren door pagina's",
|
||||
"page.keyboard_shortcuts.subtitle.actions": "Acties",
|
||||
"page.keyboard_shortcuts.go_to_unread": "Ga naar ongelezen",
|
||||
"page.keyboard_shortcuts.go_to_starred": "Ga naar favorieten",
|
||||
"page.keyboard_shortcuts.go_to_history": "Ga naar geschiedenis",
|
||||
"page.keyboard_shortcuts.go_to_feeds": "Ga naar feeds",
|
||||
"page.keyboard_shortcuts.go_to_categories": "Ga naar categorieën",
|
||||
"page.keyboard_shortcuts.go_to_settings": "Ga naar instellingen",
|
||||
"page.keyboard_shortcuts.show_keyboard_shortcuts": "Laat sneltoetsen zien",
|
||||
"page.keyboard_shortcuts.go_to_previous_item": "Vorige item",
|
||||
"page.keyboard_shortcuts.go_to_next_item": "Volgende item",
|
||||
"page.keyboard_shortcuts.show_keyboard_shortcuts": "Sneltoetsen tonen",
|
||||
"page.keyboard_shortcuts.go_to_previous_item": "Vorig artikel",
|
||||
"page.keyboard_shortcuts.go_to_next_item": "Volgend artikel",
|
||||
"page.keyboard_shortcuts.go_to_feed": "Ga naar feed",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "Vorige pagina",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "Volgende pagina",
|
||||
"page.keyboard_shortcuts.open_item": "Open geselecteerde link",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "Ga naar het onderste artikel",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "Ga naar het bovenste artikel",
|
||||
"page.keyboard_shortcuts.open_item": "Open geselecteerd artikel",
|
||||
"page.keyboard_shortcuts.open_original": "Open originele link",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "Oorspronkelijke koppeling op huidig tabblad openen",
|
||||
"page.keyboard_shortcuts.open_comments": "Open opmerkingen link",
|
||||
"page.keyboard_shortcuts.open_comments_same_window": "Open de reactiekoppeling op het huidige tabblad",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "Open originele link in huidig tabblad",
|
||||
"page.keyboard_shortcuts.open_comments": "Open reacties",
|
||||
"page.keyboard_shortcuts.open_comments_same_window": "Open reacties in huidig tabblad",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "Markeer gelezen/ongelezen, focus volgende",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Markeer gelezen/ongelezen, focus vorige",
|
||||
"page.keyboard_shortcuts.refresh_all_feeds": "Vernieuw alle feeds in de achtergrond",
|
||||
"page.keyboard_shortcuts.mark_page_as_read": "Markeer deze pagina als gelezen",
|
||||
"page.keyboard_shortcuts.download_content": "Download originele content",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Ster toevoegen/weghalen",
|
||||
"page.keyboard_shortcuts.mark_page_as_read": "Markeer huidige pagina als gelezen",
|
||||
"page.keyboard_shortcuts.download_content": "Download originele inhoud",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Favoriet toevoegen/verwijderen",
|
||||
"page.keyboard_shortcuts.save_article": "Artikel opslaan",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Scroll artikel naar boven",
|
||||
"page.keyboard_shortcuts.remove_feed": "Verwijder deze feed",
|
||||
"page.keyboard_shortcuts.go_to_search": "Focus instellen op zoekformulier",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
|
||||
"page.keyboard_shortcuts.close_modal": "Sluit dialoogscherm",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Bijlagen van artikel openen/sluiten",
|
||||
"page.keyboard_shortcuts.close_modal": "Dialoogvenster sluiten",
|
||||
"page.users.title": "Gebruikers",
|
||||
"page.users.username": "Gebruikersnaam",
|
||||
"page.users.never_logged": "Nooit",
|
||||
@@ -202,27 +208,28 @@
|
||||
"page.users.admin.no": "Nee",
|
||||
"page.users.actions": "Acties",
|
||||
"page.users.last_login": "Laatste login",
|
||||
"page.users.is_admin": "Administrator",
|
||||
"page.users.is_admin": "Beheerder",
|
||||
"page.settings.title": "Instellingen",
|
||||
"page.settings.link_google_account": "Koppel mijn Google-account",
|
||||
"page.settings.unlink_google_account": "Ontkoppel mijn Google-account",
|
||||
"page.settings.link_oidc_account": "Koppel mijn OpenID Connect-account",
|
||||
"page.settings.unlink_oidc_account": "Ontkoppel mijn OpenID Connect-account",
|
||||
"page.settings.link_oidc_account": "Koppel mijn %s account",
|
||||
"page.settings.unlink_oidc_account": "Ontkoppel mijn %s account",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
"page.settings.webauthn.added_on": "Added On",
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.register": "Wachtwoord registreren",
|
||||
"page.settings.webauthn.register.error": "Kan wachtwoord niet registreren",
|
||||
"page.settings.webauthn.actions": "Acties",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Naam",
|
||||
"page.settings.webauthn.added_on": "Toegevoegd op",
|
||||
"page.settings.webauthn.last_seen_on": "Laatst gebruikt",
|
||||
"page.settings.webauthn.register": "Passkey registreren",
|
||||
"page.settings.webauthn.register.error": "Kan passkey niet registreren",
|
||||
"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.settings.webauthn.delete": [
|
||||
"Verwijder %d wachtwoord",
|
||||
"Verwijder %d wachtwoordsleutels"
|
||||
"Verwijder %d passkey",
|
||||
"Verwijder %d passkeys"
|
||||
],
|
||||
"page.login.oidc_signin": "Inloggen via OpenID Connect",
|
||||
"page.login.webauthn_login": "Inloggen met wachtwoord",
|
||||
"page.login.webauthn_login.error": "Kan niet inloggen met wachtwoord",
|
||||
"page.login.google_signin": "Inloggen via Google",
|
||||
"page.login.oidc_signin": "Inloggen met %s",
|
||||
"page.login.webauthn_login": "Inloggen met passkey",
|
||||
"page.login.webauthn_login.error": "Kan niet inloggen met passkey",
|
||||
"page.login.google_signin": "Inloggen met Google",
|
||||
"page.integrations.title": "Integraties",
|
||||
"page.integration.miniflux_api": "Miniflux API",
|
||||
"page.integration.miniflux_api_endpoint": "API-URL",
|
||||
@@ -232,7 +239,7 @@
|
||||
"page.integration.bookmarklet": "Bookmarklet",
|
||||
"page.integration.bookmarklet.name": "Toevoegen aan Miniflux",
|
||||
"page.integration.bookmarklet.instructions": "Sleep deze link naar je bookmarks.",
|
||||
"page.integration.bookmarklet.help": "Gebruik deze link als bookmark in je browser om je direct te abboneren op een website.",
|
||||
"page.integration.bookmarklet.help": "Gebruik deze link als bookmark in je browser om je direct te abonneren op een website.",
|
||||
"page.sessions.title": "Sessies",
|
||||
"page.sessions.table.date": "Datum",
|
||||
"page.sessions.table.ip": "IP-adres",
|
||||
@@ -240,8 +247,8 @@
|
||||
"page.sessions.table.actions": "Acties",
|
||||
"page.sessions.table.current_session": "Huidige sessie",
|
||||
"page.api_keys.title": "API-sleutels",
|
||||
"page.api_keys.table.description": "Beschrijving",
|
||||
"page.api_keys.table.token": "Blijk",
|
||||
"page.api_keys.table.description": "Omschrijving",
|
||||
"page.api_keys.table.token": "Token",
|
||||
"page.api_keys.table.last_used_at": "Laatst gebruikt",
|
||||
"page.api_keys.table.created_at": "Aanmaakdatum",
|
||||
"page.api_keys.table.actions": "Acties",
|
||||
@@ -250,132 +257,163 @@
|
||||
"page.offline.title": "Offline modus",
|
||||
"page.offline.message": "Je bent offline",
|
||||
"page.offline.refresh_page": "Probeer de pagina te vernieuwen",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"alert.no_shared_entry": "Er is geen gedeelde toegang.",
|
||||
"alert.no_bookmark": "Er zijn op dit moment geen favorieten.",
|
||||
"page.webauthn_rename.title": "Hernoem Passkey",
|
||||
"alert.no_shared_entry": "Er is geen gedeeld artikel.",
|
||||
"alert.no_bookmark": "Er zijn geen favorieten.",
|
||||
"alert.no_category": "Er zijn geen categorieën.",
|
||||
"alert.no_category_entry": "Deze categorie bevat geen feeds.",
|
||||
"alert.no_category_entry": "Er zijn geen artikelen in deze categorie.",
|
||||
"alert.no_tag_entry": "Er zijn geen artikelen die overeenkomen met deze tag.",
|
||||
"alert.no_feed_entry": "Er zijn geen artikelen in deze feed.",
|
||||
"alert.no_feed": "Je hebt nog geen feeds geabboneerd staan.",
|
||||
"alert.no_feed_in_category": "Er is geen abonnement voor deze categorie.",
|
||||
"alert.no_feed": "Je hebt nog geen feed geabonneerd.",
|
||||
"alert.no_feed_in_category": "Er is geen feed voor deze categorie.",
|
||||
"alert.no_history": "Geschiedenis is op dit moment leeg.",
|
||||
"alert.feed_error": "Er is een probleem met deze feed",
|
||||
"alert.no_search_result": "Er is geen resultaat voor deze zoekopdracht.",
|
||||
"alert.no_unread_entry": "Er zijn geen ongelezen artikelen.",
|
||||
"alert.no_user": "Je bent de enige gebruiker.",
|
||||
"alert.account_unlinked": "Uw externe account is nu gedissocieerd!",
|
||||
"alert.account_linked": "Uw externe account is nu gekoppeld!",
|
||||
"alert.pocket_linked": "Uw Pocket-account is nu gekoppeld!",
|
||||
"alert.account_unlinked": "Jouw externe account is nu ontkoppeld!",
|
||||
"alert.account_linked": "Jouw externe account is nu gekoppeld!",
|
||||
"alert.pocket_linked": "Jouw Pocket-account is nu gekoppeld!",
|
||||
"alert.prefs_saved": "Instellingen opgeslagen!",
|
||||
"error.unlink_account_without_password": "U moet een wachtwoord definiëren anders kunt u zich niet opnieuw aanmelden.",
|
||||
"error.unlink_account_without_password": "Je moet een wachtwoord opgeven anders kun je niet meer inloggen.",
|
||||
"error.duplicate_linked_account": "Er is al iemand geregistreerd met deze provider!",
|
||||
"error.duplicate_fever_username": "Er is al iemand met dezelfde Fever gebruikersnaam!",
|
||||
"error.duplicate_googlereader_username": "Er is al iemand met dezelfde Google Reader gebruikersnaam!",
|
||||
"error.pocket_request_token": "Kon geen aanvraagtoken ophalen van Pocket!",
|
||||
"error.pocket_access_token": "Kon geen toegangstoken ophalen van Pocket!",
|
||||
"error.category_already_exists": "Deze categorie bestaat al.",
|
||||
"error.unable_to_create_category": "Kan deze categorie niet maken.",
|
||||
"error.unable_to_update_category": "Kon categorie niet updaten.",
|
||||
"error.unable_to_create_category": "Kan deze categorie niet aanmaken.",
|
||||
"error.unable_to_update_category": "Kan categorie niet bijwerken.",
|
||||
"error.user_already_exists": "Deze gebruiker bestaat al.",
|
||||
"error.unable_to_create_user": "Kan deze gebruiker niet maken.",
|
||||
"error.unable_to_update_user": "Kan deze gebruiker niet updaten.",
|
||||
"error.unable_to_create_user": "Kan deze gebruiker niet aanmaken.",
|
||||
"error.unable_to_update_user": "Kan deze gebruiker niet bijwerken.",
|
||||
"error.unable_to_update_feed": "Kan deze feed niet bijwerken.",
|
||||
"error.subscription_not_found": "Kon geen feeds vinden.",
|
||||
"error.subscription_not_found": "Kan geen feeds vinden.",
|
||||
"error.empty_file": "Dit bestand is leeg.",
|
||||
"error.bad_credentials": "Onjuiste gebruikersnaam of wachtwoord.",
|
||||
"error.fields_mandatory": "Alle velden moeten ingevuld zijn.",
|
||||
"error.title_required": "Naam van categorie is verplicht.",
|
||||
"error.title_required": "De titel is verplicht.",
|
||||
"error.different_passwords": "Wachtwoorden zijn niet hetzelfde.",
|
||||
"error.password_min_length": "Je moet minstens 6 tekens gebruiken.",
|
||||
"error.settings_mandatory_fields": "Gebruikersnaam, skin, taal en tijdzone zijn verplicht.",
|
||||
"error.password_min_length": "Minimaal 6 tekens gebruiken.",
|
||||
"error.settings_mandatory_fields": "Gebruikersnaam, thema, taal en tijdzone zijn verplichte velden.",
|
||||
"error.settings_reading_speed_is_positive": "De leessnelheden moeten positieve gehele getallen zijn.",
|
||||
"error.entries_per_page_invalid": "Het aantal inzendingen per pagina is niet geldig.",
|
||||
"error.feed_mandatory_fields": "The URL en de categorie zijn verplicht.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Ongeldige blokkeerregel: regel #%d mist een geldige veldnaam (Opties: %s)",
|
||||
"error.settings_block_rule_separator_required": "Ongeldige blokkeerregel: het patroon van regel #%d moet worden gescheiden door een '='",
|
||||
"error.settings_block_rule_regex_required": "Ongeldige blokkeerregel: het patroon van regel #%d is niet opgegeven",
|
||||
"error.settings_block_rule_invalid_regex": "Ongeldige blokkeerregel: het patroon van regel #%d is geen geldige regex",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Ongeldige bewaarregel: regel #%d mist een geldige veldnaam (Options: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Ongeldige bewaarregel: het patroon van regel #%d moet worden gescheiden door een '='",
|
||||
"error.settings_keep_rule_regex_required": "Ongeldige bewaarregel: het patroon van regel #%d is niet opgegeven",
|
||||
"error.settings_keep_rule_invalid_regex": "Ongeldige bewaarregel: het patroon van regel #%d is geen geldige regex",
|
||||
"error.entries_per_page_invalid": "Het aantal artikelen per pagina is niet geldig.",
|
||||
"error.feed_mandatory_fields": "De velden URL en categorie zijn verplicht.",
|
||||
"error.feed_already_exists": "Deze feed bestaat al.",
|
||||
"error.invalid_feed_url": "Ongeldige feed-URL.",
|
||||
"error.invalid_site_url": "Ongeldige site-URL.",
|
||||
"error.feed_url_not_empty": "De feed-URL mag niet leeg zijn.",
|
||||
"error.site_url_not_empty": "De site-URL mag niet leeg zijn.",
|
||||
"error.feed_title_not_empty": "De feedtitel mag niet leeg zijn.",
|
||||
"error.invalid_feed_url": "Ongeldige feed URL.",
|
||||
"error.invalid_site_url": "Ongeldige site URL.",
|
||||
"error.feed_url_not_empty": "De feed URL mag niet leeg zijn.",
|
||||
"error.site_url_not_empty": "De site URL mag niet leeg zijn.",
|
||||
"error.feed_title_not_empty": "De feed titel mag niet leeg zijn.",
|
||||
"error.feed_category_not_found": "Deze categorie bestaat niet of behoort niet tot deze gebruiker.",
|
||||
"error.feed_invalid_blocklist_rule": "De regel voor de blokkeerlijst is ongeldig.",
|
||||
"error.feed_invalid_keeplist_rule": "De regel voor het bewaren van een lijst is ongeldig.",
|
||||
"error.feed_invalid_blocklist_rule": "De blokkeerregel is ongeldig.",
|
||||
"error.feed_invalid_keeplist_rule": "De bewaarregel is ongeldig.",
|
||||
"error.user_mandatory_fields": "Gebruikersnaam is verplicht",
|
||||
"error.api_key_already_exists": "This API Key already exists.",
|
||||
"error.unable_to_create_api_key": "Kan deze API-sleutel niet maken.",
|
||||
"error.api_key_already_exists": "Deze API-sleutel bestaat al.",
|
||||
"error.unable_to_create_api_key": "Kan deze API-sleutel niet aanmaken.",
|
||||
"error.invalid_theme": "Ongeldig thema.",
|
||||
"error.invalid_language": "Ongeldige taal.",
|
||||
"error.invalid_timezone": "Ongeldige tijdzone.",
|
||||
"error.invalid_entry_direction": "Ongeldige sorteervolgorde.",
|
||||
"error.invalid_display_mode": "Ongeldige weergavemodus voor webapp.",
|
||||
"error.invalid_display_mode": "Ongeldige weergavemodus voor de webapp.",
|
||||
"error.invalid_gesture_nav": "Ongeldige gebarennavigatie.",
|
||||
"error.invalid_default_home_page": "Ongeldige standaard homepage!",
|
||||
"form.feed.label.title": "Naam",
|
||||
"error.invalid_default_home_page": "Ongeldige startpagina!",
|
||||
"form.feed.label.title": "Titel",
|
||||
"form.feed.label.site_url": "Website URL",
|
||||
"form.feed.label.feed_url": "Feed URL",
|
||||
"form.feed.label.description": "Omschrijving",
|
||||
"form.feed.label.category": "Categorie",
|
||||
"form.feed.label.crawler": "Download originele content",
|
||||
"form.feed.label.feed_username": "Feed-gebruikersnaam",
|
||||
"form.feed.label.crawler": "Download originele inhoud",
|
||||
"form.feed.label.feed_username": "Feed gebruikersnaam",
|
||||
"form.feed.label.feed_password": "Feed wachtwoord",
|
||||
"form.feed.label.user_agent": "Standaard User Agent overschrijven",
|
||||
"form.feed.label.user_agent": "Standaard User-agent overschrijven",
|
||||
"form.feed.label.cookie": "Cookies instellen",
|
||||
"form.feed.label.scraper_rules": "Scraper regels",
|
||||
"form.feed.label.rewrite_rules": "Rewrite regels",
|
||||
"form.feed.label.blocklist_rules": "Blokkeer regels",
|
||||
"form.feed.label.keeplist_rules": "toestemmingsregels",
|
||||
"form.feed.label.urlrewrite_rules": "Regels voor het herschrijven van URL's",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.scraper_rules": "Extractieregels",
|
||||
"form.feed.label.rewrite_rules": "Herschrijfregels",
|
||||
"form.feed.label.blocklist_rules": "Blokkeerregels",
|
||||
"form.feed.label.keeplist_rules": "Bewaarregels",
|
||||
"form.feed.label.urlrewrite_rules": "Herschrijfregels voor URL's",
|
||||
"form.feed.label.apprise_service_urls": "Door komma's gescheiden lijst van Apprise service URL's",
|
||||
"form.feed.label.ignore_http_cache": "Negeer HTTP-cache",
|
||||
"form.feed.label.allow_self_signed_certificates": "Sta zelfondertekende of ongeldige certificaten toe",
|
||||
"form.feed.label.allow_self_signed_certificates": "Zelfondertekende of ongeldige certificaten toestaan",
|
||||
"form.feed.label.disable_http2": "HTTP/2 uitschakelen om fingerprinting te voorkomen",
|
||||
"form.feed.label.fetch_via_proxy": "Ophalen via proxy",
|
||||
"form.feed.label.disabled": "Vernieuw deze feed niet",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.hide_globally": "Verberg items in de globale ongelezen lijst",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
"form.feed.fieldset.integration": "Third-Party Services",
|
||||
"form.category.label.title": "Naam",
|
||||
"form.category.hide_globally": "Verberg items in de globale ongelezen lijst",
|
||||
"form.feed.label.disabled": "Deze feed niet vernieuwen",
|
||||
"form.feed.label.no_media_player": "Geen mediaspeler (audio/video)",
|
||||
"form.feed.label.hide_globally": "Verberg artikelen in de globale ongelezen lijst",
|
||||
"form.feed.label.ntfy_activate": "Artikelen naar ntfy sturen",
|
||||
"form.feed.label.ntfy_priority": "Ntfy prioriteit",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy maximale prioriteit",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy hoge prioriteit",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy standaard prioriteit",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy lage prioriteit",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy minimale prioriteit",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.feed.fieldset.general": "Algemeen",
|
||||
"form.feed.fieldset.rules": "Regels",
|
||||
"form.feed.fieldset.network_settings": "Netwerk Instellingen",
|
||||
"form.feed.fieldset.integration": "Diensten van derden",
|
||||
"form.category.label.title": "Titel",
|
||||
"form.category.hide_globally": "Verberg artikelen in de globale ongelezen lijst",
|
||||
"form.user.label.username": "Gebruikersnaam",
|
||||
"form.user.label.password": "Wachtwoord",
|
||||
"form.user.label.confirmation": "Bevestig wachtwoord",
|
||||
"form.user.label.admin": "Administrator",
|
||||
"form.user.label.admin": "Beheerder",
|
||||
"form.prefs.label.language": "Taal",
|
||||
"form.prefs.label.timezone": "Tijdzone",
|
||||
"form.prefs.label.theme": "Skin",
|
||||
"form.prefs.label.entry_sorting": "Volgorde van items",
|
||||
"form.prefs.label.entries_per_page": "Inzendingen per pagina",
|
||||
"form.prefs.label.theme": "Thema",
|
||||
"form.prefs.label.entry_sorting": "Volgorde van artikelen",
|
||||
"form.prefs.label.entries_per_page": "Artikelen per pagina",
|
||||
"form.prefs.label.default_reading_speed": "Leessnelheid voor andere talen (woorden per minuut)",
|
||||
"form.prefs.label.cjk_reading_speed": "Leessnelheid voor Chinees, Koreaans en Japans (tekens per minuut)",
|
||||
"form.prefs.label.display_mode": "Weergavemodus Progressive Web App (PWA).",
|
||||
"form.prefs.select.older_first": "Oudere items eerst",
|
||||
"form.prefs.select.recent_first": "Recente items eerst",
|
||||
"form.prefs.select.older_first": "Oudere artikelen eerst",
|
||||
"form.prefs.select.recent_first": "Recente artikelen eerst",
|
||||
"form.prefs.select.fullscreen": "Volledig scherm",
|
||||
"form.prefs.select.standalone": "Standalone",
|
||||
"form.prefs.select.minimal_ui": "Minimaal",
|
||||
"form.prefs.select.browser": "Browser",
|
||||
"form.prefs.select.publish_time": "Tijd van binnenkomst",
|
||||
"form.prefs.select.created_time": "Tijdstip van binnenkomst",
|
||||
"form.prefs.select.publish_time": "Tijdstip van publiceren artikel",
|
||||
"form.prefs.select.created_time": "Tijdstip van aanmaken artikel",
|
||||
"form.prefs.select.alphabetical": "Alfabetisch",
|
||||
"form.prefs.select.unread_count": "Ongelezen tellen",
|
||||
"form.prefs.select.unread_count": "Aantal ongelezen artikelen",
|
||||
"form.prefs.select.none": "Geen",
|
||||
"form.prefs.select.tap": "Dubbeltik",
|
||||
"form.prefs.select.swipe": "Vegen",
|
||||
"form.prefs.label.keyboard_shortcuts": "Schakel sneltoetsen in",
|
||||
"form.prefs.label.entry_swipe": "Invoervegen inschakelen op aanraakschermen",
|
||||
"form.prefs.label.gesture_nav": "Gebaar om tussen ingangen te navigeren",
|
||||
"form.prefs.label.show_reading_time": "Toon geschatte leestijd voor artikelen",
|
||||
"form.prefs.label.keyboard_shortcuts": "Sneltoetsen inschakelen",
|
||||
"form.prefs.label.entry_swipe": "Vegen tussen artikelen inschakelen op aanraakschermen",
|
||||
"form.prefs.label.gesture_nav": "Gebaar om tussen artikelen te navigeren",
|
||||
"form.prefs.label.show_reading_time": "Toon geschatte leestijd van artikelen",
|
||||
"form.prefs.label.custom_css": "Aangepaste CSS",
|
||||
"form.prefs.label.entry_order": "Ingang Sorteerkolom",
|
||||
"form.prefs.label.default_home_page": "Standaard startpagina",
|
||||
"form.prefs.label.categories_sorting_order": "Categorieën sorteren",
|
||||
"form.prefs.label.mark_read_on_view": "Items automatisch markeren als gelezen wanneer ze worden bekeken",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.label.custom_js": "Aangepaste JavaScript",
|
||||
"form.prefs.label.entry_order": "Artikelen sorteren",
|
||||
"form.prefs.label.default_home_page": "Startpagina",
|
||||
"form.prefs.label.categories_sorting_order": "Volgorde categorieën",
|
||||
"form.prefs.label.mark_read_on_view": "Markeer artikelen automatisch als gelezen wanneer ze worden bekeken",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Markeer artikelen als gelezen wanneer ze worden bekeken. Voor audio/video, markeer als gelezen bij 90%% voltooiing",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Markeer artikelen alleen als gelezen wanneer het afspelen van audio/video 90%% heeft bereikt",
|
||||
"form.prefs.label.mark_read_manually": "Markeer artikelen handmatig als gelezen",
|
||||
"form.prefs.fieldset.application_settings": "Applicatie Instellingen",
|
||||
"form.prefs.fieldset.authentication_settings": "Authenticatie Instellingen",
|
||||
"form.prefs.fieldset.reader_settings": "Lees Instellingen",
|
||||
"form.prefs.fieldset.global_feed_settings": "Globale Feed Instellingen",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"form.import.label.file": "OPML-bestand",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.betula_activate": "Artikelen opslaan in Betula",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.fever_activate": "Activeer Fever API",
|
||||
"form.integration.fever_username": "Fever gebruikersnaam",
|
||||
"form.integration.fever_password": "Fever wachtwoord",
|
||||
@@ -383,86 +421,102 @@
|
||||
"form.integration.googlereader_activate": "Activeer Google Reader API",
|
||||
"form.integration.googlereader_username": "Google Reader gebruikersnaam",
|
||||
"form.integration.googlereader_password": "Google Reader wachtwoord",
|
||||
"form.integration.googlereader_endpoint": "Google Reader URL:",
|
||||
"form.integration.pinboard_activate": "Artikelen opslaan naar Pinboard",
|
||||
"form.integration.googlereader_endpoint": "Google Reader API-endpoint:",
|
||||
"form.integration.pinboard_activate": "Artikelen opslaan in Pinboard",
|
||||
"form.integration.pinboard_token": "Pinboard API token",
|
||||
"form.integration.pinboard_tags": "Pinboard tags",
|
||||
"form.integration.pinboard_bookmark": "Markeer bookmark als gelezen",
|
||||
"form.integration.instapaper_activate": "Artikelen opstaan naar Instapaper",
|
||||
"form.integration.pinboard_bookmark": "Markeer favoriet als ongelezen",
|
||||
"form.integration.instapaper_activate": "Artikelen opslaan in Instapaper",
|
||||
"form.integration.instapaper_username": "Instapaper gebruikersnaam",
|
||||
"form.integration.instapaper_password": "Instapaper wachtwoord",
|
||||
"form.integration.pocket_activate": "Bewaar artikelen in Pocket",
|
||||
"form.integration.pocket_activate": "Artikelen opslaan in Pocket",
|
||||
"form.integration.pocket_consumer_key": "Pocket Consumer Key",
|
||||
"form.integration.pocket_access_token": "Pocket Access Token",
|
||||
"form.integration.pocket_connect_link": "Verbind je Pocket-account",
|
||||
"form.integration.wallabag_activate": "Opslaan naar Wallabag",
|
||||
"form.integration.wallabag_activate": "Artikelen opslaan in Wallabag",
|
||||
"form.integration.wallabag_only_url": "Alleen URL verzenden (in plaats van volledige inhoud)",
|
||||
"form.integration.wallabag_endpoint": "Wallabag URL",
|
||||
"form.integration.wallabag_client_id": "Wallabag Client-ID",
|
||||
"form.integration.wallabag_client_secret": "Wallabag Client-Secret",
|
||||
"form.integration.wallabag_username": "Wallabag gebruikersnaam",
|
||||
"form.integration.wallabag_password": "Wallabag wachtwoord",
|
||||
"form.integration.notion_activate": "Save entries to Notion",
|
||||
"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.apprise_activate": "Push entries to Apprise",
|
||||
"form.integration.apprise_activate": "Artikelen opslaan in Apprise",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
|
||||
"form.integration.nunux_keeper_activate": "Opslaan naar Nunux Keeper",
|
||||
"form.integration.apprise_services_url": "Door komma's gescheiden lijst van Apprise service URL's",
|
||||
"form.integration.nunux_keeper_activate": "Artikelen opslaan in Nunux Keeper",
|
||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper URL",
|
||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper API-sleutel",
|
||||
"form.integration.omnivore_activate": "Opslaan naar Omnivore",
|
||||
"form.integration.omnivore_activate": "Artikelen opslaan in Omnivore",
|
||||
"form.integration.omnivore_url": "Omnivore URL",
|
||||
"form.integration.omnivore_api_key": "Omnivore API-sleutel",
|
||||
"form.integration.espial_activate": "Opslaan naar Espial",
|
||||
"form.integration.espial_activate": "Artikelen opslaan in Espial",
|
||||
"form.integration.espial_endpoint": "Espial URL",
|
||||
"form.integration.espial_api_key": "Espial API-sleutel",
|
||||
"form.integration.espial_tags": "Espial tags",
|
||||
"form.integration.readwise_activate": "Save entries to Readwise Reader",
|
||||
"form.integration.readwise_activate": "Artikelen opslaan in Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
"form.integration.telegram_bot_activate": "Push nieuwe artikelen naar Telegram-chat",
|
||||
"form.integration.readwise_api_key_link": "Readwise Access Token ophalen",
|
||||
"form.integration.telegram_bot_activate": "Stuur nieuwe artikelen naar Telegram",
|
||||
"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_disable_web_page_preview": "Disable web page preview",
|
||||
"form.integration.telegram_bot_disable_notification": "Disable notification",
|
||||
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Webpaginavoorbeeld uitschakelen",
|
||||
"form.integration.telegram_bot_disable_notification": "Notificatie uitschakelen",
|
||||
"form.integration.telegram_bot_disable_buttons": "Knoppen uitschakelen",
|
||||
"form.integration.linkace_activate": "Artikelen opslaan in LinkAce",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_tags": "LinkAce Tags",
|
||||
"form.integration.linkace_is_private": "Mark link as private",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
"form.integration.linkding_activate": "Opslaan naar Linkding",
|
||||
"form.integration.linkace_api_key": "LinkAce API-sleutel",
|
||||
"form.integration.linkace_tags": "LinkAce tags",
|
||||
"form.integration.linkace_is_private": "Koppeling als privé markeren",
|
||||
"form.integration.linkace_check_disabled": "Koppelingcontrole uitschakelen",
|
||||
"form.integration.linkding_activate": "Artikelen opslaan in Linkding",
|
||||
"form.integration.linkding_endpoint": "Linkding URL",
|
||||
"form.integration.linkding_api_key": "Linkding API-sleutel",
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkding_bookmark": "Markeer bookmark als gelezen",
|
||||
"form.integration.linkwarden_activate": "Opslaan naar Linkwarden",
|
||||
"form.integration.linkding_tags": "Linkding tags",
|
||||
"form.integration.linkding_bookmark": "Markeer favoriet als ongelezen",
|
||||
"form.integration.linkwarden_activate": "Artikelen opslaan in Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden URL",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API-sleutel",
|
||||
"form.integration.matrix_bot_activate": "Nieuwe artikelen overbrengen naar Matrix",
|
||||
"form.integration.matrix_bot_user": "Gebruikersnaam voor Matrix",
|
||||
"form.integration.matrix_bot_activate": "Nieuwe artikelen opslaan in Matrix",
|
||||
"form.integration.matrix_bot_user": "Matrix gebruikersnaam",
|
||||
"form.integration.matrix_bot_password": "Wachtwoord voor Matrix-gebruiker",
|
||||
"form.integration.matrix_bot_url": "URL van de Matrix-server",
|
||||
"form.integration.matrix_bot_chat_id": "ID van Matrix-kamer",
|
||||
"form.integration.shiori_activate": "Opslaan naar Shiori",
|
||||
"form.integration.raindrop_activate": "Artikelen opslaan in Raindrop",
|
||||
"form.integration.raindrop_token": "Raindrop Token",
|
||||
"form.integration.raindrop_collection_id": "Collectie ID",
|
||||
"form.integration.raindrop_tags": "Tags (commagescheiden)",
|
||||
"form.integration.readeck_activate": "Artikelen opslaan in Readeck",
|
||||
"form.integration.readeck_endpoint": "Readeck URL",
|
||||
"form.integration.readeck_api_key": "Readeck API-sleutel",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "Alleen URL verzenden (in plaats van volledige inhoud)",
|
||||
"form.integration.shiori_activate": "Artikelen opslaan in Shiori",
|
||||
"form.integration.shiori_endpoint": "Shiori URL",
|
||||
"form.integration.shiori_username": "Shiori gebruikersnaam",
|
||||
"form.integration.shiori_password": "Shiori wachtwoord",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_activate": "Artikelen opslaan in Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_activate": "Webhook activeren",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_activate": "Controleer RSS-Bridge bij het toevoegen van abonnementen",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.api_key.label.description": "API-sleutellabel",
|
||||
"form.integration.ntfy_activate": "Stuur artikelen naar ntfy",
|
||||
"form.integration.ntfy_topic": "Ntfy topic",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optioneel, standaard is ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optioneel)",
|
||||
"form.integration.ntfy_username": "Ntfy gebruikersnaam (optioneel)",
|
||||
"form.integration.ntfy_password": "Ntfy wachtwoord (optioneel)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optioneel)",
|
||||
"form.api_key.label.description": "API-sleutel omschrijving",
|
||||
"form.submit.loading": "Laden...",
|
||||
"form.submit.saving": "Opslaag...",
|
||||
"time_elapsed.not_yet": "in de toekomst",
|
||||
"form.submit.saving": "Opslaan...",
|
||||
"time_elapsed.not_yet": "nog niet",
|
||||
"time_elapsed.yesterday": "gisteren",
|
||||
"time_elapsed.now": "minder dan een minuut geleden",
|
||||
"time_elapsed.minutes": [
|
||||
@@ -490,32 +544,43 @@
|
||||
"%d jaar geleden"
|
||||
],
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again.",
|
||||
"You have triggered too many feed refreshes. Please wait %d minutes before trying again."
|
||||
"Je hebt te veel feed-vernieuwingen getriggered. Wacht aub %d minuut voor opnieuw proberen.",
|
||||
"Je hebt te veel feed-vernieuwingen getriggered. Wacht aub %d minuten voor opnieuw proberen."
|
||||
],
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"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_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||
"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.http_client_error": "HTTP client error: %v.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||
"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_bad_gateway": "The website is not available at the moment due to a bad gateway error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"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_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_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.database_error": "Database error: %v.",
|
||||
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||
"error.duplicated_feed": "This feed already exists.",
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||
"alert.background_feed_refresh": "Alle feeds worden op de achtergrond vernieuwd. Je kunt Miniflux blijven gebruiker terwijl dit proces draait.",
|
||||
"error.http_response_too_large": "De HTTP-respons is te groot. Je kunt de limiet voor de HTTP-responsgrootte verhogen in de globale instellingen (server herstart noodzakelijk)",
|
||||
"error.http_body_read": "Kan de HTTP-body niet lezen: %v.",
|
||||
"error.http_empty_response_body": "De HTTP-respons body is leeg.",
|
||||
"error.http_empty_response": "De HTTP-respons is leeg. Misschien gebruikt deze website een botbeveiligingsmechanisme?",
|
||||
"error.tls_error": "TLS fout: %q. Als je wilt, kun je TLS-verificatie uitschakelen in de feed-instellingen.",
|
||||
"error.network_operation": "Miniflux kan deze website niet bereiken vanwege een netwerkfout: %v.",
|
||||
"error.network_timeout": "Deze website is te traag en de aanvraag gaf timeout: %v",
|
||||
"error.http_client_error": "HTTP-client-fout: %v.",
|
||||
"error.http_not_authorized": "Toegang tot deze website is niet geautoriseerd. Het kan een foute gebruikersnaam of wachtwoord zijn.",
|
||||
"error.http_too_many_requests": "Miniflux heeft te veel aanvragen gegenereerd voor deze website. Probeer het later nog eens of wijzig de applicatieconfiguratie.",
|
||||
"error.http_forbidden": "Toegang tot deze website is verboden. Misschien heeft deze website een botbeveiligingsmechanisme?",
|
||||
"error.http_resource_not_found": "De gevraagde bron is niet gevonden. Controleer de URL.",
|
||||
"error.http_internal_server_error": "De website is momenteel niet beschikbaar vanwege een interne-server-fout. De oorzaak hiervan ligt niet bij Miniflux. Probeer het later nogmaals aub.",
|
||||
"error.http_bad_gateway": "De website is momenteel niet beschikbaar vanwege een slechte-gateway-fout. De oorzaak hiervan ligt niet bij Miniflux. Probeer het later nogmaals aub.",
|
||||
"error.http_service_unavailable": "De website is momenteel niet beschikbaar vanwege een interne-server-fout. De oorzaak hiervan ligt niet bij Miniflux. Probeer het later nogmaals aub.",
|
||||
"error.http_gateway_timeout": "De website is momenteel niet beschikbaar vanwege een timeout bij de gateway. De oorzaak hiervan ligt niet bij Miniflux. Probeer het later nogmaals aub.",
|
||||
"error.http_unexpected_status_code": "De website is momenteel niet beschikbaar vanwege een onverwachte HTTP-statuscode: %d. De oorzaak hiervan ligt niet bij Miniflux. Probeer het later nogmaals aub.",
|
||||
"error.database_error": "Database fout: %v.",
|
||||
"error.category_not_found": "Deze categorie bestaat niet of hoort niet bij deze gebruiker.",
|
||||
"error.duplicated_feed": "Deze feed bestaat al.",
|
||||
"error.unable_to_parse_feed": "Kan deze feed niet verwerken: %v.",
|
||||
"error.feed_not_found": "Deze feed bestaat niet of is niet van deze gebruiker.",
|
||||
"error.unable_to_detect_rssbridge": "Kan feed niet detecteren met RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Feed-formaat kan niet worden gedetecteerd: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Afspeelsnelheid van de audio/video",
|
||||
"error.settings_media_playback_rate_range": "Afspeelsnelheid is buiten bereik",
|
||||
"enclosure_media_controls.seek" : "Vooruit/terug:",
|
||||
"enclosure_media_controls.seek.title" : " Vooruit/terug met %s seconden",
|
||||
"enclosure_media_controls.speed" : "Snelheid:",
|
||||
"enclosure_media_controls.speed.faster" : "Versnel",
|
||||
"enclosure_media_controls.speed.faster.title" : "Versnel met %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Vertraag",
|
||||
"enclosure_media_controls.speed.slower.title" : "Vertraag met %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset snelheid naar 1x"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"skip_to_content": "Skip to content",
|
||||
"confirm.question": "Czy jesteś pewny?",
|
||||
"confirm.question.refresh": "Czy chcesz wymusić odświeżenie?",
|
||||
"skip_to_content": "Przejdź do treści",
|
||||
"confirm.question": "Czy na pewno?",
|
||||
"confirm.question.refresh": "Czy na pewno chcesz wymusić odświeżenie?",
|
||||
"confirm.yes": "tak",
|
||||
"confirm.no": "nie",
|
||||
"confirm.loading": "W toku...",
|
||||
"confirm.loading": "W toku…",
|
||||
"action.subscribe": "Subskrypcja",
|
||||
"action.save": "Zapisz",
|
||||
"action.or": "lub",
|
||||
@@ -20,7 +20,7 @@
|
||||
"tooltip.keyboard_shortcuts": "Skróty klawiszowe: %s",
|
||||
"tooltip.logged_user": "Zalogowany jako %s",
|
||||
"menu.title": "Menu",
|
||||
"menu.home_page": "Home page",
|
||||
"menu.home_page": "Strona główna",
|
||||
"menu.unread": "Nieprzeczytane",
|
||||
"menu.starred": "Ulubione",
|
||||
"menu.history": "Historia",
|
||||
@@ -32,128 +32,139 @@
|
||||
"menu.integrations": "Usługi",
|
||||
"menu.sessions": "Sesje",
|
||||
"menu.users": "Użytkownicy",
|
||||
"menu.about": "O stronie",
|
||||
"menu.about": "O czytniku",
|
||||
"menu.export": "Eksportuj",
|
||||
"menu.import": "Importuj",
|
||||
"menu.search": "Szukaj",
|
||||
"menu.create_category": "Utwórz kategorię",
|
||||
"menu.mark_page_as_read": "Oznacz jako przeczytane",
|
||||
"menu.mark_all_as_read": "Oznacz wszystko jako przeczytane",
|
||||
"menu.show_all_entries": "Pokaż wszystkie artykuły",
|
||||
"menu.show_only_unread_entries": "Pokaż tylko nieprzeczytane artykuły",
|
||||
"menu.mark_all_as_read": "Oznacz wszystkie jako przeczytane",
|
||||
"menu.show_all_entries": "Pokaż wszystkie wpisy",
|
||||
"menu.show_only_unread_entries": "Pokaż tylko nieprzeczytane wpisy",
|
||||
"menu.show_only_starred_entries": "Pokaż tylko ulubione wpisy",
|
||||
"menu.refresh_feed": "Odśwież",
|
||||
"menu.refresh_all_feeds": "Odśwież wszystkie subskrypcje w tle",
|
||||
"menu.refresh_all_feeds": "Odśwież w tle wszystkie subskrypcje",
|
||||
"menu.edit_feed": "Edytuj",
|
||||
"menu.edit_category": "Edytuj",
|
||||
"menu.add_feed": "Dodaj subskrypcję",
|
||||
"menu.add_feed": "Dodaj kanał",
|
||||
"menu.add_user": "Dodaj użytkownika",
|
||||
"menu.flush_history": "Usuń historię",
|
||||
"menu.feed_entries": "Artykuły",
|
||||
"menu.feed_entries": "Wpisy",
|
||||
"menu.api_keys": "Klucze API",
|
||||
"menu.create_api_key": "Utwórz nowy klucz API",
|
||||
"menu.shared_entries": "Udostępnione wpisy",
|
||||
"search.label": "Szukaj",
|
||||
"search.placeholder": "Szukaj...",
|
||||
"search.submit": "Search",
|
||||
"pagination.next": "Następny",
|
||||
"pagination.previous": "Poprzedni",
|
||||
"entry.status.unread": "Nieprzeczytane",
|
||||
"entry.status.read": "Przeczytane",
|
||||
"entry.status.toast.unread": "Oznaczone jako nieprzeczytane",
|
||||
"entry.status.toast.read": "Oznaczone jako przeczytane",
|
||||
"entry.status.title": "Zmień status artykułu",
|
||||
"entry.bookmark.toggle.on": "Oznacz gwiazdką",
|
||||
"entry.bookmark.toggle.off": "Usuń gwiazdkę",
|
||||
"entry.bookmark.toast.on": "Oznaczone gwiazdką",
|
||||
"entry.bookmark.toast.off": "Bez gwiazdek",
|
||||
"entry.state.saving": "Zapisywanie...",
|
||||
"entry.state.loading": "Ładowanie...",
|
||||
"search.placeholder": "Szukaj…",
|
||||
"search.submit": "Szukaj",
|
||||
"pagination.last": "Ostatnia",
|
||||
"pagination.next": "Następna",
|
||||
"pagination.first": "Pierwsza",
|
||||
"pagination.previous": "Poprzednia",
|
||||
"entry.status.unread": "Nieprzeczytany",
|
||||
"entry.status.read": "Przeczytany",
|
||||
"entry.status.toast.unread": "Oznaczono jako nieprzeczytany",
|
||||
"entry.status.toast.read": "Oznaczono jako przeczytany",
|
||||
"entry.status.title": "Zmień status wpisu",
|
||||
"entry.bookmark.toggle.on": "Dodaj do ulubionych",
|
||||
"entry.bookmark.toggle.off": "Usuń z ulubionych",
|
||||
"entry.bookmark.toast.on": "Dodano do ulubionych",
|
||||
"entry.bookmark.toast.off": "Usunięto z ulubionych",
|
||||
"entry.state.saving": "Zapisywanie…",
|
||||
"entry.state.loading": "Ładowanie…",
|
||||
"entry.save.label": "Zapisz",
|
||||
"entry.save.title": "Zapisz ten artykuł",
|
||||
"entry.save.title": "Zapisz ten wpis",
|
||||
"entry.save.completed": "Gotowe!",
|
||||
"entry.save.toast.completed": "Artykuł zapisany",
|
||||
"entry.scraper.label": "Ściągnij",
|
||||
"entry.save.toast.completed": "Zapisano wpis",
|
||||
"entry.scraper.label": "Pobierz treść",
|
||||
"entry.scraper.title": "Pobierz oryginalną treść",
|
||||
"entry.scraper.completed": "Gotowe!",
|
||||
"entry.external_link.label": "Link zewnętrzny",
|
||||
"entry.external_link.label": "Łącze zewnętrzne",
|
||||
"entry.comments.label": "Komentarze",
|
||||
"entry.comments.title": "Zobacz komentarze",
|
||||
"entry.share.label": "Podzielić się",
|
||||
"entry.share.title": "Podzielić się ten artykuł",
|
||||
"entry.unshare.label": "Unshare",
|
||||
"entry.shared_entry.title": "Otwórz publiczny link",
|
||||
"entry.shared_entry.label": "Udostępnianie",
|
||||
"entry.share.label": "Udostępnij",
|
||||
"entry.share.title": "Udostępnij ten wpis",
|
||||
"entry.unshare.label": "Cofnij udostępnianie",
|
||||
"entry.shared_entry.title": "Otwórz publiczne łącze",
|
||||
"entry.shared_entry.label": "Udostępnij",
|
||||
"entry.estimated_reading_time": [
|
||||
"%d minuta czytania",
|
||||
"%d minuty czytania",
|
||||
"%d minut czytania"
|
||||
],
|
||||
"entry.tags.label": "Tagi:",
|
||||
"entry.tags.label": "Znaczniki:",
|
||||
"page.shared_entries.title": "Udostępnione wpisy",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries"
|
||||
"%d udostępniony wpis",
|
||||
"%d udostępnione wpisy",
|
||||
"%d udostępnionych wpisów"
|
||||
],
|
||||
"page.unread.title": "Nieprzeczytane",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries"
|
||||
"%d nieprzeczytany wpis",
|
||||
"%d nieprzeczytane wpisy",
|
||||
"%d nieprzeczytanych wpisów"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total"
|
||||
"%d wpis łącznie",
|
||||
"%d wpisy łącznie",
|
||||
"%d wpisów łącznie"
|
||||
],
|
||||
"page.starred.title": "Oznaczone gwiazdką",
|
||||
"page.starred.title": "Ulubione",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries"
|
||||
"%d ulubiony wpis",
|
||||
"%d ulubione wpisy",
|
||||
"%d ulubionych wpisów"
|
||||
],
|
||||
"page.categories.title": "Kategorie",
|
||||
"page.categories.no_feed": "Brak kanałów.",
|
||||
"page.categories.entries": "Artykuły",
|
||||
"page.categories.feeds": "Subskrypcje",
|
||||
"page.categories.entries": "Wpisy",
|
||||
"page.categories.feeds": "Kanały",
|
||||
"page.categories.feed_count": [
|
||||
"Jest %d kanał.",
|
||||
"Są %d kanały.",
|
||||
"Jest %d kanałów."
|
||||
],
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories"
|
||||
"%d kategoria",
|
||||
"%d kategorie",
|
||||
"%d kategorii"
|
||||
],
|
||||
"page.new_category.title": "Nowa kategoria",
|
||||
"page.new_user.title": "Nowy użytkownik",
|
||||
"page.edit_category.title": "Edycja Kategorii: %s",
|
||||
"page.edit_category.title": "Edytuj kategorię: %s",
|
||||
"page.edit_user.title": "Edytuj użytkownika: %s",
|
||||
"page.feeds.title": "Kanały",
|
||||
"page.category_label": "Category: %s",
|
||||
"page.category_label": "Kategoria: %s",
|
||||
"page.feeds.last_check": "Ostatnia aktualizacja:",
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.next_check": "Następna aktualizacja:",
|
||||
"page.feeds.read_counter": "Liczba przeczytanych wpisów",
|
||||
"page.feeds.error_count": [
|
||||
"%d błąd",
|
||||
"%d błąd",
|
||||
"%d błędy",
|
||||
"%d błędów"
|
||||
],
|
||||
"page.history.title": "Historia",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries"
|
||||
"%d przeczytany wpis",
|
||||
"%d przeczytane wpisy",
|
||||
"%d przeczytanych wpisów"
|
||||
],
|
||||
"page.import.title": "Importuj",
|
||||
"page.search.title": "Wyniki wyszukiwania",
|
||||
"page.about.title": "O",
|
||||
"page.about.title": "O stronie",
|
||||
"page.about.credits": "Prawa autorskie",
|
||||
"page.about.version": "Wersja:",
|
||||
"page.about.build_date": "Data opracowania:",
|
||||
"page.about.author": "Autor:",
|
||||
"page.about.license": "Licencja:",
|
||||
"page.about.postgres_version": "Postgres wersja:",
|
||||
"page.about.go_version": "Go wersja:",
|
||||
"page.about.global_config_options": "globalne opcje konfiguracji",
|
||||
"page.about.postgres_version": "Wersja PostgreSQL:",
|
||||
"page.about.go_version": "Wersja Go:",
|
||||
"page.about.global_config_options": "Globalne opcje konfiguracji",
|
||||
"page.add_feed.title": "Nowa subskrypcja",
|
||||
"page.add_feed.no_category": "Nie ma żadnej kategorii. Musisz mieć co najmniej jedną kategorię.",
|
||||
"page.add_feed.label.url": "URL",
|
||||
"page.add_feed.label.url": "Adres URL",
|
||||
"page.add_feed.submit": "Znajdź subskrypcję",
|
||||
"page.add_feed.legend.advanced_options": "Zaawansowane opcje",
|
||||
"page.add_feed.legend.advanced_options": "Opcje zaawansowane",
|
||||
"page.add_feed.choose_feed": "Wybierz subskrypcję",
|
||||
"page.edit_feed.title": "Edytuj kanał: %s",
|
||||
"page.edit_feed.last_check": "Ostatnia aktualizacja:",
|
||||
@@ -164,37 +175,39 @@
|
||||
"page.entry.attachments": "Załączniki",
|
||||
"page.keyboard_shortcuts.title": "Skróty klawiszowe",
|
||||
"page.keyboard_shortcuts.subtitle.sections": "Nawigacja między punktami menu",
|
||||
"page.keyboard_shortcuts.subtitle.items": "Nawigacja między artykułami",
|
||||
"page.keyboard_shortcuts.subtitle.items": "Nawigacja między elementami",
|
||||
"page.keyboard_shortcuts.subtitle.pages": "Nawigacja między stronami",
|
||||
"page.keyboard_shortcuts.subtitle.actions": "Działania",
|
||||
"page.keyboard_shortcuts.go_to_unread": "Przejdź do nieprzeczytanych artykułów",
|
||||
"page.keyboard_shortcuts.go_to_starred": "Przejdź do zakładek",
|
||||
"page.keyboard_shortcuts.go_to_unread": "Przejdź do nieprzeczytanych",
|
||||
"page.keyboard_shortcuts.go_to_starred": "Przejdź do ulubionych",
|
||||
"page.keyboard_shortcuts.go_to_history": "Przejdź do historii",
|
||||
"page.keyboard_shortcuts.go_to_feeds": "Przejdź do kanałów",
|
||||
"page.keyboard_shortcuts.go_to_categories": "Przejdź do kategorii",
|
||||
"page.keyboard_shortcuts.go_to_settings": "Przejdź do ustawień",
|
||||
"page.keyboard_shortcuts.show_keyboard_shortcuts": "Pokaż listę skrótów klawiszowych",
|
||||
"page.keyboard_shortcuts.go_to_previous_item": "Przejdź do poprzedniego artykułu",
|
||||
"page.keyboard_shortcuts.go_to_next_item": "Przejdź do następnego punktu artykułu",
|
||||
"page.keyboard_shortcuts.go_to_previous_item": "Przejdź do poprzedniego elementu",
|
||||
"page.keyboard_shortcuts.go_to_next_item": "Przejdź do następnego elementu",
|
||||
"page.keyboard_shortcuts.go_to_feed": "Przejdź do subskrypcji",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "Przejdź do poprzedniej strony",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "Przejdź do następnej strony",
|
||||
"page.keyboard_shortcuts.open_item": "Otwórz zaznaczony artykuł",
|
||||
"page.keyboard_shortcuts.open_original": "Otwórz oryginalny artykuł",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "Otwórz oryginalny link w bieżącej karcie",
|
||||
"page.keyboard_shortcuts.open_comments": "Otwórz link do komentarzy",
|
||||
"page.keyboard_shortcuts.open_comments_same_window": "Otwórz link do komentarzy w bieżącej karcie",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "Oznacz jako przeczytane/nieprzeczytane, skup się dalej",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Oznacz jako przeczytane/nieprzeczytane, skup poprzednie",
|
||||
"page.keyboard_shortcuts.refresh_all_feeds": "Odśwież wszystkie subskrypcje w tle",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "Przejdź do dolnego elementu",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "Przejdź do górnego elementu",
|
||||
"page.keyboard_shortcuts.open_item": "Otwórz zaznaczony element",
|
||||
"page.keyboard_shortcuts.open_original": "Otwórz oryginalne łącze",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "Otwórz oryginalne łącze w bieżącej karcie",
|
||||
"page.keyboard_shortcuts.open_comments": "Otwórz łącze do komentarzy",
|
||||
"page.keyboard_shortcuts.open_comments_same_window": "Otwórz łącze do komentarzy w bieżącej karcie",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "Przełącz przeczytane/nieprzeczytane, przejdź dalej",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Przełącz przeczytane/nieprzeczytane, przejdź wstecz",
|
||||
"page.keyboard_shortcuts.refresh_all_feeds": "Odśwież w tle wszystkie kanały",
|
||||
"page.keyboard_shortcuts.mark_page_as_read": "Zaznacz aktualną stronę jako przeczytaną",
|
||||
"page.keyboard_shortcuts.download_content": "Pobierz oryginalną zawartość",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Dodaj/usuń zakładki",
|
||||
"page.keyboard_shortcuts.save_article": "Zapisz artykuł",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Przewiń artykuł do góry",
|
||||
"page.keyboard_shortcuts.download_content": "Pobierz oryginalną treść",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Przełącz dodanie do ulubionych",
|
||||
"page.keyboard_shortcuts.save_article": "Zapisz wpis",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Przewiń element do góry",
|
||||
"page.keyboard_shortcuts.remove_feed": "Usuń ten kanał",
|
||||
"page.keyboard_shortcuts.go_to_search": "Ustaw fokus na formularzu wyszukiwania",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Przełącz otwieranie/zamykanie załączników wpisów",
|
||||
"page.keyboard_shortcuts.close_modal": "Zamknij listę skrótów klawiszowych",
|
||||
"page.users.title": "Użytkownicy",
|
||||
"page.users.username": "Nazwa użytkownika",
|
||||
@@ -207,35 +220,36 @@
|
||||
"page.settings.title": "Ustawienia",
|
||||
"page.settings.link_google_account": "Połącz z moim kontem Google",
|
||||
"page.settings.unlink_google_account": "Odłącz moje konto Google",
|
||||
"page.settings.link_oidc_account": "Połącz z moim kontem OpenID Connect",
|
||||
"page.settings.unlink_oidc_account": "Odłącz moje konto OpenID Connect",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
"page.settings.webauthn.added_on": "Added On",
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.link_oidc_account": "Połącz z moim kontem %s",
|
||||
"page.settings.unlink_oidc_account": "Odłącz moje konto %s",
|
||||
"page.settings.webauthn.passkeys": "Klucze dostępu",
|
||||
"page.settings.webauthn.actions": "Działania",
|
||||
"page.settings.webauthn.passkey_name": "Nazwa klucza dostępu",
|
||||
"page.settings.webauthn.added_on": "Dodano",
|
||||
"page.settings.webauthn.last_seen_on": "Ostatnio użyte",
|
||||
"page.settings.webauthn.register": "Zarejestruj klucz dostępu",
|
||||
"page.settings.webauthn.register.error": "Nie można zarejestrować klucza dostępu",
|
||||
"page.login.webauthn_login.help": "Wpisz swoją nazwę użytkownika, jeśli używasz klucza bezpieczeństwa. Nie jest to wymagane, jeśli używasz klucza dostępu (wykrywalnych danych uwierzytelniających).",
|
||||
"page.settings.webauthn.delete": [
|
||||
"Usuń %d klucz dostępu",
|
||||
"Usuń %d klucze dostępu",
|
||||
"Usuń %d klucze dostępu"
|
||||
"Usuń %d kluczy dostępu"
|
||||
],
|
||||
"page.login.title": "Zaloguj się",
|
||||
"page.login.google_signin": "Zaloguj przez Google",
|
||||
"page.login.oidc_signin": "Zaloguj przez OpenID Connect",
|
||||
"page.login.webauthn_login": "Zaloguj się za pomocą hasła",
|
||||
"page.login.google_signin": "Zaloguj się przez Google",
|
||||
"page.login.oidc_signin": "Zaloguj się przez %s",
|
||||
"page.login.webauthn_login": "Zaloguj się przez klucz dostępu",
|
||||
"page.login.webauthn_login.error": "Nie można zalogować się za pomocą klucza dostępu",
|
||||
"page.integrations.title": "Usługi",
|
||||
"page.integration.miniflux_api": "Miniflux API",
|
||||
"page.integration.miniflux_api": "API Miniflux",
|
||||
"page.integration.miniflux_api_endpoint": "Punkt końcowy API",
|
||||
"page.integration.miniflux_api_username": "Nazwa Użytkownika",
|
||||
"page.integration.miniflux_api_username": "Nazwa użytkownika",
|
||||
"page.integration.miniflux_api_password": "Hasło",
|
||||
"page.integration.miniflux_api_password_value": "Hasło konta",
|
||||
"page.integration.bookmarklet": "Bookmarklet",
|
||||
"page.integration.miniflux_api_password_value": "Hasło do konta",
|
||||
"page.integration.bookmarklet": "Skryptozakładka",
|
||||
"page.integration.bookmarklet.name": "Dodaj do Miniflux",
|
||||
"page.integration.bookmarklet.instructions": "Przeciągnij i upuść to łącze do zakładek.",
|
||||
"page.integration.bookmarklet.help": "Ten link umożliwia subskrypcję strony internetowej bezpośrednio za pomocą zakładki w przeglądarce internetowej.",
|
||||
"page.integration.bookmarklet.help": "To łącze umożliwia subskrypcję strony internetowej bezpośrednio za pomocą zakładki w przeglądarce internetowej.",
|
||||
"page.sessions.title": "Sesje",
|
||||
"page.sessions.table.date": "Data",
|
||||
"page.sessions.table.ip": "Adres IP",
|
||||
@@ -244,7 +258,7 @@
|
||||
"page.sessions.table.current_session": "Bieżąca sesja",
|
||||
"page.api_keys.title": "Klucze API",
|
||||
"page.api_keys.table.description": "Opis",
|
||||
"page.api_keys.table.token": "Znak",
|
||||
"page.api_keys.table.token": "Token",
|
||||
"page.api_keys.table.last_used_at": "Ostatnio używane",
|
||||
"page.api_keys.table.created_at": "Data utworzenia",
|
||||
"page.api_keys.table.actions": "Działania",
|
||||
@@ -253,18 +267,19 @@
|
||||
"page.offline.title": "Tryb offline",
|
||||
"page.offline.message": "Jesteś odłączony od sieci",
|
||||
"page.offline.refresh_page": "Spróbuj odświeżyć stronę",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"alert.no_shared_entry": "Brak wspólnego wpisu.",
|
||||
"alert.no_bookmark": "Obecnie nie ma żadnych zakładek.",
|
||||
"alert.no_category": "Nie ma żadnej kategorii!",
|
||||
"alert.no_category_entry": "W tej kategorii nie ma żadnych artykułów",
|
||||
"alert.no_feed_entry": "Nie ma artykułu dla tego kanału.",
|
||||
"page.webauthn_rename.title": "Zmień nazwę klucza dostępu",
|
||||
"alert.no_shared_entry": "Brak udostępnionego wpisu.",
|
||||
"alert.no_bookmark": "Brak ulubionych w tej chwili.",
|
||||
"alert.no_category": "Brak kategorii!",
|
||||
"alert.no_category_entry": "Brak wpisów w tej kategorii",
|
||||
"alert.no_tag_entry": "Brak wpisów pasujących do tego znacznika.",
|
||||
"alert.no_feed_entry": "Brak wpisów tego kanału.",
|
||||
"alert.no_feed": "Nie masz żadnej subskrypcji.",
|
||||
"alert.no_feed_in_category": "Nie ma subskrypcji dla tej kategorii.",
|
||||
"alert.no_feed_in_category": "Nie ma subskrypcji tej kategorii.",
|
||||
"alert.no_history": "Obecnie nie ma żadnej historii.",
|
||||
"alert.feed_error": "Z tym kanałem jest problem",
|
||||
"alert.no_search_result": "Brak wyników dla tego wyszukiwania.",
|
||||
"alert.no_unread_entry": "Nie ma żadnych nieprzeczytanych artykułów.",
|
||||
"alert.no_search_result": "Brak wyników tego wyszukiwania.",
|
||||
"alert.no_unread_entry": "Nie ma żadnych nieprzeczytanych wpisów.",
|
||||
"alert.no_user": "Jesteś jedynym użytkownikiem.",
|
||||
"alert.account_unlinked": "Twoje konto zewnętrzne jest teraz zdysocjowane!",
|
||||
"alert.account_linked": "Twoje konto zewnętrzne jest teraz połączone!",
|
||||
@@ -273,7 +288,7 @@
|
||||
"error.unlink_account_without_password": "Musisz zdefiniować hasło, inaczej nie będziesz mógł się ponownie zalogować.",
|
||||
"error.duplicate_linked_account": "Już ktoś jest powiązany z tym dostawcą!",
|
||||
"error.duplicate_fever_username": "Już ktoś inny używa tej nazwy użytkownika Fever!",
|
||||
"error.duplicate_googlereader_username": "Już ktoś inny używa tej nazwy użytkownika Google Reader!",
|
||||
"error.duplicate_googlereader_username": "Istnieje już ktoś inny z tą samą nazwą użytkownika Google Reader!",
|
||||
"error.pocket_request_token": "Nie można pobrać tokena żądania z Pocket!",
|
||||
"error.pocket_access_token": "Nie można pobrać tokena dostępu z Pocket!",
|
||||
"error.category_already_exists": "Ta kategoria już istnieje.",
|
||||
@@ -283,7 +298,7 @@
|
||||
"error.unable_to_create_user": "Nie można utworzyć tego użytkownika.",
|
||||
"error.unable_to_update_user": "Nie można zaktualizować tego użytkownika.",
|
||||
"error.unable_to_update_feed": "Nie można zaktualizować tego kanału.",
|
||||
"error.subscription_not_found": "Nie znaleziono żadnych subskrypcji.",
|
||||
"error.subscription_not_found": "Nie znaleziono żadnych kanałów.",
|
||||
"error.empty_file": "Ten plik jest pusty.",
|
||||
"error.bad_credentials": "Nieprawidłowa nazwa użytkownika lub hasło.",
|
||||
"error.fields_mandatory": "Wszystkie pola są obowiązkowe.",
|
||||
@@ -291,9 +306,17 @@
|
||||
"error.different_passwords": "Hasła nie są identyczne.",
|
||||
"error.password_min_length": "Musisz użyć co najmniej 6 znaków.",
|
||||
"error.settings_mandatory_fields": "Pola nazwy użytkownika, tematu, języka i strefy czasowej są obowiązkowe.",
|
||||
"error.settings_reading_speed_is_positive": "Prędkości odczytu muszą być dodatnimi liczbami całkowitymi.",
|
||||
"error.settings_reading_speed_is_positive": "Szybkości czytania muszą być dodatnimi liczbami całkowitymi.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Nieprawidłowa reguła blokowania: w regule #%d brakuje prawidłowej nazwy pola (opcje: %s)",
|
||||
"error.settings_block_rule_separator_required": "Nieprawidłowa reguła blokowania: wzór reguły #%d musi być oddzielony znakiem '='",
|
||||
"error.settings_block_rule_regex_required": "Nieprawidłowa reguła blokowania: nie podano wzorca reguły #%d",
|
||||
"error.settings_block_rule_invalid_regex": "Nieprawidłowa reguła blokowania: wzór reguły #%d nie jest prawidłowym wyrażeniem regularnym",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Nieprawidłowa reguła utrzymywania: w regule #%d brakuje prawidłowej nazwy pola (opcje: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Nieprawidłowa reguła utrzymywania: wzór reguły #%d musi być oddzielony znakiem '='",
|
||||
"error.settings_keep_rule_regex_required": "Nieprawidłowa reguła utrzymywania nie podano wzorca reguły #%d",
|
||||
"error.settings_keep_rule_invalid_regex": "Nieprawidłowa reguła utrzymywania: wzór reguły #%d nie jest prawidłowym wyrażeniem regularnym",
|
||||
"error.entries_per_page_invalid": "Liczba wpisów na stronę jest nieprawidłowa.",
|
||||
"error.feed_mandatory_fields": "URL i kategoria są obowiązkowe.",
|
||||
"error.feed_mandatory_fields": "Adres URL i kategoria są obowiązkowe.",
|
||||
"error.feed_already_exists": "Ten kanał już istnieje.",
|
||||
"error.invalid_feed_url": "Nieprawidłowy adres URL kanału.",
|
||||
"error.invalid_site_url": "Nieprawidłowy adres URL witryny.",
|
||||
@@ -304,40 +327,51 @@
|
||||
"error.feed_invalid_blocklist_rule": "Reguła listy zablokowanych jest nieprawidłowa.",
|
||||
"error.feed_invalid_keeplist_rule": "Reguła listy zachowania jest nieprawidłowa.",
|
||||
"error.user_mandatory_fields": "Nazwa użytkownika jest obowiązkowa.",
|
||||
"error.api_key_already_exists": "Deze API-sleutel bestaat al.",
|
||||
"error.api_key_already_exists": "Ten klucz API już istnieje.",
|
||||
"error.unable_to_create_api_key": "Nie można utworzyć tego klucza API.",
|
||||
"error.invalid_theme": "Nieprawidłowy motyw.",
|
||||
"error.invalid_language": "Nieprawidłowy język.",
|
||||
"error.invalid_timezone": "Nieprawidłowa strefa czasowa.",
|
||||
"error.invalid_entry_direction": "Nieprawidłowa kolejność sortowania.",
|
||||
"error.invalid_display_mode": "Nieprawidłowy tryb wyświetlania aplikacji internetowej.",
|
||||
"error.invalid_display_mode": "Nieprawidłowy tryb wyświetlania aplikacji sieciowej.",
|
||||
"error.invalid_gesture_nav": "Nieprawidłowa nawigacja gestami.",
|
||||
"error.invalid_default_home_page": "Nieprawidłowa domyślna strona główna!",
|
||||
"form.feed.label.title": "Tytuł",
|
||||
"form.feed.label.site_url": "URL strony",
|
||||
"form.feed.label.feed_url": "URL kanału",
|
||||
"form.feed.label.site_url": "Adres URL strony",
|
||||
"form.feed.label.feed_url": "Adres URL kanału",
|
||||
"form.feed.label.description": "Opis",
|
||||
"form.feed.label.category": "Kategoria",
|
||||
"form.feed.label.crawler": "Pobierz oryginalną treść",
|
||||
"form.feed.label.feed_username": "Subskrypcję nazwa użytkownika",
|
||||
"form.feed.label.feed_password": "Subskrypcję Hasło",
|
||||
"form.feed.label.feed_username": "Nazwa użytkownika subskrypcji",
|
||||
"form.feed.label.feed_password": "Hasło do subskrypcji",
|
||||
"form.feed.label.user_agent": "Zastąp domyślny agent użytkownika",
|
||||
"form.feed.label.cookie": "Ustawianie ciasteczek",
|
||||
"form.feed.label.scraper_rules": "Zasady ekstrakcji",
|
||||
"form.feed.label.cookie": "Ustaw ciasteczka",
|
||||
"form.feed.label.scraper_rules": "Reguły ekstrakcji",
|
||||
"form.feed.label.rewrite_rules": "Reguły zapisu",
|
||||
"form.feed.label.blocklist_rules": "Zasady blokowania",
|
||||
"form.feed.label.keeplist_rules": "Zasady zezwoleń",
|
||||
"form.feed.label.urlrewrite_rules": "Zasady przepisywania adresów URL",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.ignore_http_cache": "Zignoruj pamięć podręczną HTTP",
|
||||
"form.feed.label.allow_self_signed_certificates": "Zezwalaj na certyfikaty z podpisem własnym lub nieprawidłowe certyfikaty",
|
||||
"form.feed.label.blocklist_rules": "Reguły blokowania",
|
||||
"form.feed.label.keeplist_rules": "Reguły utrzymywania",
|
||||
"form.feed.label.urlrewrite_rules": "Reguły przepisywania adresów URL",
|
||||
"form.feed.label.apprise_service_urls": "Rozdzielana przecinkami lista adresów URL usług Appprise",
|
||||
"form.feed.label.ignore_http_cache": "Zignoruj pamięć podręczną HTTP",
|
||||
"form.feed.label.allow_self_signed_certificates": "Zezwalaj na samopodpisane lub nieprawidłowe certyfikaty",
|
||||
"form.feed.label.disable_http2": "Wyłącz protokół HTTP/2, aby uniknąć identyfikowania",
|
||||
"form.feed.label.fetch_via_proxy": "Pobierz przez proxy",
|
||||
"form.feed.label.disabled": "Nie odświeżaj tego kanału",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.disabled": "Nie aktualizuj tego kanału",
|
||||
"form.feed.label.no_media_player": "Brak odtwarzacza multimedialnego (audio i wideo)",
|
||||
"form.feed.label.hide_globally": "Ukryj wpisy na globalnej liście nieprzeczytanych",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
"form.feed.fieldset.integration": "Third-Party Services",
|
||||
"form.feed.label.ntfy_activate": "Prześlij wpisy do ntfy",
|
||||
"form.feed.label.ntfy_priority": "Priorytet ntfy",
|
||||
"form.feed.label.ntfy_max_priority": "Maksymalny priorytet ntfy",
|
||||
"form.feed.label.ntfy_high_priority": "Wysoki priorytet ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Domyślny priorytet ntfy",
|
||||
"form.feed.label.ntfy_low_priority": "Niski priorytet ntfy",
|
||||
"form.feed.label.ntfy_min_priority": "Minimalny priorytet ntfy",
|
||||
"form.integration.cubox_activate": "Zapisuj wpisy w Cubox",
|
||||
"form.integration.cubox_api_link": "Łącze API Cubox",
|
||||
"form.feed.fieldset.general": "Ogólne",
|
||||
"form.feed.fieldset.rules": "Reguły",
|
||||
"form.feed.fieldset.network_settings": "Ustawienia sieci",
|
||||
"form.feed.fieldset.integration": "Usługi dostawców zewnętrznych",
|
||||
"form.category.label.title": "Tytuł",
|
||||
"form.category.hide_globally": "Ukryj wpisy na globalnej liście nieprzeczytanych",
|
||||
"form.user.label.username": "Nazwa użytkownika",
|
||||
@@ -347,124 +381,151 @@
|
||||
"form.prefs.label.language": "Język",
|
||||
"form.prefs.label.timezone": "Strefa czasowa",
|
||||
"form.prefs.label.theme": "Wygląd",
|
||||
"form.prefs.label.entry_sorting": "Sortowanie artykułów",
|
||||
"form.prefs.label.entries_per_page": "Wpisy na stronie",
|
||||
"form.prefs.label.default_reading_speed": "Tryb wyświetlania Progressive Web App (PWA).",
|
||||
"form.prefs.label.cjk_reading_speed": "Prędkość czytania dla języka chińskiego, koreańskiego i japońskiego (znaki na minutę)",
|
||||
"form.prefs.label.display_mode": "Tryb wyświetlania aplikacji internetowej (wymaga ponownej instalacji)",
|
||||
"form.prefs.label.entry_sorting": "Sortowanie wpisów",
|
||||
"form.prefs.label.entries_per_page": "Wpisy na stronę",
|
||||
"form.prefs.label.default_reading_speed": "Szybkość czytania w innych językach (słowa na minutę)",
|
||||
"form.prefs.label.cjk_reading_speed": "Szybkość czytania w języku chińskim, koreańskim i japońskim (znaki na minutę)",
|
||||
"form.prefs.label.display_mode": "Tryb wyświetlania progresywnej aplikacji sieciowej (PWA)",
|
||||
"form.prefs.select.older_first": "Najstarsze wpisy jako pierwsze",
|
||||
"form.prefs.label.keyboard_shortcuts": "Włącz skróty klawiaturowe",
|
||||
"form.prefs.label.entry_swipe": "Włącz machnięcie wpisu na ekranach dotykowych",
|
||||
"form.prefs.label.gesture_nav": "Gest, aby poruszać się między wpisami",
|
||||
"form.prefs.label.show_reading_time": "Pokaż szacowany czas czytania artykułów",
|
||||
"form.prefs.label.keyboard_shortcuts": "Włącz skróty klawiszowe",
|
||||
"form.prefs.label.entry_swipe": "Włącz przesuwanie wpisów na ekranach dotykowych",
|
||||
"form.prefs.label.gesture_nav": "Gest do poruszania się między wpisami",
|
||||
"form.prefs.label.show_reading_time": "Pokaż szacowany czas czytania wpisów",
|
||||
"form.prefs.select.recent_first": "Najnowsze wpisy jako pierwsze",
|
||||
"form.prefs.select.fullscreen": "Pełny ekran",
|
||||
"form.prefs.select.fullscreen": "Pełnoekranowy",
|
||||
"form.prefs.select.standalone": "Samodzielny",
|
||||
"form.prefs.select.minimal_ui": "Minimalny",
|
||||
"form.prefs.select.browser": "Przeglądarka",
|
||||
"form.prefs.select.browser": "Przeglądarkowy",
|
||||
"form.prefs.select.publish_time": "Czas publikacji wpisu",
|
||||
"form.prefs.select.created_time": "Czas utworzenia wpisu",
|
||||
"form.prefs.select.alphabetical": "Alfabetycznie",
|
||||
"form.prefs.select.unread_count": "Liczba nieprzeczytanych",
|
||||
"form.prefs.select.none": "Nic",
|
||||
"form.prefs.select.tap": "Podwójne wciśnięcie",
|
||||
"form.prefs.select.swipe": "Trzepnąć",
|
||||
"form.prefs.select.none": "Brak",
|
||||
"form.prefs.select.tap": "Podwójne stuknięcie",
|
||||
"form.prefs.select.swipe": "Przesuwanie",
|
||||
"form.prefs.label.custom_css": "Niestandardowy CSS",
|
||||
"form.prefs.label.custom_js": "Niestandardowy JavaScript",
|
||||
"form.prefs.label.entry_order": "Kolumna sortowania wpisów",
|
||||
"form.prefs.label.default_home_page": "Domyślna strona główna",
|
||||
"form.prefs.label.categories_sorting_order": "Sortowanie kategorii",
|
||||
"form.prefs.label.mark_read_on_view": "Automatycznie oznaczaj wpisy jako przeczytane podczas przeglądania",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.label.mark_read_on_view": "Automatycznie oznacz wpisy jako przeczytane podczas przeglądania",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Oznacz wpisy jako przeczytane po wyświetleniu. W przypadku audio i wideo oznacz jako przeczytane po ukończeniu 90%%",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Oznacz jako przeczytane dopiero wtedy, gdy odtwarzanie audio i wideo osiągnie 90%% ukończenia",
|
||||
"form.prefs.label.mark_read_manually": "Oznacz wpisy jako przeczytane ręcznie",
|
||||
"form.prefs.fieldset.application_settings": "Ustawienia aplikacji",
|
||||
"form.prefs.fieldset.authentication_settings": "Ustawienia uwierzytelniania",
|
||||
"form.prefs.fieldset.reader_settings": "Ustawienia czytnika",
|
||||
"form.prefs.fieldset.global_feed_settings": "Globalne ustawienia kanałów",
|
||||
"form.prefs.label.external_font_hosts": "Hosty zewnętrznych czcionek",
|
||||
"form.prefs.help.external_font_hosts": "Lista hostów zewnętrznych czcionek, na które należy zezwolić, rozdzielona spacjami. Na przykład: „fonts.gstatic.com fonts.googleapis.com”.",
|
||||
"error.settings_invalid_domain_list": "Nieprawidłowa lista domen. Podaj listę domen rozdzielonych spacjami.",
|
||||
"form.import.label.file": "Plik OPML",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.fever_activate": "Aktywuj Fever API",
|
||||
"form.import.label.url": "Adres URL",
|
||||
"form.integration.betula_activate": "Zapisuj wpisy w Betula",
|
||||
"form.integration.betula_url": "Adres URL serwera Betula",
|
||||
"form.integration.betula_token": "Token do Betula",
|
||||
"form.integration.fever_activate": "Aktywuj API Fever",
|
||||
"form.integration.fever_username": "Login do Fever",
|
||||
"form.integration.fever_password": "Hasło do Fever",
|
||||
"form.integration.fever_endpoint": "Punkt końcowy API gorączka:",
|
||||
"form.integration.googlereader_activate": "Aktywuj Google Reader API",
|
||||
"form.integration.fever_endpoint": "Punkt końcowy API Fever:",
|
||||
"form.integration.googlereader_activate": "Aktywuj API Google Reader",
|
||||
"form.integration.googlereader_username": "Login do Google Reader",
|
||||
"form.integration.googlereader_password": "Hasło do Google Reader",
|
||||
"form.integration.googlereader_endpoint": "Punkt końcowy API gorączka:",
|
||||
"form.integration.pinboard_activate": "Zapisz artykuł w Pinboard",
|
||||
"form.integration.pinboard_token": "Token Pinboard API",
|
||||
"form.integration.pinboard_tags": "Pinboard Tags",
|
||||
"form.integration.googlereader_endpoint": "Punkt końcowy API Google Reader:",
|
||||
"form.integration.pinboard_activate": "Zapisuj wpisy w Pinboard",
|
||||
"form.integration.pinboard_token": "Token API do Pinboard",
|
||||
"form.integration.pinboard_tags": "Znaczniki Pinboard",
|
||||
"form.integration.pinboard_bookmark": "Zaznacz zakładkę jako nieprzeczytaną",
|
||||
"form.integration.instapaper_activate": "Zapisz artykuł w Instapaper",
|
||||
"form.integration.instapaper_activate": "Zapisuj wpisy w Instapaper",
|
||||
"form.integration.instapaper_username": "Login do Instapaper",
|
||||
"form.integration.instapaper_password": "Hasło do Instapaper",
|
||||
"form.integration.pocket_activate": "Zapisz artykuły w Pocket",
|
||||
"form.integration.pocket_consumer_key": "Pocket Consumer Key",
|
||||
"form.integration.pocket_access_token": "Token dostępu kieszeń",
|
||||
"form.integration.pocket_activate": "Zapisuj wpisy w Pocket",
|
||||
"form.integration.pocket_consumer_key": "Klucz klienta do Pocket",
|
||||
"form.integration.pocket_access_token": "Token dostępu do Pocket",
|
||||
"form.integration.pocket_connect_link": "Połącz swoje konto Pocket",
|
||||
"form.integration.wallabag_activate": "Zapisz artykuły do Wallabag",
|
||||
"form.integration.wallabag_only_url": "Wyślij tylko adres URL (zamiast pełnej treści)",
|
||||
"form.integration.wallabag_endpoint": "Wallabag URL",
|
||||
"form.integration.wallabag_client_id": "Wallabag Client-ID",
|
||||
"form.integration.wallabag_client_secret": "Wallabag Client Secret",
|
||||
"form.integration.wallabag_activate": "Zapisuj wpisy w Wallabag",
|
||||
"form.integration.wallabag_only_url": "Przesyłaj tylko adres URL (zamiast pełnej treści)",
|
||||
"form.integration.wallabag_endpoint": "Punkt końcowy API Wallabag",
|
||||
"form.integration.wallabag_client_id": "Identyfikator klienta Wallabag",
|
||||
"form.integration.wallabag_client_secret": "Tajny klucz klienta Wallabag",
|
||||
"form.integration.wallabag_username": "Login do Wallabag",
|
||||
"form.integration.wallabag_password": "Hasło do Wallabag",
|
||||
"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.apprise_activate": "Push entries to Apprise",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
|
||||
"form.integration.nunux_keeper_activate": "Zapisz artykuly do Nunux Keeper",
|
||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper URL",
|
||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper API key",
|
||||
"form.integration.omnivore_activate": "Zapisz artykuly do Omnivore",
|
||||
"form.integration.omnivore_url": "Omnivore URL",
|
||||
"form.integration.omnivore_api_key": "Omnivore API key",
|
||||
"form.integration.espial_activate": "Zapisz artykuly do Espial",
|
||||
"form.integration.espial_endpoint": "Espial URL",
|
||||
"form.integration.espial_api_key": "Espial API key",
|
||||
"form.integration.espial_tags": "Espial Tags",
|
||||
"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.telegram_bot_activate": "Przesyłaj nowe artykuły do czatu Telegram",
|
||||
"form.integration.telegram_bot_token": "Token bota",
|
||||
"form.integration.notion_activate": "Zapisuj wpisy w Notion",
|
||||
"form.integration.notion_page_id": "Identyfikator strony Notion",
|
||||
"form.integration.notion_token": "Tajny token do Notion",
|
||||
"form.integration.apprise_activate": "Przesyłaj wpisy do Apprise",
|
||||
"form.integration.apprise_url": "Adres URL API Apprise",
|
||||
"form.integration.apprise_services_url": "Oddzielona przecinkami lista adresów URL usługi Apprise",
|
||||
"form.integration.nunux_keeper_activate": "Zapisuj wpisy w Nunux Keeper",
|
||||
"form.integration.nunux_keeper_endpoint": "Punkt końcowy API Nunux Keeper",
|
||||
"form.integration.nunux_keeper_api_key": "Klucz API do Nunux Keeper",
|
||||
"form.integration.omnivore_activate": "Zapisuj wpisy w Omnivore",
|
||||
"form.integration.omnivore_url": "Punkt końcowy API Omnivore",
|
||||
"form.integration.omnivore_api_key": "Klucz API do Omnivore",
|
||||
"form.integration.espial_activate": "Zapisuj wpisy w Espial",
|
||||
"form.integration.espial_endpoint": "Punkt końcowy API Espial",
|
||||
"form.integration.espial_api_key": "Klucz API do Espial",
|
||||
"form.integration.espial_tags": "Znaczniki Espial",
|
||||
"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",
|
||||
"form.integration.telegram_bot_activate": "Przesyłaj nowe wpisy do czatu Telegram",
|
||||
"form.integration.telegram_bot_token": "Token do bota",
|
||||
"form.integration.telegram_chat_id": "Identyfikator czatu",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
|
||||
"form.integration.telegram_bot_disable_notification": "Disable notification",
|
||||
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_tags": "LinkAce Tags",
|
||||
"form.integration.linkace_is_private": "Mark link as private",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
"form.integration.linkding_activate": "Zapisz artykuły do Linkding",
|
||||
"form.integration.linkding_endpoint": "Linkding URL",
|
||||
"form.integration.linkding_api_key": "Linkding API key",
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkding_bookmark": "Zaznacz zakładkę jako nieprzeczytaną",
|
||||
"form.integration.linkwarden_activate": "Zapisz artykuły do Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden URL",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API key",
|
||||
"form.integration.matrix_bot_activate": "Przenieś nowe artykuły do Matrix",
|
||||
"form.integration.matrix_bot_user": "Nazwa użytkownika dla Matrix",
|
||||
"form.integration.matrix_bot_password": "Hasło dla użytkownika Matrix",
|
||||
"form.integration.matrix_bot_url": "URL serwera Matrix",
|
||||
"form.integration.telegram_topic_id": "Identyfikator tematu",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Wyłącz podgląd strony internetowej",
|
||||
"form.integration.telegram_bot_disable_notification": "Wyłącz powiadomienie",
|
||||
"form.integration.telegram_bot_disable_buttons": "Wyłącz przyciski",
|
||||
"form.integration.linkace_activate": "Zapisuj wpisy w LinkAce",
|
||||
"form.integration.linkace_endpoint": "Punkt końcowy API LinkAce",
|
||||
"form.integration.linkace_api_key": "Klucz API do LinkAce",
|
||||
"form.integration.linkace_tags": "Znaczniki LinkAce",
|
||||
"form.integration.linkace_is_private": "Oznacz łącze jako prywatne",
|
||||
"form.integration.linkace_check_disabled": "Wyłącz sprawdzanie łączy",
|
||||
"form.integration.linkding_activate": "Zapisuj wpisy w Linkding",
|
||||
"form.integration.linkding_endpoint": "Punkt końcowy API Linkding",
|
||||
"form.integration.linkding_api_key": "Klucz API do Linkding",
|
||||
"form.integration.linkding_tags": "Znaczniki Linkding",
|
||||
"form.integration.linkding_bookmark": "Oznacz zakładkę jako nieprzeczytaną",
|
||||
"form.integration.linkwarden_activate": "Zapisuj wpisy w Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Punkt końcowy API Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Klucz API do Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Przesyłaj nowe wpisy do Matrix",
|
||||
"form.integration.matrix_bot_user": "Login do Matrix",
|
||||
"form.integration.matrix_bot_password": "Hasło do Matrix",
|
||||
"form.integration.matrix_bot_url": "Adres URL serwera Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Identyfikator pokoju Matrix",
|
||||
"form.integration.shiori_activate": "Zapisz artykuły do Shiori",
|
||||
"form.integration.shiori_endpoint": "Shiori URL",
|
||||
"form.integration.raindrop_activate": "Zapisuj wpisy do Raindrop",
|
||||
"form.integration.raindrop_token": "Token (testowy)",
|
||||
"form.integration.raindrop_collection_id": "Identyfikator kolekcji",
|
||||
"form.integration.raindrop_tags": "Znaczniki (oddzielone przecinkami)",
|
||||
"form.integration.readeck_activate": "Zapisuj wpisy do Readeck",
|
||||
"form.integration.readeck_endpoint": "Punkt końcowy API Readeck",
|
||||
"form.integration.readeck_api_key": "Tajny klucz API Readeck",
|
||||
"form.integration.readeck_labels": "Etykiety Readeck",
|
||||
"form.integration.readeck_only_url": "Wysyłaj tylko adres URL (zamiast pełnej treści)",
|
||||
"form.integration.shiori_activate": "Zapisuj artykuły w Shiori",
|
||||
"form.integration.shiori_endpoint": "Punkt końcowy API Shiori",
|
||||
"form.integration.shiori_username": "Login do Shiori",
|
||||
"form.integration.shiori_password": "Hasło do Shiori",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Zapisuj artykuły w Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Adres URL Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Tajny klucz API do Shaarli",
|
||||
"form.integration.webhook_activate": "Włącz Webhook",
|
||||
"form.integration.webhook_url": "Adres URL Webhook",
|
||||
"form.integration.webhook_secret": "Tajny klucz do Webhook",
|
||||
"form.integration.rssbridge_activate": "Sprawdź RSS-Bridge podczas dodawania subskrypcji",
|
||||
"form.integration.rssbridge_url": "Adres URL serwera RSS-Bridge",
|
||||
"form.integration.ntfy_activate": "Przesyłaj wpisy do ntfy",
|
||||
"form.integration.ntfy_topic": "Temay ntfy",
|
||||
"form.integration.ntfy_url": "Adres URL ntfy (opcjonalny, domyślny to ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Token API ntfy (opcjonalny)",
|
||||
"form.integration.ntfy_username": "Login do ntfy (opcjonalny)",
|
||||
"form.integration.ntfy_password": "Hasło do ntfy (opcjonalne)",
|
||||
"form.integration.ntfy_icon_url": "Adres URL ikony ntfy (opcjonalny)",
|
||||
"form.api_key.label.description": "Etykieta klucza API",
|
||||
"form.submit.loading": "Ładowanie...",
|
||||
"form.submit.saving": "Zapisywanie...",
|
||||
"form.submit.loading": "Ładowanie…",
|
||||
"form.submit.saving": "Zapisywanie…",
|
||||
"time_elapsed.not_yet": "jeszcze nie",
|
||||
"time_elapsed.yesterday": "wczoraj",
|
||||
"time_elapsed.now": "przed chwilą",
|
||||
@@ -485,12 +546,12 @@
|
||||
],
|
||||
"time_elapsed.weeks": [
|
||||
"%d tydzień temu",
|
||||
"%d tygodni temu",
|
||||
"%d tygodnie temu",
|
||||
"%d tygodni temu"
|
||||
],
|
||||
"time_elapsed.months": [
|
||||
"%d miesiąc temu",
|
||||
"%d miesięcy temu",
|
||||
"%d miesiące temu",
|
||||
"%d miesięcy temu"
|
||||
],
|
||||
"time_elapsed.years": [
|
||||
@@ -499,32 +560,44 @@
|
||||
"%d lat temu"
|
||||
],
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again.",
|
||||
"You have triggered too many feed refreshes. Please wait %d minutes before trying again."
|
||||
"Wykonano zbyt wiele odświeżeń kanału. Poczekaj %d minutę przed ponowną próbą.",
|
||||
"Wykonano zbyt wiele odświeżeń kanału. Poczekaj %d minuty przed ponowną próbą.",
|
||||
"Wykonano zbyt wiele odświeżeń kanału. Poczekaj %d minut przed ponowną próbą."
|
||||
],
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"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_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||
"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.http_client_error": "HTTP client error: %v.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||
"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_bad_gateway": "The website is not available at the moment due to a bad gateway error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"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_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_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.database_error": "Database error: %v.",
|
||||
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||
"error.duplicated_feed": "This feed already exists.",
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||
"alert.background_feed_refresh": "Wszystkie kanały są odświeżane w tle. Możesz kontynuować korzystanie z Miniflux podczas trwania tego procesu.",
|
||||
"error.http_response_too_large": "Odpowiedź HTTP jest za duża. Możesz zwiększyć limit rozmiaru odpowiedzi HTTP w ustawieniach globalnych (wymaga ponownego uruchomienia serwera).",
|
||||
"error.http_body_read": "Nie można odczytać treści HTTP: %v.",
|
||||
"error.http_empty_response_body": "Treść odpowiedzi HTTP jest pusta.",
|
||||
"error.http_empty_response": "Odpowiedź HTTP jest pusta. Być może ta witryna korzysta z mechanizmu ochrony przed botami?",
|
||||
"error.tls_error": "Błąd TLS: %q. Jeśli chcesz, możesz wyłączyć weryfikację TLS w ustawieniach kanału.",
|
||||
"error.network_operation": "Miniflux nie może połączyć się z tą witryną z powodu błędu sieci: %v.",
|
||||
"error.network_timeout": "Ta witryna internetowa jest zbyt wolna i upłynął limit czasu żądania: %v",
|
||||
"error.http_client_error": "Błąd klienta HTTP: %v.",
|
||||
"error.http_not_authorized": "Dostęp do tej witryny nie jest autoryzowany. Może to być błędna nazwa użytkownika lub hasło.",
|
||||
"error.http_too_many_requests": "Miniflux wygenerował zbyt wiele żądań do tej witryny. Spróbuj ponownie później lub zmień konfigurację aplikacji.",
|
||||
"error.http_forbidden": "Dostęp do tej strony jest zabroniony. Być może ta strona ma mechanizm zabezpieczający przed botami?",
|
||||
"error.http_resource_not_found": "Nie znaleziono żądanego zasobu. Sprawdź adres URL.",
|
||||
"error.http_internal_server_error": "Strona jest w tej chwili niedostępna z powodu błędu serwera. Problem nie leży po stronie Miniflux. Spróbuj ponownie później.",
|
||||
"error.http_bad_gateway": "Strona jest w tej chwili niedostępna z powodu błędu nieprawidłowej bramy. Problem nie leży po stronie Miniflux. Spróbuj ponownie później.",
|
||||
"error.http_service_unavailable": "Strona jest w tej chwili niedostępna z powodu wewnętrznego błędu serwera. Problem nie leży po stronie Miniflux. Spróbuj ponownie później.",
|
||||
"error.http_gateway_timeout": "Strona internetowa jest w tej chwili niedostępna z powodu błędu przekroczenia limitu czasu bramy. Problem nie leży po stronie Miniflux. Spróbuj ponownie później.",
|
||||
"error.http_unexpected_status_code": "Strona jest w tej chwili niedostępna z powodu nieoczekiwanego kodu stanu HTTP: %d. Problem nie leży po stronie Miniflux. Spróbuj ponownie później.",
|
||||
"error.database_error": "Błąd bazy danych: %v.",
|
||||
"error.category_not_found": "Ta kategoria nie istnieje lub nie należy do tego użytkownika.",
|
||||
"error.duplicated_feed": "Ten kanał już istnieje.",
|
||||
"error.unable_to_parse_feed": "Nie można przeanalizować tego kanału: %v.",
|
||||
"error.feed_not_found": "Ten kanał nie istnieje lub nie należy do tego użytkownika.",
|
||||
"error.unable_to_detect_rssbridge": "Nie można wykryć kanału za pomocą RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Nie można wykryć formatu kanału: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Szybkość odtwarzania audio i wideo",
|
||||
"error.settings_media_playback_rate_range": "Szybkość odtwarzania jest poza zakresem",
|
||||
"enclosure_media_controls.seek" : "Przewiń:",
|
||||
"enclosure_media_controls.seek.title" : "Przewiń o %s sek.",
|
||||
"enclosure_media_controls.speed" : "Szybkość:",
|
||||
"enclosure_media_controls.speed.faster" : "Szybciej",
|
||||
"enclosure_media_controls.speed.faster.title" : "Szybciej o %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Wolniej",
|
||||
"enclosure_media_controls.speed.slower.title" : "Wolniej o %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Przywróć",
|
||||
"enclosure_media_controls.speed.reset.title" : "Przywróć szybkość do 1x"
|
||||
}
|
||||
|
||||
@@ -35,11 +35,13 @@
|
||||
"menu.about": "Sobre",
|
||||
"menu.export": "Exportar",
|
||||
"menu.import": "Importar",
|
||||
"menu.search": "Buscar",
|
||||
"menu.create_category": "Criar uma categoria",
|
||||
"menu.mark_page_as_read": "Marcar essa página como lida",
|
||||
"menu.mark_all_as_read": "Marcar todos como lido",
|
||||
"menu.show_all_entries": "Mostrar todas os itens",
|
||||
"menu.show_only_unread_entries": "Mostrar apenas itens não lidos",
|
||||
"menu.show_only_starred_entries": "Mostrar apenas os favoritos",
|
||||
"menu.refresh_feed": "Atualizar",
|
||||
"menu.refresh_all_feeds": "Atualizar todas as fontes",
|
||||
"menu.edit_feed": "Editar",
|
||||
@@ -54,7 +56,9 @@
|
||||
"search.label": "Buscar",
|
||||
"search.placeholder": "Buscar por...",
|
||||
"search.submit": "Search",
|
||||
"pagination.last": "Last",
|
||||
"pagination.next": "Próximo",
|
||||
"pagination.first": "First",
|
||||
"pagination.previous": "Anterior",
|
||||
"entry.status.unread": "Não lido",
|
||||
"entry.status.read": "Lido",
|
||||
@@ -177,6 +181,8 @@
|
||||
"page.keyboard_shortcuts.go_to_feed": "Ir a fonte",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "Ir a página anterior",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "Ir a página seguinte",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "Ir para o item inferior",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "Ir para o item superior",
|
||||
"page.keyboard_shortcuts.open_item": "Abrir o item selecionado",
|
||||
"page.keyboard_shortcuts.open_original": "Abrir o conteúdo original",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "Abrir o conteúdo original na janela atual",
|
||||
@@ -205,8 +211,8 @@
|
||||
"page.settings.title": "Ajustes",
|
||||
"page.settings.link_google_account": "Vincular minha conta do Google",
|
||||
"page.settings.unlink_google_account": "Desvincular minha conta do Google",
|
||||
"page.settings.link_oidc_account": "Vincular minha conta do OpenID Connect",
|
||||
"page.settings.unlink_oidc_account": "Desvincular minha conta do OpenID Connect",
|
||||
"page.settings.link_oidc_account": "Vincular minha conta do %s",
|
||||
"page.settings.unlink_oidc_account": "Desvincular minha conta do %s",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
@@ -214,13 +220,14 @@
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.register": "Registrar senha",
|
||||
"page.settings.webauthn.register.error": "Não foi possível registrar a senha",
|
||||
"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.settings.webauthn.delete": [
|
||||
"Remover %d senha",
|
||||
"Remover %d senhas"
|
||||
],
|
||||
"page.login.title": "Iniciar Sessão",
|
||||
"page.login.google_signin": "Iniciar Sessão com sua conta do Google",
|
||||
"page.login.oidc_signin": "Iniciar Sessão com sua conta do OpenID Connect",
|
||||
"page.login.oidc_signin": "Iniciar Sessão com sua conta do %s",
|
||||
"page.login.webauthn_login": "Entrar com senha",
|
||||
"page.login.webauthn_login.error": "Não é possível fazer login com senha",
|
||||
"page.integrations.title": "Integrações",
|
||||
@@ -255,6 +262,7 @@
|
||||
"alert.no_bookmark": "Não há favorito neste momento.",
|
||||
"alert.no_category": "Não há categoria.",
|
||||
"alert.no_category_entry": "Não há itens nesta categoria.",
|
||||
"alert.no_tag_entry": "Não há itens que correspondam a esta etiqueta.",
|
||||
"alert.no_feed_entry": "Não há itens nessa fonte.",
|
||||
"alert.no_feed": "Não há inscrições.",
|
||||
"alert.no_feed_in_category": "Não há inscrições nessa categoria.",
|
||||
@@ -289,6 +297,14 @@
|
||||
"error.password_min_length": "A senha deve ter no mínimo 6 caracteres.",
|
||||
"error.settings_mandatory_fields": "Os campos de nome de usuário, tema, idioma e fuso horário são obrigatórios.",
|
||||
"error.settings_reading_speed_is_positive": "As velocidades de leitura devem ser inteiros positivos.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.entries_per_page_invalid": "O número de itens por página é inválido.",
|
||||
"error.feed_mandatory_fields": "O campo de URL e categoria são obrigatórios.",
|
||||
"error.feed_already_exists": "Este feed já existe.",
|
||||
@@ -313,6 +329,7 @@
|
||||
"form.feed.label.title": "Título",
|
||||
"form.feed.label.site_url": "URL do site",
|
||||
"form.feed.label.feed_url": "URL da fonte",
|
||||
"form.feed.label.description": "Descrição",
|
||||
"form.feed.label.category": "Categoria",
|
||||
"form.feed.label.crawler": "Obter conteúdo original",
|
||||
"form.feed.label.feed_username": "Nome de usuário da fonte",
|
||||
@@ -327,10 +344,20 @@
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.ignore_http_cache": "Ignorar cache HTTP",
|
||||
"form.feed.label.allow_self_signed_certificates": "Permitir certificados autoassinados ou inválidos",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.disabled": "Não atualizar esta fonte",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.fetch_via_proxy": "Buscar via proxy",
|
||||
"form.feed.label.hide_globally": "Ocultar entradas na lista global não lida",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
@@ -367,15 +394,26 @@
|
||||
"form.prefs.label.gesture_nav": "Gesto para navegar entre as entradas",
|
||||
"form.prefs.label.show_reading_time": "Mostrar tempo estimado de leitura de artigos",
|
||||
"form.prefs.label.custom_css": "CSS customizado",
|
||||
"form.prefs.label.custom_js": "JavaScript customizado",
|
||||
"form.prefs.label.entry_order": "Coluna de Ordenação de Entrada",
|
||||
"form.prefs.label.default_home_page": "Página inicial predefinida",
|
||||
"form.prefs.label.categories_sorting_order": "Classificação das categorias",
|
||||
"form.prefs.label.mark_read_on_view": "Marcar automaticamente as entradas como lidas quando visualizadas",
|
||||
"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_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"form.import.label.file": "Arquivo OPML",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.fever_activate": "Ativar API do Fever",
|
||||
"form.integration.fever_username": "Nome de usuário do Fever",
|
||||
"form.integration.fever_password": "Senha do Fever",
|
||||
@@ -447,6 +485,15 @@
|
||||
"form.integration.matrix_bot_password": "Palavra-passe para utilizador da Matrix",
|
||||
"form.integration.matrix_bot_url": "URL do servidor Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Identificação da sala Matrix",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.readeck_activate": "Salvar itens no Readeck",
|
||||
"form.integration.readeck_endpoint": "Endpoint de API do Readeck",
|
||||
"form.integration.readeck_api_key": "Chave de API do Readeck",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "Enviar apenas URL (em vez de conteúdo completo)",
|
||||
"form.integration.shiori_activate": "Salvar itens no Shiori",
|
||||
"form.integration.shiori_endpoint": "Endpoint da API do Shiori",
|
||||
"form.integration.shiori_username": "Nome de usuário do Shiori",
|
||||
@@ -459,6 +506,13 @@
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_topic": "Ntfy topic",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.api_key.label.description": "Etiqueta da chave de API",
|
||||
"form.submit.loading": "Carregando...",
|
||||
"form.submit.saving": "Salvando...",
|
||||
@@ -498,7 +552,7 @@
|
||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"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.http_client_error": "HTTP client error: %v.",
|
||||
@@ -517,5 +571,16 @@
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Velocidade de reprodução do áudio/vídeo",
|
||||
"error.settings_media_playback_rate_range": "A velocidade de reprodução está fora do intervalo",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
||||
@@ -35,11 +35,13 @@
|
||||
"menu.about": "О приложении",
|
||||
"menu.export": "Экспорт",
|
||||
"menu.import": "Импорт",
|
||||
"menu.search": "Поиск",
|
||||
"menu.create_category": "Создать категорию",
|
||||
"menu.mark_page_as_read": "Отметить эту страницу прочитанной",
|
||||
"menu.mark_all_as_read": "Отметить всё как прочитанное",
|
||||
"menu.show_all_entries": "Показать все статьи",
|
||||
"menu.show_only_unread_entries": "Показывать только непрочитанные статьи",
|
||||
"menu.show_only_starred_entries": "Показывать только избранные статьи",
|
||||
"menu.refresh_feed": "Обновить",
|
||||
"menu.refresh_all_feeds": "Обновить все подписки в фоне",
|
||||
"menu.edit_feed": "Изменить",
|
||||
@@ -54,7 +56,9 @@
|
||||
"search.label": "Поиск",
|
||||
"search.placeholder": "Поиск…",
|
||||
"search.submit": "Search",
|
||||
"pagination.last": "Last",
|
||||
"pagination.next": "Следующая",
|
||||
"pagination.first": "First",
|
||||
"pagination.previous": "Предыдущая",
|
||||
"entry.status.unread": "Не прочитано",
|
||||
"entry.status.read": "Прочитано",
|
||||
@@ -84,26 +88,31 @@
|
||||
"entry.shared_entry.label": "Поделиться",
|
||||
"entry.estimated_reading_time": [
|
||||
"%d минута чтения",
|
||||
"%d минуты чтения",
|
||||
"%d минут чтения"
|
||||
],
|
||||
"entry.tags.label": "Теги:",
|
||||
"page.shared_entries.title": "Общедоступные статьи",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries",
|
||||
"%d shared entries"
|
||||
],
|
||||
"page.unread.title": "Непрочитанное",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries",
|
||||
"%d unread entries"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total",
|
||||
"%d entries in total"
|
||||
],
|
||||
"page.starred.title": "Избранное",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries",
|
||||
"%d starred entries"
|
||||
],
|
||||
"page.categories.title": "Категории",
|
||||
@@ -117,6 +126,7 @@
|
||||
],
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories",
|
||||
"%d categories"
|
||||
],
|
||||
"page.new_category.title": "Новая категория",
|
||||
@@ -136,6 +146,7 @@
|
||||
"page.history.title": "История",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries",
|
||||
"%d read entries"
|
||||
],
|
||||
"page.import.title": "Импорт",
|
||||
@@ -179,6 +190,8 @@
|
||||
"page.keyboard_shortcuts.go_to_feed": "Перейти к подписке",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "Перейти к предыдущей странице",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "Перейти к следующей странице",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "Перейти к нижнему элементу",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "Перейти к верхнему элементу",
|
||||
"page.keyboard_shortcuts.open_item": "Открыть выбранный элемент",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "Открыть оригинальную ссылку в текущей вкладке",
|
||||
"page.keyboard_shortcuts.open_original": "Открыть оригинальную ссылку",
|
||||
@@ -207,8 +220,8 @@
|
||||
"page.settings.title": "Настройки",
|
||||
"page.settings.link_google_account": "Привязать мой Google аккаунт",
|
||||
"page.settings.unlink_google_account": "Отвязать мой Google аккаунт",
|
||||
"page.settings.link_oidc_account": "Привязать мой OpenID Connect аккаунт",
|
||||
"page.settings.unlink_oidc_account": "Отвязать мой OpenID Connect аккаунт",
|
||||
"page.settings.link_oidc_account": "Привязать мой %s аккаунт",
|
||||
"page.settings.unlink_oidc_account": "Отвязать мой %s аккаунт",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
@@ -216,6 +229,7 @@
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.register": "Зарегистрировать пароль",
|
||||
"page.settings.webauthn.register.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.settings.webauthn.delete": [
|
||||
"Удалить %d пароль",
|
||||
"Удалить %d пароля",
|
||||
@@ -223,7 +237,7 @@
|
||||
],
|
||||
"page.login.title": "Войти",
|
||||
"page.login.google_signin": "Войти с помощью Google",
|
||||
"page.login.oidc_signin": "Войти с помощью OpenID Connect",
|
||||
"page.login.oidc_signin": "Войти с помощью %s",
|
||||
"page.login.webauthn_login": "Войти с паролем",
|
||||
"page.login.webauthn_login.error": "Невозможно войти с паролем",
|
||||
"page.integrations.title": "Интеграции",
|
||||
@@ -258,6 +272,7 @@
|
||||
"alert.no_bookmark": "Избранное отсутствует.",
|
||||
"alert.no_category": "Категории отсутствуют.",
|
||||
"alert.no_category_entry": "В этой категории нет статей.",
|
||||
"alert.no_tag_entry": "Нет записей, соответствующих этому тегу.",
|
||||
"alert.no_feed_entry": "В этой подписке отсутствуют статьи.",
|
||||
"alert.no_feed": "У вас нет ни одной подписки.",
|
||||
"alert.no_feed_in_category": "Для этой категории нет подписки.",
|
||||
@@ -292,6 +307,14 @@
|
||||
"error.password_min_length": "Вы должны использовать минимум 6 символов.",
|
||||
"error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.",
|
||||
"error.settings_reading_speed_is_positive": "Скорость чтения должна быть целым положительным числом.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.entries_per_page_invalid": "Недопустимое значение количества записей на странице.",
|
||||
"error.feed_mandatory_fields": "Ссылка и категория обязательны.",
|
||||
"error.feed_already_exists": "Эта подписка уже существует.",
|
||||
@@ -316,6 +339,7 @@
|
||||
"form.feed.label.title": "Название",
|
||||
"form.feed.label.site_url": "Адрес сайта",
|
||||
"form.feed.label.feed_url": "Адрес подписки",
|
||||
"form.feed.label.description": "Описание",
|
||||
"form.feed.label.category": "Категория",
|
||||
"form.feed.label.crawler": "Извлечь оригинальное содержимое",
|
||||
"form.feed.label.feed_username": "Имя пользователя подписки",
|
||||
@@ -330,10 +354,20 @@
|
||||
"form.feed.label.apprise_service_urls": "Список ссылок сервисов Apprise, разделенный запятой",
|
||||
"form.feed.label.ignore_http_cache": "Игнорировать HTTP кеш",
|
||||
"form.feed.label.allow_self_signed_certificates": "Разрешить самоподписанные или недействительные сертификаты",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.fetch_via_proxy": "Использовать прокси",
|
||||
"form.feed.label.disabled": "Не обновлять эту подписку",
|
||||
"form.feed.label.no_media_player": "Отключить медиаплеер (аудио и видео)",
|
||||
"form.feed.label.hide_globally": "Скрыть записи в глобальном списке непрочитанных",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
@@ -370,15 +404,26 @@
|
||||
"form.prefs.label.gesture_nav": "Жест для перехода между статьями",
|
||||
"form.prefs.label.show_reading_time": "Показать примерное время чтения статей",
|
||||
"form.prefs.label.custom_css": "Пользовательский CSS",
|
||||
"form.prefs.label.custom_js": "Пользовательский JavaScript",
|
||||
"form.prefs.label.entry_order": "Столбец сортировки статей",
|
||||
"form.prefs.label.default_home_page": "Домашняя страница по умолчанию",
|
||||
"form.prefs.label.categories_sorting_order": "Сортировка категорий",
|
||||
"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_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"form.import.label.file": "OPML файл",
|
||||
"form.import.label.url": "Ссылка",
|
||||
"form.integration.betula_activate": "Сохранять статьи в Бетулу",
|
||||
"form.integration.betula_url": "Адрес сервера Бетулы",
|
||||
"form.integration.betula_token": "Токен Бетулы",
|
||||
"form.integration.fever_activate": "Активировать Fever API",
|
||||
"form.integration.fever_username": "Имя пользователя Fever",
|
||||
"form.integration.fever_password": "Пароль Fever",
|
||||
@@ -450,6 +495,15 @@
|
||||
"form.integration.matrix_bot_password": "Пароль пользователя Matrix",
|
||||
"form.integration.matrix_bot_url": "Ссылка на сервер Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID комнаты Matrix",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.readeck_activate": "Сохранять статьи в Readeck",
|
||||
"form.integration.readeck_endpoint": "Конечная точка Readeck API",
|
||||
"form.integration.readeck_api_key": "API-ключ Readeck",
|
||||
"form.integration.readeck_labels": "Теги Readeck",
|
||||
"form.integration.readeck_only_url": "Отправлять только ссылку (без содержимого)",
|
||||
"form.integration.shiori_activate": "Сохранять статьи в Shiori",
|
||||
"form.integration.shiori_endpoint": "Конечная точка Shiori API",
|
||||
"form.integration.shiori_username": "Имя пользователя Shiori",
|
||||
@@ -462,6 +516,13 @@
|
||||
"form.integration.webhook_secret": "Секретный ключ для вебхуков",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_topic": "Ntfy topic",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.api_key.label.description": "Описание API-ключа",
|
||||
"form.submit.loading": "Загрузка…",
|
||||
"form.submit.saving": "Сохранение…",
|
||||
@@ -500,6 +561,7 @@
|
||||
],
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again.",
|
||||
"You have triggered too many feed refreshes. Please wait %d minutes before trying again.",
|
||||
"You have triggered too many feed refreshes. Please wait %d minutes before trying again."
|
||||
],
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
@@ -507,7 +569,7 @@
|
||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"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.http_client_error": "HTTP client error: %v.",
|
||||
@@ -526,5 +588,16 @@
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Скорость воспроизведения аудио/видео",
|
||||
"error.settings_media_playback_rate_range": "Скорость воспроизведения выходит за пределы диапазона",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,8 +19,8 @@
|
||||
"action.home_screen": "Додати до головного екрану",
|
||||
"tooltip.keyboard_shortcuts": "Комбінація клавіш: %s",
|
||||
"tooltip.logged_user": "Здійснено вхід як %s",
|
||||
"menu.title": "Menu",
|
||||
"menu.home_page": "Home page",
|
||||
"menu.title": "Меню",
|
||||
"menu.home_page": "Головна сторінка",
|
||||
"menu.unread": "Непрочитане",
|
||||
"menu.starred": "З зірочкою",
|
||||
"menu.history": "Історія",
|
||||
@@ -35,11 +35,13 @@
|
||||
"menu.about": "Про додаток",
|
||||
"menu.export": "Експорт",
|
||||
"menu.import": "Імпорт",
|
||||
"menu.search": "Пошук",
|
||||
"menu.create_category": "Створити категорію",
|
||||
"menu.mark_page_as_read": "Відмітити цю сторінку як прочитане",
|
||||
"menu.mark_all_as_read": "Відмітити все як прочитане",
|
||||
"menu.show_all_entries": "Показати всі записи",
|
||||
"menu.show_only_unread_entries": "Показати тільки непрочитані записи",
|
||||
"menu.show_only_starred_entries": "Показати тільки записи з зірочкою",
|
||||
"menu.refresh_feed": "Оновити",
|
||||
"menu.refresh_all_feeds": "Оновити всі стрічки у фоновому режимі",
|
||||
"menu.edit_feed": "Редагувати",
|
||||
@@ -53,9 +55,11 @@
|
||||
"menu.shared_entries": "Спільні записи",
|
||||
"search.label": "Пошук",
|
||||
"search.placeholder": "Шукати...",
|
||||
"search.submit": "Search",
|
||||
"pagination.next": "Вперед",
|
||||
"pagination.previous": "Назад",
|
||||
"search.submit": "Знайти",
|
||||
"pagination.last": "Остання",
|
||||
"pagination.next": "Наступна",
|
||||
"pagination.first": "Перша",
|
||||
"pagination.previous": "Попередня",
|
||||
"entry.status.unread": "Непрочитане",
|
||||
"entry.status.read": "Прочитане",
|
||||
"entry.status.toast.unread": "Відмічено непрочитаним",
|
||||
@@ -88,23 +92,27 @@
|
||||
"читати %d хвилин"
|
||||
],
|
||||
"entry.tags.label": "Теги:",
|
||||
"page.shared_entries.title": "Спильні записи",
|
||||
"page.shared_entries.title": "Спільні записи",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries",
|
||||
"%d shared entries"
|
||||
],
|
||||
"page.unread.title": "Непрочитане",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries",
|
||||
"%d unread entries"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total",
|
||||
"%d entries in total"
|
||||
],
|
||||
"page.starred.title": "З зірочкою",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries",
|
||||
"%d starred entries"
|
||||
],
|
||||
"page.categories.title": "Категорії",
|
||||
@@ -118,6 +126,7 @@
|
||||
],
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories",
|
||||
"%d categories"
|
||||
],
|
||||
"page.new_category.title": "Нова категорія",
|
||||
@@ -125,9 +134,9 @@
|
||||
"page.edit_category.title": "Редагування категорії: %s",
|
||||
"page.edit_user.title": "Редагування користувача: %s",
|
||||
"page.feeds.title": "Стрічки",
|
||||
"page.category_label": "Category: %s",
|
||||
"page.category_label": "Категорія: %s",
|
||||
"page.feeds.last_check": "Остання перевірка:",
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.next_check": "Наступна перевірка:",
|
||||
"page.feeds.read_counter": "Кількість прочитаних записів",
|
||||
"page.feeds.error_count": [
|
||||
"%d помилка",
|
||||
@@ -137,6 +146,7 @@
|
||||
"page.history.title": "Історія",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries",
|
||||
"%d read entries"
|
||||
],
|
||||
"page.import.title": "Імпорт",
|
||||
@@ -180,6 +190,8 @@
|
||||
"page.keyboard_shortcuts.go_to_feed": "Перейти до стрічки",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "Перейти до попередньої сторінки",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "Перейти до наступної сторінки",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "Перейти до нижнього пункту",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "Перейти до верхнього пункту",
|
||||
"page.keyboard_shortcuts.open_item": "Відкрити виділений запис",
|
||||
"page.keyboard_shortcuts.open_original": "Відкрити оригінальне посилання",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "Відкрити оригінальне посилання в поточній вкладці",
|
||||
@@ -208,8 +220,8 @@
|
||||
"page.settings.title": "Налаштування ",
|
||||
"page.settings.link_google_account": "Підключити мій обліковий запис Google",
|
||||
"page.settings.unlink_google_account": "Відключити мій обліковий запис Google",
|
||||
"page.settings.link_oidc_account": "Підключити мій обліковий запис OpenID Connect",
|
||||
"page.settings.unlink_oidc_account": "Відключити мій обліковий запис OpenID Connect",
|
||||
"page.settings.link_oidc_account": "Підключити мій обліковий запис %s",
|
||||
"page.settings.unlink_oidc_account": "Відключити мій обліковий запис %s",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
@@ -217,6 +229,7 @@
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.register": "Зареєструвати пароль",
|
||||
"page.settings.webauthn.register.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.settings.webauthn.delete": [
|
||||
"Видалити %d ключ доступу",
|
||||
"Видаліть %d ключа доступу",
|
||||
@@ -224,7 +237,7 @@
|
||||
],
|
||||
"page.login.title": "Вхід",
|
||||
"page.login.google_signin": "Увійти через Google",
|
||||
"page.login.oidc_signin": "Увійти через OpenID Connect",
|
||||
"page.login.oidc_signin": "Увійти через %s",
|
||||
"page.login.webauthn_login": "Увійти за допомогою пароля",
|
||||
"page.login.webauthn_login.error": "Неможливо ввійти за допомогою ключа доступу",
|
||||
"page.integrations.title": "Інтеграції",
|
||||
@@ -259,6 +272,7 @@
|
||||
"alert.no_bookmark": "Наразі закладки відсутні.",
|
||||
"alert.no_category": "Немає категорії.",
|
||||
"alert.no_category_entry": "У цій категорії немає записів.",
|
||||
"alert.no_tag_entry": "Немає записів, що відповідають цьому тегу.",
|
||||
"alert.no_feed_entry": "У цій стрічці немає записів.",
|
||||
"alert.no_feed": "У вас немає підписок.",
|
||||
"alert.no_feed_in_category": "У цій категорії немає підписок.",
|
||||
@@ -300,6 +314,14 @@
|
||||
"error.password_min_length": "Пароль має складати щонайменше 6 символів.",
|
||||
"error.settings_mandatory_fields": "Поля імені, теми, мови та часового поясу є обов’язковими.",
|
||||
"error.settings_reading_speed_is_positive": "Швидкість читання має бути додатнім цілим числом.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.entries_per_page_invalid": "Число записів на сторінку недійсне.",
|
||||
"error.feed_mandatory_fields": "URL та категорія є обов’язковими.",
|
||||
"error.feed_already_exists": "Така стрічка вже існує.",
|
||||
@@ -317,6 +339,7 @@
|
||||
"form.feed.label.title": "Назва",
|
||||
"form.feed.label.site_url": "URL-адреса сайту",
|
||||
"form.feed.label.feed_url": "URL-адреса стрічки",
|
||||
"form.feed.label.description": "Опис",
|
||||
"form.feed.label.category": "Категорія",
|
||||
"form.feed.label.crawler": "Завантажувати оригінальний вміст",
|
||||
"form.feed.label.feed_username": "Ім’я користувача для завантаження",
|
||||
@@ -331,10 +354,20 @@
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.ignore_http_cache": "Ігнорувати кеш HTTP",
|
||||
"form.feed.label.allow_self_signed_certificates": "Дозволити сертифікати з власним підписом або недійсні",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.fetch_via_proxy": "Використати проксі-сервер",
|
||||
"form.feed.label.disabled": "Не оновлювати цю стрічку",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.hide_globally": "Приховати записи в глобальному списку непрочитаного",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.category.label.title": "Назва",
|
||||
"form.category.hide_globally": "Приховати записи в глобальному списку непрочитаного",
|
||||
"form.feed.fieldset.general": "General",
|
||||
@@ -371,15 +404,26 @@
|
||||
"form.prefs.label.gesture_nav": "Жест для переходу між записами",
|
||||
"form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів",
|
||||
"form.prefs.label.custom_css": "Спеціальний CSS",
|
||||
"form.prefs.label.custom_js": "Спеціальний JavaScript",
|
||||
"form.prefs.label.entry_order": "Стовпець сортування записів",
|
||||
"form.prefs.label.default_home_page": "Домашня сторінка за умовчанням",
|
||||
"form.prefs.label.categories_sorting_order": "Сортування за категоріями",
|
||||
"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_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"form.import.label.file": "Файл OPML",
|
||||
"form.import.label.url": "URL-адреса",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.fever_activate": "Увімкнути API Fever",
|
||||
"form.integration.fever_username": "Ім’я користувача Fever",
|
||||
"form.integration.fever_password": "Пароль Fever",
|
||||
@@ -451,6 +495,15 @@
|
||||
"form.integration.matrix_bot_password": "Пароль для користувача Matrix",
|
||||
"form.integration.matrix_bot_url": "URL-адреса сервера Матриці",
|
||||
"form.integration.matrix_bot_chat_id": "Ідентифікатор кімнати Матриці",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.readeck_activate": "Зберігати статті до Readeck",
|
||||
"form.integration.readeck_endpoint": "Readeck API Endpoint",
|
||||
"form.integration.readeck_api_key": "Ключ API Readeck",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "Надіслати лише URL (замість повного вмісту)",
|
||||
"form.integration.shiori_activate": "Save articles to Shiori",
|
||||
"form.integration.shiori_endpoint": "Shiori API Endpoint",
|
||||
"form.integration.shiori_username": "Shiori Username",
|
||||
@@ -463,6 +516,13 @@
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_topic": "Ntfy topic",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.api_key.label.description": "Назва ключа API",
|
||||
"form.submit.loading": "Завантаження...",
|
||||
"form.submit.saving": "Зберігаю...",
|
||||
@@ -501,6 +561,7 @@
|
||||
],
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again.",
|
||||
"You have triggered too many feed refreshes. Please wait %d minutes before trying again.",
|
||||
"You have triggered too many feed refreshes. Please wait %d minutes before trying again."
|
||||
],
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
@@ -508,7 +569,7 @@
|
||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"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.http_client_error": "HTTP client error: %v.",
|
||||
@@ -527,5 +588,16 @@
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Швидкість відтворення аудіо/відео",
|
||||
"error.settings_media_playback_rate_range": "Швидкість відтворення виходить за межі діапазону",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"skip_to_content": "Skip to content",
|
||||
"skip_to_content": "跳转至内容",
|
||||
"confirm.question": "您确认吗?",
|
||||
"confirm.question.refresh": "您是否要强制刷新?",
|
||||
"confirm.yes": "是",
|
||||
@@ -19,8 +19,8 @@
|
||||
"action.home_screen": "添加到主屏幕",
|
||||
"tooltip.keyboard_shortcuts": "快捷键: %s",
|
||||
"tooltip.logged_user": "当前登录 %s",
|
||||
"menu.title": "Menu",
|
||||
"menu.home_page": "Home page",
|
||||
"menu.title": "菜单",
|
||||
"menu.home_page": "首页",
|
||||
"menu.unread": "未读",
|
||||
"menu.starred": "收藏",
|
||||
"menu.history": "历史",
|
||||
@@ -35,11 +35,13 @@
|
||||
"menu.about": "关于",
|
||||
"menu.export": "导出",
|
||||
"menu.import": "导入",
|
||||
"menu.search": "搜索",
|
||||
"menu.create_category": "新建分类",
|
||||
"menu.mark_page_as_read": "标记为已读",
|
||||
"menu.mark_all_as_read": "全部标为已读",
|
||||
"menu.show_all_entries": "显示所有文章",
|
||||
"menu.show_only_unread_entries": "仅显示未读文章",
|
||||
"menu.show_only_starred_entries": "仅显示已收藏文章",
|
||||
"menu.refresh_feed": "更新",
|
||||
"menu.refresh_all_feeds": "在后台更新全部源",
|
||||
"menu.edit_feed": "编辑",
|
||||
@@ -53,8 +55,10 @@
|
||||
"menu.shared_entries": "已分享的文章",
|
||||
"search.label": "搜索",
|
||||
"search.placeholder": "搜索…",
|
||||
"search.submit": "Search",
|
||||
"search.submit": "查找",
|
||||
"pagination.last": "最后一页",
|
||||
"pagination.next": "下一页",
|
||||
"pagination.first": "第一页",
|
||||
"pagination.previous": "上一页",
|
||||
"entry.status.unread": "标为未读",
|
||||
"entry.status.read": "标为已读",
|
||||
@@ -83,28 +87,23 @@
|
||||
"entry.shared_entry.title": "打开公共链接",
|
||||
"entry.shared_entry.label": "分享",
|
||||
"entry.estimated_reading_time": [
|
||||
"需要 %d 分钟阅读",
|
||||
"需要 %d 分钟阅读"
|
||||
],
|
||||
"entry.tags.label": "标签:",
|
||||
"page.shared_entries.title": "已分享的文章",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries"
|
||||
"%d 已分享的文章"
|
||||
],
|
||||
"page.unread.title": "未读",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries"
|
||||
"%d 未读的文章"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total"
|
||||
"%d 文章总数"
|
||||
],
|
||||
"page.starred.title": "收藏",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries"
|
||||
"%d 收藏的文章"
|
||||
],
|
||||
"page.categories.title": "分类",
|
||||
"page.categories.no_feed": "没有源",
|
||||
@@ -114,15 +113,14 @@
|
||||
"有 %d 个源"
|
||||
],
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories"
|
||||
"%d 分类"
|
||||
],
|
||||
"page.new_category.title": "新分类",
|
||||
"page.new_user.title": "新用户",
|
||||
"page.edit_category.title": "编辑分类 : %s",
|
||||
"page.edit_user.title": "编辑用户 : %s",
|
||||
"page.feeds.title": "源",
|
||||
"page.category_label": "Category: %s",
|
||||
"page.category_label": "分类: %s",
|
||||
"page.feeds.last_check": "最后检查时间:",
|
||||
"page.feeds.next_check": "下次检查时间:",
|
||||
"page.feeds.read_counter": "已读文章数",
|
||||
@@ -131,8 +129,7 @@
|
||||
],
|
||||
"page.history.title": "历史",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries"
|
||||
"%d 阅读文章"
|
||||
],
|
||||
"page.import.title": "导入",
|
||||
"page.search.title": "搜索结果",
|
||||
@@ -175,6 +172,8 @@
|
||||
"page.keyboard_shortcuts.go_to_feed": "转到源页面",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "上一页",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "下一页",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "转到底部项目",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "转到顶部项目",
|
||||
"page.keyboard_shortcuts.open_item": "打开选定的文章",
|
||||
"page.keyboard_shortcuts.open_original": "打开原始链接",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "在当前标签页中打开原始链接",
|
||||
@@ -203,8 +202,8 @@
|
||||
"page.settings.title": "设置",
|
||||
"page.settings.link_google_account": "关联我的 Google 账户",
|
||||
"page.settings.unlink_google_account": "解除 Google 账号关联",
|
||||
"page.settings.link_oidc_account": "关联我的 OpenID Connect 账户",
|
||||
"page.settings.unlink_oidc_account": "解除 OpenID Connect 账号关联",
|
||||
"page.settings.link_oidc_account": "关联我的 %s 账户",
|
||||
"page.settings.unlink_oidc_account": "解除 %s 账号关联",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "操作",
|
||||
"page.settings.webauthn.passkey_name": "Passkey 名称",
|
||||
@@ -212,13 +211,13 @@
|
||||
"page.settings.webauthn.last_seen_on": "最后使用时间",
|
||||
"page.settings.webauthn.register": "注册 Passkey",
|
||||
"page.settings.webauthn.register.error": "无法注册 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.settings.webauthn.delete": [
|
||||
"删除 %d 个 Passkey",
|
||||
"删除 %d 个 Passkey"
|
||||
],
|
||||
"page.login.title": "登录",
|
||||
"page.login.google_signin": "使用 Google 登录",
|
||||
"page.login.oidc_signin": "使用 OpenID Connect 登录",
|
||||
"page.login.oidc_signin": "使用 %s 登录",
|
||||
"page.login.webauthn_login": "使用密码登录",
|
||||
"page.login.webauthn_login.error": "无法使用密码登录",
|
||||
"page.integrations.title": "集成",
|
||||
@@ -253,6 +252,7 @@
|
||||
"alert.no_bookmark": "目前没有收藏",
|
||||
"alert.no_category": "目前没有分类",
|
||||
"alert.no_category_entry": "该分类下没有文章",
|
||||
"alert.no_tag_entry": "没有与此标签匹配的条目。",
|
||||
"alert.no_feed_entry": "该源中没有文章",
|
||||
"alert.no_feed": "目前没有源",
|
||||
"alert.no_history": "目前没有历史",
|
||||
@@ -295,6 +295,14 @@
|
||||
"error.site_url_not_empty": "源网站的网址不能为空。",
|
||||
"error.feed_title_not_empty": "订阅源的标题不能为空。",
|
||||
"error.settings_reading_speed_is_positive": "阅读速度必须是正整数。",
|
||||
"error.settings_block_rule_fieldname_invalid": "无效的阻止规则: 规则 #%d 缺少合法的字段名 (可选: %s)",
|
||||
"error.settings_block_rule_separator_required": "无效的阻止规则: 规则 #%d 的模式字符必须用‘=’分开。",
|
||||
"error.settings_block_rule_regex_required": "无效的阻止规则: 规则 #%d 的模式字符没有提供。",
|
||||
"error.settings_block_rule_invalid_regex": "无效的阻止规则: 规则 #%d 的模式字符不是合法的正则表达式。",
|
||||
"error.settings_keep_rule_fieldname_invalid": "无效的保留规则: 规则 #%d 缺少合法的字段名 (可选: %s)",
|
||||
"error.settings_keep_rule_separator_required": "无效的保留规则: 规则 #%d 的模式字符必须用‘=’分开。",
|
||||
"error.settings_keep_rule_regex_required": "无效的保留规则: 规则 #%d 的模式字符没有提供。",
|
||||
"error.settings_keep_rule_invalid_regex": "无效的保留规则: 规则 #%d 的模式字符不是合法的正则表达式。",
|
||||
"error.feed_category_not_found": "此类别不存在或不属于该用户。",
|
||||
"error.feed_invalid_blocklist_rule": "阻止列表规则无效。",
|
||||
"error.feed_invalid_keeplist_rule": "保留列表规则无效。",
|
||||
@@ -311,6 +319,7 @@
|
||||
"form.feed.label.title": "标题",
|
||||
"form.feed.label.site_url": "源网站 URL",
|
||||
"form.feed.label.feed_url": "订阅源 URL",
|
||||
"form.feed.label.description": "描述",
|
||||
"form.feed.label.category": "类别",
|
||||
"form.feed.label.crawler": "抓取全文内容",
|
||||
"form.feed.label.feed_username": "源用户名",
|
||||
@@ -325,10 +334,20 @@
|
||||
"form.feed.label.apprise_service_urls": "使用逗号分隔的 Apprise 服务 URL 列表",
|
||||
"form.feed.label.ignore_http_cache": "忽略 HTTP 缓存",
|
||||
"form.feed.label.allow_self_signed_certificates": "允许自签名证书或无效证书",
|
||||
"form.feed.label.disable_http2": "关闭 HTTP/2 避免记录指纹",
|
||||
"form.feed.label.fetch_via_proxy": "通过代理获取",
|
||||
"form.feed.label.disabled": "请勿刷新此源",
|
||||
"form.feed.label.no_media_player": "没有媒体播放器(音频/视频)",
|
||||
"form.feed.label.hide_globally": "隐藏全局未读列表中的文章",
|
||||
"form.feed.label.ntfy_activate": "推送条目到ntfy",
|
||||
"form.feed.label.ntfy_priority": "Ntfy优先级",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy最高优先级",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy高优先级",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy默认优先级",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy低优先级",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy最低优先级",
|
||||
"form.integration.cubox_activate": "保存文章到 Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API 链接",
|
||||
"form.feed.fieldset.general": "通用",
|
||||
"form.feed.fieldset.rules": "规则",
|
||||
"form.feed.fieldset.network_settings": "网络设置",
|
||||
@@ -365,15 +384,26 @@
|
||||
"form.prefs.label.gesture_nav": "在条目之间导航的手势",
|
||||
"form.prefs.label.show_reading_time": "显示文章的预计阅读时间",
|
||||
"form.prefs.label.custom_css": "自定义 CSS",
|
||||
"form.prefs.label.custom_js": "自定义 JavaScript",
|
||||
"form.prefs.label.entry_order": "文章排序依据",
|
||||
"form.prefs.label.default_home_page": "默认主页",
|
||||
"form.prefs.label.categories_sorting_order": "分类排序",
|
||||
"form.prefs.label.mark_read_on_view": "查看时自动将条目标记为已读",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "当浏览时标记条目为已读。对于音频/视频,当播放完成90%%时标记为已读",
|
||||
"form.prefs.label.mark_read_on_media_completion": "仅当音频/视频播放完成90%%时标记为已读",
|
||||
"form.prefs.label.mark_read_manually": "手动标记条目为已读",
|
||||
"form.prefs.fieldset.application_settings": "应用设置",
|
||||
"form.prefs.fieldset.authentication_settings": "用户认证设置",
|
||||
"form.prefs.fieldset.reader_settings": "阅读器设置",
|
||||
"form.prefs.fieldset.global_feed_settings": "全局订阅源设置",
|
||||
"form.prefs.label.external_font_hosts": "外部字体托管",
|
||||
"form.prefs.help.external_font_hosts": "允许外部字体托管的空格分隔列表。例如:\"fonts.gstatic.com fonts.googleapis.com\"。",
|
||||
"error.settings_invalid_domain_list": "域名列表无效。请提供一个用空格分隔的域名列表。",
|
||||
"form.import.label.file": "OPML 文件",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.betula_activate": "保存文章到 Betula",
|
||||
"form.integration.betula_url": "Betula 服务地址",
|
||||
"form.integration.betula_token": "Betula 密钥",
|
||||
"form.integration.fever_activate": "启用 Fever API",
|
||||
"form.integration.fever_username": "Fever 用户名",
|
||||
"form.integration.fever_password": "Fever 密码",
|
||||
@@ -383,7 +413,7 @@
|
||||
"form.integration.googlereader_password": "Google Reader 密码",
|
||||
"form.integration.googlereader_endpoint": "Google Reader API 端点:",
|
||||
"form.integration.pinboard_activate": "保存文章到 Pinboard",
|
||||
"form.integration.pinboard_token": "Pinboard API Token",
|
||||
"form.integration.pinboard_token": "Pinboard API 密钥",
|
||||
"form.integration.pinboard_tags": "Pinboard 标签",
|
||||
"form.integration.pinboard_bookmark": "标记为未读",
|
||||
"form.integration.instapaper_activate": "保存文章到 Instapaper",
|
||||
@@ -397,12 +427,12 @@
|
||||
"form.integration.wallabag_only_url": "仅发送 URL(而不是完整内容)",
|
||||
"form.integration.wallabag_endpoint": "Wallabag URL",
|
||||
"form.integration.wallabag_client_id": "Wallabag 客户端 ID",
|
||||
"form.integration.wallabag_client_secret": "Wallabag 客户端 Secret",
|
||||
"form.integration.wallabag_client_secret": "Wallabag 客户端 密钥",
|
||||
"form.integration.wallabag_username": "Wallabag 用户名",
|
||||
"form.integration.wallabag_password": "Wallabag 密码",
|
||||
"form.integration.notion_activate": "保存文章到 Notion",
|
||||
"form.integration.notion_page_id": "Notion Page ID",
|
||||
"form.integration.notion_token": "Notion Secret Token",
|
||||
"form.integration.notion_page_id": "Notion 页面ID",
|
||||
"form.integration.notion_token": "Notion 密钥",
|
||||
"form.integration.apprise_activate": "将新文章推送到 Apprise",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.apprise_services_url": "使用逗号分隔的 Apprise 服务 URL 列表",
|
||||
@@ -412,13 +442,13 @@
|
||||
"form.integration.omnivore_activate": "保存文章到 Omnivore",
|
||||
"form.integration.omnivore_url": "Omnivore API 端点",
|
||||
"form.integration.omnivore_api_key": "Omnivore API 密钥",
|
||||
"form.integration.espial_activate": "保存文章到 Espial",
|
||||
"form.integration.espial_endpoint": "Espial API 端点",
|
||||
"form.integration.espial_api_key": "Espial API 密钥",
|
||||
"form.integration.espial_activate": "保存文章到 Espial",
|
||||
"form.integration.espial_endpoint": "Espial API 端点",
|
||||
"form.integration.espial_api_key": "Espial API 密钥",
|
||||
"form.integration.espial_tags": "Espial 标签",
|
||||
"form.integration.readwise_activate": "保存文章到 Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "获取你的 Readwise Access Token",
|
||||
"form.integration.readwise_api_key": "Readwise Reader 密钥",
|
||||
"form.integration.readwise_api_key_link": "获取你的 Readwise 密钥",
|
||||
"form.integration.telegram_bot_activate": "将新文章推送到 Telegram",
|
||||
"form.integration.telegram_bot_token": "机器人令牌",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
@@ -426,12 +456,12 @@
|
||||
"form.integration.telegram_bot_disable_notification": "禁用通知",
|
||||
"form.integration.telegram_bot_disable_buttons": "不展示按钮",
|
||||
"form.integration.telegram_chat_id": "聊天ID",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_tags": "LinkAce Tags",
|
||||
"form.integration.linkace_is_private": "Mark link as private",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
"form.integration.linkace_activate": "保存文章到 LinkAce",
|
||||
"form.integration.linkace_endpoint": "LinkAce API URL",
|
||||
"form.integration.linkace_api_key": "LinkAce API 密钥",
|
||||
"form.integration.linkace_tags": "LinkAce 标签",
|
||||
"form.integration.linkace_is_private": "将链接标记为私有",
|
||||
"form.integration.linkace_check_disabled": "关闭链接检查",
|
||||
"form.integration.linkding_activate": "保存文章到 Linkding",
|
||||
"form.integration.linkding_endpoint": "Linkding API 端点",
|
||||
"form.integration.linkding_api_key": "Linkding API 密钥",
|
||||
@@ -445,18 +475,34 @@
|
||||
"form.integration.matrix_bot_password": "Matrix Bot 密码",
|
||||
"form.integration.matrix_bot_url": "Matrix 服务器 URL",
|
||||
"form.integration.matrix_bot_chat_id": "Matrix 聊天 ID",
|
||||
"form.integration.raindrop_activate": "保存文章到 Raindrop",
|
||||
"form.integration.raindrop_token": "(Test) 密钥",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.readeck_activate": "保存文章到 Readeck",
|
||||
"form.integration.readeck_endpoint": "Readeck API 端点",
|
||||
"form.integration.readeck_api_key": "Readeck API 密钥",
|
||||
"form.integration.readeck_labels": "Readeck 默认标签",
|
||||
"form.integration.readeck_only_url": "仅发送 URL(而不是完整内容)",
|
||||
"form.integration.shiori_activate": "保存文章到 Shiori",
|
||||
"form.integration.shiori_endpoint": "Shiori API 端点",
|
||||
"form.integration.shiori_username": "Shiori 用户名",
|
||||
"form.integration.shiori_password": "Shiori 密码",
|
||||
"form.integration.shaarli_activate": "保存文章到 Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API 密钥",
|
||||
"form.integration.webhook_activate": "启用 Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.integration.webhook_secret": "Webhook 密钥",
|
||||
"form.integration.rssbridge_activate": "添加订阅时检查 RSS-Bridge",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge 服务器 URL",
|
||||
"form.integration.ntfy_activate": "推送条目到ntfy",
|
||||
"form.integration.ntfy_topic": "Ntfy主题",
|
||||
"form.integration.ntfy_url": "Ntfy URL(可选,默认为ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Ntfy API令牌(可选)",
|
||||
"form.integration.ntfy_username": "Ntfy用户名(可选)",
|
||||
"form.integration.ntfy_password": "Ntfy密码(可选)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy图标URL(可选)",
|
||||
"form.api_key.label.description": "API密钥标签",
|
||||
"form.submit.loading": "载入中…",
|
||||
"form.submit.saving": "保存中…",
|
||||
@@ -482,32 +528,42 @@
|
||||
"%d 年前"
|
||||
],
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again.",
|
||||
"You have triggered too many feed refreshes. Please wait %d minutes before trying again."
|
||||
"多次触发订阅源更新,请等待 %d 分钟后重试。"
|
||||
],
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"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_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||
"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.http_client_error": "HTTP client error: %v.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||
"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_bad_gateway": "The website is not available at the moment due to a bad gateway error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"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_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_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.database_error": "Database error: %v.",
|
||||
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||
"error.duplicated_feed": "This feed already exists.",
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||
"alert.background_feed_refresh": "所有的订阅源都在后台刷新中。您可以继续使用Miniflux,同时此过程正在运行。",
|
||||
"error.http_response_too_large": "HTTP响应内容过大,您可以在全局设置中增加HTTP响应大小限制(需要服务器重新启动)。",
|
||||
"error.http_body_read": "无法读取HTTP主体: %v。",
|
||||
"error.http_empty_response_body": "HTTP响应主体为空。",
|
||||
"error.http_empty_response": "HTTP响应内容为空,该网站可能正在使用机器人保护机制。",
|
||||
"error.tls_error": "TLS 错误: %q。如果您愿意的话可以在订阅源设置里关闭TLS验证。",
|
||||
"error.network_operation": "Miniflux无法访问该网站由于网络错误: %v。",
|
||||
"error.network_timeout": "该网站响应过慢,请求超时: %v",
|
||||
"error.http_client_error": "HTTP 客户端错误r: %v。",
|
||||
"error.http_not_authorized": "该网站访问未授权,可能用户名和密码错误。",
|
||||
"error.http_too_many_requests": "Miniflux 对该网站请求过多次数,请稍后重试或修改应用配置项。",
|
||||
"error.http_forbidden": "该网站被禁止访问,网站可能有机器人保护机制?",
|
||||
"error.http_resource_not_found": "请求资源无法找到,请检查URL。",
|
||||
"error.http_internal_server_error": "当前由于服务器错误导致该网站无法访问,问题不在Miniflux,请稍后重试。",
|
||||
"error.http_bad_gateway": "当前由于错误的网关导致该网站无法访问,问题不在Miniflux,请稍后重试。",
|
||||
"error.http_service_unavailable": "当前由于服务器内部错误导致该网站无法访问,问题不在Miniflux,请稍后重试。",
|
||||
"error.http_gateway_timeout": "当前由于网关超时导致该网站无法访问,问题不在Miniflux,请稍后重试。",
|
||||
"error.http_unexpected_status_code": "当前由于意外的HTTP状态码:%d 导致该网站无法访问,问题不在Miniflux,请稍后重试。",
|
||||
"error.database_error": "数据库错误: %v。",
|
||||
"error.category_not_found": "该分类不存在或不属于该用户。",
|
||||
"error.duplicated_feed": "该订阅源已经存在。",
|
||||
"error.unable_to_parse_feed": "无法解析该订阅源: %v。",
|
||||
"error.feed_not_found": "该订阅源不存在或不属于该用户。",
|
||||
"error.unable_to_detect_rssbridge": "无法使用RSS-Bridge去检测订阅源: %v。",
|
||||
"error.feed_format_not_detected": "无法解析订阅源格式: %v。",
|
||||
"form.prefs.label.media_playback_rate": "音频/视频的播放速度",
|
||||
"error.settings_media_playback_rate_range": "播放速度超出范围",
|
||||
"enclosure_media_controls.seek" : "查找:",
|
||||
"enclosure_media_controls.seek.title" : "查找 %s 秒",
|
||||
"enclosure_media_controls.speed" : "速度:",
|
||||
"enclosure_media_controls.speed.faster" : "快进",
|
||||
"enclosure_media_controls.speed.faster.title" : "速度快进到 %sx",
|
||||
"enclosure_media_controls.speed.slower" : "减慢",
|
||||
"enclosure_media_controls.speed.slower.title" : "速度减慢到 %sx",
|
||||
"enclosure_media_controls.speed.reset" : "重置",
|
||||
"enclosure_media_controls.speed.reset.title" : "重置速度到 1x"
|
||||
}
|
||||
|
||||
@@ -35,11 +35,13 @@
|
||||
"menu.about": "關於",
|
||||
"menu.export": "匯出",
|
||||
"menu.import": "匯入",
|
||||
"menu.search": "搜尋",
|
||||
"menu.create_category": "新建分類",
|
||||
"menu.mark_page_as_read": "將此頁面標記為已讀",
|
||||
"menu.mark_all_as_read": "全部標為已讀",
|
||||
"menu.show_all_entries": "顯示所有文章",
|
||||
"menu.show_only_unread_entries": "僅顯示未讀文章",
|
||||
"menu.show_only_starred_entries": "Show only starred entries",
|
||||
"menu.refresh_feed": "更新",
|
||||
"menu.refresh_all_feeds": "背景更新全部Feeds",
|
||||
"menu.edit_feed": "編輯",
|
||||
@@ -54,7 +56,9 @@
|
||||
"search.label": "搜尋",
|
||||
"search.placeholder": "搜尋…",
|
||||
"search.submit": "送出",
|
||||
"pagination.last": "Last",
|
||||
"pagination.next": "下一頁",
|
||||
"pagination.first": "First",
|
||||
"pagination.previous": "上一頁",
|
||||
"entry.status.unread": "標為未讀",
|
||||
"entry.status.read": "標為已讀",
|
||||
@@ -83,40 +87,33 @@
|
||||
"entry.shared_entry.title": "開啟公共連結",
|
||||
"entry.shared_entry.label": "分享",
|
||||
"entry.estimated_reading_time": [
|
||||
"需要 %d 分鐘閱讀",
|
||||
"需要 %d 分鐘閱讀"
|
||||
],
|
||||
"entry.tags.label": "標籤:",
|
||||
"page.shared_entries.title": "已分享的文章",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries"
|
||||
"%d shared entry"
|
||||
],
|
||||
"page.unread.title": "未讀",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries"
|
||||
"%d unread entry"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total"
|
||||
"%d entry in total"
|
||||
],
|
||||
"page.starred.title": "收藏",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries"
|
||||
"%d starred entry"
|
||||
],
|
||||
"page.categories.title": "分類",
|
||||
"page.categories.no_feed": "沒有Feed",
|
||||
"page.categories.entries": "檢視內容",
|
||||
"page.categories.feeds": "檢視Feeds",
|
||||
"page.categories.feed_count": [
|
||||
"有 %d 個Feed",
|
||||
"有 %d 個Feeds"
|
||||
"有 %d 個Feed"
|
||||
],
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories"
|
||||
"%d category"
|
||||
],
|
||||
"page.new_category.title": "新分類",
|
||||
"page.new_user.title": "新使用者",
|
||||
@@ -128,13 +125,11 @@
|
||||
"page.feeds.next_check": "下次檢查時間:",
|
||||
"page.feeds.read_counter": "已讀文章數",
|
||||
"page.feeds.error_count": [
|
||||
"%d 錯誤",
|
||||
"%d 錯誤"
|
||||
],
|
||||
"page.history.title": "歷史",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries"
|
||||
"%d read entry"
|
||||
],
|
||||
"page.import.title": "匯入",
|
||||
"page.search.title": "搜尋結果",
|
||||
@@ -177,6 +172,8 @@
|
||||
"page.keyboard_shortcuts.go_to_feed": "轉到Feed頁面",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "上一頁",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "下一頁",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "转到底部项目",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "转到顶部项目",
|
||||
"page.keyboard_shortcuts.open_item": "開啟選定的文章",
|
||||
"page.keyboard_shortcuts.open_original": "開啟原始連結",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "在當前標籤頁中開啟原始連結",
|
||||
@@ -205,8 +202,8 @@
|
||||
"page.settings.title": "設定",
|
||||
"page.settings.link_google_account": "關聯我的 Google 賬戶",
|
||||
"page.settings.unlink_google_account": "解除 Google 帳號關聯",
|
||||
"page.settings.link_oidc_account": "關聯我的 OpenID Connect 賬戶",
|
||||
"page.settings.unlink_oidc_account": "解除 OpenID Connect 帳號關聯",
|
||||
"page.settings.link_oidc_account": "關聯我的 %s 賬戶",
|
||||
"page.settings.unlink_oidc_account": "解除 %s 帳號關聯",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "操作",
|
||||
"page.settings.webauthn.passkey_name": "Passkey 名稱",
|
||||
@@ -214,13 +211,13 @@
|
||||
"page.settings.webauthn.last_seen_on": "最後使用時間",
|
||||
"page.settings.webauthn.register": "註冊 Passkey",
|
||||
"page.settings.webauthn.register.error": "無法註冊 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.settings.webauthn.delete": [
|
||||
"刪除 %d 個 Passkey",
|
||||
"刪除 %d 個 Passkey"
|
||||
],
|
||||
"page.login.title": "登入",
|
||||
"page.login.google_signin": "使用 Google 登入",
|
||||
"page.login.oidc_signin": "使用 OpenID Connect 登入",
|
||||
"page.login.oidc_signin": "使用 %s 登入",
|
||||
"page.login.webauthn_login": "使用密碼登錄",
|
||||
"page.login.webauthn_login.error": "無法使用密碼登錄",
|
||||
"page.integrations.title": "整合",
|
||||
@@ -255,6 +252,7 @@
|
||||
"alert.no_bookmark": "目前沒有收藏",
|
||||
"alert.no_category": "目前沒有分類",
|
||||
"alert.no_category_entry": "該分類下沒有文章",
|
||||
"alert.no_tag_entry": "沒有與此標籤相符的條目。",
|
||||
"alert.no_feed_entry": "該Feed中沒有文章",
|
||||
"alert.no_feed": "目前沒有Feed",
|
||||
"alert.no_history": "目前沒有歷史",
|
||||
@@ -289,6 +287,14 @@
|
||||
"error.password_min_length": "請至少輸入 6 個字元",
|
||||
"error.settings_mandatory_fields": "必須填寫使用者名稱、主題、語言以及時區",
|
||||
"error.settings_reading_speed_is_positive": "閱讀速度必須是正整數。",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.entries_per_page_invalid": "每頁的文章數無效。",
|
||||
"error.feed_mandatory_fields": "必須填寫網址和分類",
|
||||
"error.feed_already_exists": "此Feed已存在。",
|
||||
@@ -313,6 +319,7 @@
|
||||
"form.feed.label.title": "標題",
|
||||
"form.feed.label.site_url": "網站 URL",
|
||||
"form.feed.label.feed_url": "訂閱 Feed URL",
|
||||
"form.feed.label.description": "描述",
|
||||
"form.feed.label.category": "類別",
|
||||
"form.feed.label.crawler": "下載原文內容",
|
||||
"form.feed.label.feed_username": "Feed 使用者名稱",
|
||||
@@ -327,10 +334,20 @@
|
||||
"form.feed.label.apprise_service_urls": "使用逗號分隔的 Apprise 服務 URL 列表",
|
||||
"form.feed.label.ignore_http_cache": "忽略 HTTP 快取",
|
||||
"form.feed.label.allow_self_signed_certificates": "允許自簽章憑證或無效憑證",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.fetch_via_proxy": "透過代理獲取",
|
||||
"form.feed.label.disabled": "請勿更新此 Feed",
|
||||
"form.feed.label.no_media_player": "沒有媒體播放器(音訊/視訊)",
|
||||
"form.feed.label.hide_globally": "隱藏全域性未讀列表中的文章",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.feed.fieldset.general": "通用",
|
||||
"form.feed.fieldset.rules": "規則",
|
||||
"form.feed.fieldset.network_settings": "網路設定",
|
||||
@@ -367,15 +384,26 @@
|
||||
"form.prefs.label.gesture_nav": "在條目之間導航的手勢",
|
||||
"form.prefs.label.show_reading_time": "顯示文章的預計閱讀時間",
|
||||
"form.prefs.label.custom_css": "自定義 CSS",
|
||||
"form.prefs.label.custom_js": "自定義 JavaScript",
|
||||
"form.prefs.label.entry_order": "文章排序依據",
|
||||
"form.prefs.label.default_home_page": "預設主頁",
|
||||
"form.prefs.label.categories_sorting_order": "分類排序",
|
||||
"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_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.fieldset.application_settings": "應用程式設定",
|
||||
"form.prefs.fieldset.authentication_settings": "使用者認證設定",
|
||||
"form.prefs.fieldset.reader_settings": "閱讀器設定",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"form.import.label.file": "OPML 檔案",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.fever_activate": "啟用 Fever API",
|
||||
"form.integration.fever_username": "Fever 使用者名稱",
|
||||
"form.integration.fever_password": "Fever 密碼",
|
||||
@@ -447,6 +475,15 @@
|
||||
"form.integration.matrix_bot_password": "Matrix 的密碼",
|
||||
"form.integration.matrix_bot_url": "Matrix 伺服器的 URL",
|
||||
"form.integration.matrix_bot_chat_id": "Matrix 房間 ID",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.readeck_activate": "儲存文章到 Readeck",
|
||||
"form.integration.readeck_endpoint": "Readeck API 端點",
|
||||
"form.integration.readeck_api_key": "Readeck API 金鑰",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "仅发送 URL(而不是完整内容)",
|
||||
"form.integration.shiori_activate": "儲存文章到 Shiori",
|
||||
"form.integration.shiori_endpoint": "Shiori API 端點",
|
||||
"form.integration.shiori_username": "Shiori 使用者名稱",
|
||||
@@ -459,6 +496,13 @@
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.integration.rssbridge_activate": "新增訂閱時檢查 RSS-Bridge",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge 伺服器的 URL",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_topic": "Ntfy topic",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.api_key.label.description": "API金鑰標籤",
|
||||
"form.submit.loading": "載入中…",
|
||||
"form.submit.saving": "儲存中…",
|
||||
@@ -466,39 +510,32 @@
|
||||
"time_elapsed.yesterday": "昨天",
|
||||
"time_elapsed.now": "剛剛",
|
||||
"time_elapsed.minutes": [
|
||||
"%d 分鐘前",
|
||||
"%d 分鐘前"
|
||||
],
|
||||
"time_elapsed.hours": [
|
||||
"%d 小時前",
|
||||
"%d 小時前"
|
||||
],
|
||||
"time_elapsed.days": [
|
||||
"%d 天前",
|
||||
"%d 天前"
|
||||
],
|
||||
"time_elapsed.weeks": [
|
||||
"%d 周前",
|
||||
"%d 周前"
|
||||
],
|
||||
"time_elapsed.months": [
|
||||
"%d 月前",
|
||||
"%d 月前"
|
||||
],
|
||||
"time_elapsed.years": [
|
||||
"%d 年前",
|
||||
"%d 年前"
|
||||
],
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again.",
|
||||
"You have triggered too many feed refreshes. Please wait %d minutes before trying again."
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again."
|
||||
],
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"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_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"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.http_client_error": "HTTP client error: %v.",
|
||||
@@ -517,5 +554,16 @@
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "音訊/視訊的播放速度",
|
||||
"error.settings_media_playback_rate_range": "播放速度超出範圍",
|
||||
"enclosure_media_controls.seek" : "Seek:",
|
||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
||||
"enclosure_media_controls.speed" : "Speed:",
|
||||
"enclosure_media_controls.speed.faster" : "Faster",
|
||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower" : "Slower",
|
||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset" : "Reset",
|
||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package proxy // import "miniflux.app/v2/internal/proxy"
|
||||
package mediaproxy // import "miniflux.app/v2/internal/mediaproxy"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -29,11 +29,11 @@ func TestProxyFilterWithHttpDefault(t *testing.T) {
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ProxyRewriter(r, input)
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,11 +53,11 @@ func TestProxyFilterWithHttpsDefault(t *testing.T) {
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ProxyRewriter(r, input)
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,11 +76,11 @@ func TestProxyFilterWithHttpNever(t *testing.T) {
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ProxyRewriter(r, input)
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
expected := input
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,11 +99,11 @@ func TestProxyFilterWithHttpsNever(t *testing.T) {
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ProxyRewriter(r, input)
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
expected := input
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,11 +124,11 @@ func TestProxyFilterWithHttpAlways(t *testing.T) {
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ProxyRewriter(r, input)
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,11 +149,98 @@ func TestProxyFilterWithHttpsAlways(t *testing.T) {
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ProxyRewriter(r, input)
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
expected := `<p><img src="/proxy/LdPNR1GBDigeeNp2ArUQRyZsVqT_PWLfHGjYFrrWWIY=/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbsoluteProxyFilterWithHttpsAlways(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
output := RewriteDocumentWithAbsoluteProxyURL(r, input)
|
||||
expected := `<p><img src="http://localhost/proxy/LdPNR1GBDigeeNp2ArUQRyZsVqT_PWLfHGjYFrrWWIY=/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbsoluteProxyFilterWithCustomPortAndSubfolderInBaseURL(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("BASE_URL", "http://example.org:88/folder/")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if config.Opts.BaseURL() != "http://example.org:88/folder" {
|
||||
t.Fatalf(`Unexpected base URL, got "%s"`, config.Opts.BaseURL())
|
||||
}
|
||||
|
||||
if config.Opts.RootURL() != "http://example.org:88" {
|
||||
t.Fatalf(`Unexpected root URL, got "%s"`, config.Opts.RootURL())
|
||||
}
|
||||
|
||||
router := mux.NewRouter()
|
||||
|
||||
if config.Opts.BasePath() != "" {
|
||||
router = router.PathPrefix(config.Opts.BasePath()).Subrouter()
|
||||
}
|
||||
|
||||
router.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
||||
output := RewriteDocumentWithAbsoluteProxyURL(router, input)
|
||||
expected := `<p><img src="http://example.org:88/folder/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbsoluteProxyFilterWithHttpsAlwaysAndAudioTag(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "audio")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<audio src="https://website/folder/audio.mp3"></audio>`
|
||||
output := RewriteDocumentWithAbsoluteProxyURL(r, input)
|
||||
expected := `<audio src="http://localhost/proxy/EmBTvmU5B17wGuONkeknkptYopW_Tl6Y6_W8oYbN_Xs=/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9hdWRpby5tcDM="></audio>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,11 +261,61 @@ func TestProxyFilterWithHttpsAlwaysAndCustomProxyServer(t *testing.T) {
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ProxyRewriter(r, input)
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
expected := `<p><img src="https://proxy-example/proxy/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFilterWithHttpsAlwaysAndIncorrectCustomProxyServer(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("PROXY_URL", "http://:8080example.com")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAbsoluteProxyFilterWithHttpsAlwaysAndCustomProxyServer(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
|
||||
os.Setenv("MEDIA_PROXY_CUSTOM_URL", "https://proxy-example/proxy")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
output := RewriteDocumentWithAbsoluteProxyURL(r, input)
|
||||
expected := `<p><img src="https://proxy-example/proxy/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,11 +335,11 @@ func TestProxyFilterWithHttpInvalid(t *testing.T) {
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ProxyRewriter(r, input)
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,11 +359,11 @@ func TestProxyFilterWithHttpsInvalid(t *testing.T) {
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
output := ProxyRewriter(r, input)
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +385,7 @@ func TestProxyFilterWithSrcset(t *testing.T) {
|
||||
|
||||
input := `<p><img src="http://website/folder/image.png" srcset="http://website/folder/image2.png 656w, http://website/folder/image3.png 360w" alt="test"></p>`
|
||||
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" srcset="/proxy/aY5Hb4urDnUCly2vTJ7ExQeeaVS-52O7kjUr2v9VrAs=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, /proxy/QgAmrJWiAud_nNAsz3F8OTxaIofwAiO36EDzH_YfMzo=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMy5wbmc= 360w" alt="test"/></p>`
|
||||
output := ProxyRewriter(r, input)
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %s`, output)
|
||||
@@ -273,7 +410,7 @@ func TestProxyFilterWithEmptySrcset(t *testing.T) {
|
||||
|
||||
input := `<p><img src="http://website/folder/image.png" srcset="" alt="test"></p>`
|
||||
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" srcset="" alt="test"/></p>`
|
||||
output := ProxyRewriter(r, input)
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %s`, output)
|
||||
@@ -298,7 +435,7 @@ func TestProxyFilterWithPictureSource(t *testing.T) {
|
||||
|
||||
input := `<picture><source srcset="http://website/folder/image2.png 656w, http://website/folder/image3.png 360w, https://website/some,image.png 2x"></picture>`
|
||||
expected := `<picture><source srcset="/proxy/aY5Hb4urDnUCly2vTJ7ExQeeaVS-52O7kjUr2v9VrAs=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, /proxy/QgAmrJWiAud_nNAsz3F8OTxaIofwAiO36EDzH_YfMzo=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMy5wbmc= 360w, /proxy/ZIw0hv8WhSTls5aSqhnFaCXlUrKIqTnBRaY0-NaLnds=/aHR0cHM6Ly93ZWJzaXRlL3NvbWUsaW1hZ2UucG5n 2x"/></picture>`
|
||||
output := ProxyRewriter(r, input)
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %s`, output)
|
||||
@@ -323,7 +460,7 @@ func TestProxyFilterOnlyNonHTTPWithPictureSource(t *testing.T) {
|
||||
|
||||
input := `<picture><source srcset="http://website/folder/image2.png 656w, https://website/some,image.png 2x"></picture>`
|
||||
expected := `<picture><source srcset="/proxy/aY5Hb4urDnUCly2vTJ7ExQeeaVS-52O7kjUr2v9VrAs=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, https://website/some,image.png 2x"/></picture>`
|
||||
output := ProxyRewriter(r, input)
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %s`, output)
|
||||
@@ -347,7 +484,7 @@ func TestProxyWithImageDataURL(t *testing.T) {
|
||||
|
||||
input := `<img src="data:image/gif;base64,test">`
|
||||
expected := `<img src="data:image/gif;base64,test"/>`
|
||||
output := ProxyRewriter(r, input)
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %s`, output)
|
||||
@@ -371,7 +508,82 @@ func TestProxyWithImageSourceDataURL(t *testing.T) {
|
||||
|
||||
input := `<picture><source srcset="data:image/gif;base64,test"/></picture>`
|
||||
expected := `<picture><source srcset="data:image/gif;base64,test"/></picture>`
|
||||
output := ProxyRewriter(r, input)
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %s`, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFilterWithVideo(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "video")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<video poster="https://example.com/img.png" src="https://example.com/video.mp4"></video>`
|
||||
expected := `<video poster="/proxy/aDFfroYL57q5XsojIzATT6OYUCkuVSPXYJQAVrotnLw=/aHR0cHM6Ly9leGFtcGxlLmNvbS9pbWcucG5n" src="/proxy/0y3LR8zlx8S8qJkj1qWFOO6x3a-5yf2gLWjGIJV5yyc=/aHR0cHM6Ly9leGFtcGxlLmNvbS92aWRlby5tcDQ="></video>`
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %s`, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFilterVideoPoster(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<video poster="https://example.com/img.png" src="https://example.com/video.mp4"></video>`
|
||||
expected := `<video poster="/proxy/aDFfroYL57q5XsojIzATT6OYUCkuVSPXYJQAVrotnLw=/aHR0cHM6Ly9leGFtcGxlLmNvbS9pbWcucG5n" src="https://example.com/video.mp4"></video>`
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %s`, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyFilterVideoPosterOnce(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image,video")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
input := `<video poster="https://example.com/img.png" src="https://example.com/video.mp4"></video>`
|
||||
expected := `<video poster="/proxy/aDFfroYL57q5XsojIzATT6OYUCkuVSPXYJQAVrotnLw=/aHR0cHM6Ly9leGFtcGxlLmNvbS9pbWcucG5n" src="/proxy/0y3LR8zlx8S8qJkj1qWFOO6x3a-5yf2gLWjGIJV5yyc=/aHR0cHM6Ly9leGFtcGxlLmNvbS92aWRlby5tcDQ="></video>`
|
||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Not expected output: got %s`, output)
|
||||
@@ -0,0 +1,113 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mediaproxy // import "miniflux.app/v2/internal/mediaproxy"
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/reader/sanitizer"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type urlProxyRewriter func(router *mux.Router, url string) string
|
||||
|
||||
func RewriteDocumentWithRelativeProxyURL(router *mux.Router, htmlDocument string) string {
|
||||
return genericProxyRewriter(router, ProxifyRelativeURL, htmlDocument)
|
||||
}
|
||||
|
||||
func RewriteDocumentWithAbsoluteProxyURL(router *mux.Router, htmlDocument string) string {
|
||||
return genericProxyRewriter(router, ProxifyAbsoluteURL, htmlDocument)
|
||||
}
|
||||
|
||||
func genericProxyRewriter(router *mux.Router, proxifyFunction urlProxyRewriter, htmlDocument string) string {
|
||||
proxyOption := config.Opts.MediaProxyMode()
|
||||
if proxyOption == "none" {
|
||||
return htmlDocument
|
||||
}
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlDocument))
|
||||
if err != nil {
|
||||
return htmlDocument
|
||||
}
|
||||
|
||||
for _, mediaType := range config.Opts.MediaProxyResourceTypes() {
|
||||
switch mediaType {
|
||||
case "image":
|
||||
doc.Find("img, picture source").Each(func(i int, img *goquery.Selection) {
|
||||
if srcAttrValue, ok := img.Attr("src"); ok {
|
||||
if shouldProxy(srcAttrValue, proxyOption) {
|
||||
img.SetAttr("src", proxifyFunction(router, srcAttrValue))
|
||||
}
|
||||
}
|
||||
|
||||
if srcsetAttrValue, ok := img.Attr("srcset"); ok {
|
||||
proxifySourceSet(img, router, proxifyFunction, proxyOption, srcsetAttrValue)
|
||||
}
|
||||
})
|
||||
|
||||
if !slices.Contains(config.Opts.MediaProxyResourceTypes(), "video") {
|
||||
doc.Find("video").Each(func(i int, video *goquery.Selection) {
|
||||
if posterAttrValue, ok := video.Attr("poster"); ok {
|
||||
if shouldProxy(posterAttrValue, proxyOption) {
|
||||
video.SetAttr("poster", proxifyFunction(router, posterAttrValue))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
case "audio":
|
||||
doc.Find("audio, audio source").Each(func(i int, audio *goquery.Selection) {
|
||||
if srcAttrValue, ok := audio.Attr("src"); ok {
|
||||
if shouldProxy(srcAttrValue, proxyOption) {
|
||||
audio.SetAttr("src", proxifyFunction(router, srcAttrValue))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
case "video":
|
||||
doc.Find("video, video source").Each(func(i int, video *goquery.Selection) {
|
||||
if srcAttrValue, ok := video.Attr("src"); ok {
|
||||
if shouldProxy(srcAttrValue, proxyOption) {
|
||||
video.SetAttr("src", proxifyFunction(router, srcAttrValue))
|
||||
}
|
||||
}
|
||||
|
||||
if posterAttrValue, ok := video.Attr("poster"); ok {
|
||||
if shouldProxy(posterAttrValue, proxyOption) {
|
||||
video.SetAttr("poster", proxifyFunction(router, posterAttrValue))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
output, err := doc.Find("body").First().Html()
|
||||
if err != nil {
|
||||
return htmlDocument
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func proxifySourceSet(element *goquery.Selection, router *mux.Router, proxifyFunction urlProxyRewriter, proxyOption, srcsetAttrValue string) {
|
||||
imageCandidates := sanitizer.ParseSrcSetAttribute(srcsetAttrValue)
|
||||
|
||||
for _, imageCandidate := range imageCandidates {
|
||||
if shouldProxy(imageCandidate.ImageURL, proxyOption) {
|
||||
imageCandidate.ImageURL = proxifyFunction(router, imageCandidate.ImageURL)
|
||||
}
|
||||
}
|
||||
|
||||
element.SetAttr("srcset", imageCandidates.String())
|
||||
}
|
||||
|
||||
func shouldProxy(attrValue, proxyOption string) bool {
|
||||
return !strings.HasPrefix(attrValue, "data:") &&
|
||||
(proxyOption == "all" || !urllib.IsHTTPS(attrValue))
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mediaproxy // import "miniflux.app/v2/internal/mediaproxy"
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/route"
|
||||
)
|
||||
|
||||
func ProxifyRelativeURL(router *mux.Router, mediaURL string) string {
|
||||
if mediaURL == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if customProxyURL := config.Opts.MediaCustomProxyURL(); customProxyURL != "" {
|
||||
return proxifyURLWithCustomProxy(mediaURL, customProxyURL)
|
||||
}
|
||||
|
||||
mac := hmac.New(sha256.New, config.Opts.MediaProxyPrivateKey())
|
||||
mac.Write([]byte(mediaURL))
|
||||
digest := mac.Sum(nil)
|
||||
return route.Path(router, "proxy", "encodedDigest", base64.URLEncoding.EncodeToString(digest), "encodedURL", base64.URLEncoding.EncodeToString([]byte(mediaURL)))
|
||||
}
|
||||
|
||||
func ProxifyAbsoluteURL(router *mux.Router, mediaURL string) string {
|
||||
if mediaURL == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if customProxyURL := config.Opts.MediaCustomProxyURL(); customProxyURL != "" {
|
||||
return proxifyURLWithCustomProxy(mediaURL, customProxyURL)
|
||||
}
|
||||
|
||||
// Note that the proxyified URL is relative to the root URL.
|
||||
proxifiedUrl := ProxifyRelativeURL(router, mediaURL)
|
||||
absoluteURL, err := url.JoinPath(config.Opts.RootURL(), proxifiedUrl)
|
||||
if err != nil {
|
||||
return mediaURL
|
||||
}
|
||||
|
||||
return absoluteURL
|
||||
}
|
||||
|
||||
func proxifyURLWithCustomProxy(mediaURL, customProxyURL string) string {
|
||||
if customProxyURL == "" {
|
||||
return mediaURL
|
||||
}
|
||||
|
||||
absoluteURL, err := url.JoinPath(customProxyURL, base64.URLEncoding.EncodeToString([]byte(mediaURL)))
|
||||
if err != nil {
|
||||
slog.Error("Incorrect custom media proxy URL",
|
||||
slog.String("custom_proxy_url", customProxyURL),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
return mediaURL
|
||||
}
|
||||
|
||||
return absoluteURL
|
||||
}
|
||||
@@ -67,5 +67,5 @@ type Session struct {
|
||||
}
|
||||
|
||||
func (s *Session) String() string {
|
||||
return fmt.Sprintf(`ID="%s", Data={%v}`, s.ID, s.Data)
|
||||
return fmt.Sprintf(`ID=%q, Data={%v}`, s.ID, s.Data)
|
||||
}
|
||||
|
||||
+79
-10
@@ -2,7 +2,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
import "strings"
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/mediaproxy"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
)
|
||||
|
||||
// Enclosure represents an attachment.
|
||||
type Enclosure struct {
|
||||
@@ -15,20 +23,81 @@ type Enclosure struct {
|
||||
MediaProgression int64 `json:"media_progression"`
|
||||
}
|
||||
|
||||
type EnclosureUpdateRequest struct {
|
||||
MediaProgression int64 `json:"media_progression"`
|
||||
}
|
||||
|
||||
// Html5MimeType will modify the actual MimeType to allow direct playback from HTML5 player for some kind of MimeType
|
||||
func (e Enclosure) Html5MimeType() string {
|
||||
if strings.HasPrefix(e.MimeType, "video") {
|
||||
switch e.MimeType {
|
||||
// Solution from this stackoverflow discussion:
|
||||
// https://stackoverflow.com/questions/15277147/m4v-mimetype-video-mp4-or-video-m4v/66945470#66945470
|
||||
// tested at the time of this commit (06/2023) on latest Firefox & Vivaldi on this feed
|
||||
// https://www.florenceporcel.com/podcast/lfhdu.xml
|
||||
case "video/m4v":
|
||||
return "video/x-m4v"
|
||||
}
|
||||
if e.MimeType == "video/m4v" {
|
||||
return "video/x-m4v"
|
||||
}
|
||||
return e.MimeType
|
||||
}
|
||||
|
||||
func (e *Enclosure) IsAudio() bool {
|
||||
return strings.HasPrefix(strings.ToLower(e.MimeType), "audio/")
|
||||
}
|
||||
|
||||
func (e *Enclosure) IsVideo() bool {
|
||||
return strings.HasPrefix(strings.ToLower(e.MimeType), "video/")
|
||||
}
|
||||
|
||||
func (e *Enclosure) IsImage() bool {
|
||||
mimeType := strings.ToLower(e.MimeType)
|
||||
mediaURL := strings.ToLower(e.URL)
|
||||
return strings.HasPrefix(mimeType, "image/") || strings.HasSuffix(mediaURL, ".jpg") || strings.HasSuffix(mediaURL, ".jpeg") || strings.HasSuffix(mediaURL, ".png") || strings.HasSuffix(mediaURL, ".gif")
|
||||
}
|
||||
|
||||
// EnclosureList represents a list of attachments.
|
||||
type EnclosureList []*Enclosure
|
||||
|
||||
// FindMediaPlayerEnclosure returns the first enclosure that can be played by a media player.
|
||||
func (el EnclosureList) FindMediaPlayerEnclosure() *Enclosure {
|
||||
for _, enclosure := range el {
|
||||
if enclosure.URL != "" && strings.Contains(enclosure.MimeType, "audio/") || strings.Contains(enclosure.MimeType, "video/") {
|
||||
return enclosure
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (el EnclosureList) ContainsAudioOrVideo() bool {
|
||||
for _, enclosure := range el {
|
||||
if strings.Contains(enclosure.MimeType, "audio/") || strings.Contains(enclosure.MimeType, "video/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (el EnclosureList) ProxifyEnclosureURL(router *mux.Router) {
|
||||
proxyOption := config.Opts.MediaProxyMode()
|
||||
|
||||
if proxyOption == "all" || proxyOption != "none" {
|
||||
for i := range el {
|
||||
if urllib.IsHTTPS(el[i].URL) {
|
||||
for _, mediaType := range config.Opts.MediaProxyResourceTypes() {
|
||||
if strings.HasPrefix(el[i].MimeType, mediaType+"/") {
|
||||
el[i].URL = mediaproxy.ProxifyAbsoluteURL(router, el[i].URL)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Enclosure) ProxifyEnclosureURL(router *mux.Router) {
|
||||
proxyOption := config.Opts.MediaProxyMode()
|
||||
|
||||
if proxyOption == "all" || proxyOption != "none" && !urllib.IsHTTPS(e.URL) {
|
||||
for _, mediaType := range config.Opts.MediaProxyResourceTypes() {
|
||||
if strings.HasPrefix(e.MimeType, mediaType+"/") {
|
||||
e.URL = mediaproxy.ProxifyAbsoluteURL(router, e.URL)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,22 @@ func NewEntry() *Entry {
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldMarkAsReadOnView Return whether the entry should be marked as viewed considering all user settings and entry state.
|
||||
func (e *Entry) ShouldMarkAsReadOnView(user *User) bool {
|
||||
// Already read, no need to mark as read again. Removed entries are not marked as read
|
||||
if e.Status != EntryStatusUnread {
|
||||
return false
|
||||
}
|
||||
|
||||
// There is an enclosure, markAsRead will happen at enclosure completion time, no need to mark as read on view
|
||||
if user.MarkReadOnMediaPlayerCompletion && e.Enclosures.ContainsAudioOrVideo() {
|
||||
return false
|
||||
}
|
||||
|
||||
// The user wants to mark as read on view
|
||||
return user.MarkReadOnView
|
||||
}
|
||||
|
||||
// Entries represents a list of entries.
|
||||
type Entries []*Entry
|
||||
|
||||
|
||||
+29
-29
@@ -28,6 +28,7 @@ type Feed struct {
|
||||
FeedURL string `json:"feed_url"`
|
||||
SiteURL string `json:"site_url"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
CheckedAt time.Time `json:"checked_at"`
|
||||
NextCheckAt time.Time `json:"next_check_at"`
|
||||
EtagHeader string `json:"etag_header"`
|
||||
@@ -50,13 +51,17 @@ type Feed struct {
|
||||
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
DisableHTTP2 bool `json:"disable_http2"`
|
||||
AppriseServiceURLs string `json:"apprise_service_urls"`
|
||||
NtfyEnabled bool `json:"ntfy_enabled"`
|
||||
NtfyPriority int `json:"ntfy_priority"`
|
||||
|
||||
// Non persisted attributes
|
||||
// Non-persisted attributes
|
||||
Category *Category `json:"category,omitempty"`
|
||||
Icon *FeedIcon `json:"icon"`
|
||||
Entries Entries `json:"entries,omitempty"`
|
||||
|
||||
// Internal attributes (not exposed in the API and not persisted in the database)
|
||||
TTL int `json:"-"`
|
||||
IconURL string `json:"-"`
|
||||
UnreadCount int `json:"-"`
|
||||
@@ -107,12 +112,13 @@ func (f *Feed) CheckedNow() {
|
||||
}
|
||||
|
||||
// ScheduleNextCheck set "next_check_at" of a feed based on the scheduler selected from the configuration.
|
||||
func (f *Feed) ScheduleNextCheck(weeklyCount int, newTTL int) {
|
||||
f.TTL = newTTL
|
||||
func (f *Feed) ScheduleNextCheck(weeklyCount int, refreshDelayInMinutes int) {
|
||||
f.TTL = refreshDelayInMinutes
|
||||
|
||||
// Default to the global config Polling Frequency.
|
||||
var intervalMinutes int
|
||||
switch config.Opts.PollingScheduler() {
|
||||
case SchedulerEntryFrequency:
|
||||
intervalMinutes := config.Opts.SchedulerRoundRobinMinInterval()
|
||||
|
||||
if config.Opts.PollingScheduler() == SchedulerEntryFrequency {
|
||||
if weeklyCount <= 0 {
|
||||
intervalMinutes = config.Opts.SchedulerEntryFrequencyMaxInterval()
|
||||
} else {
|
||||
@@ -120,13 +126,13 @@ func (f *Feed) ScheduleNextCheck(weeklyCount int, newTTL int) {
|
||||
intervalMinutes = int(math.Min(float64(intervalMinutes), float64(config.Opts.SchedulerEntryFrequencyMaxInterval())))
|
||||
intervalMinutes = int(math.Max(float64(intervalMinutes), float64(config.Opts.SchedulerEntryFrequencyMinInterval())))
|
||||
}
|
||||
default:
|
||||
intervalMinutes = config.Opts.SchedulerRoundRobinMinInterval()
|
||||
}
|
||||
// If the feed has a TTL defined, we use it to make sure we don't check it too often.
|
||||
if newTTL > intervalMinutes && newTTL > 0 {
|
||||
intervalMinutes = newTTL
|
||||
|
||||
// If the feed has a TTL or a Retry-After defined, we use it to make sure we don't check it too often.
|
||||
if refreshDelayInMinutes > 0 && refreshDelayInMinutes > intervalMinutes {
|
||||
intervalMinutes = refreshDelayInMinutes
|
||||
}
|
||||
|
||||
f.NextCheckAt = time.Now().Add(time.Minute * time.Duration(intervalMinutes))
|
||||
}
|
||||
|
||||
@@ -150,6 +156,7 @@ type FeedCreationRequest struct {
|
||||
KeeplistRules string `json:"keeplist_rules"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
UrlRewriteRules string `json:"urlrewrite_rules"`
|
||||
DisableHTTP2 bool `json:"disable_http2"`
|
||||
}
|
||||
|
||||
type FeedCreationRequestFromSubscriptionDiscovery struct {
|
||||
@@ -157,24 +164,7 @@ type FeedCreationRequestFromSubscriptionDiscovery struct {
|
||||
ETag string
|
||||
LastModified string
|
||||
|
||||
FeedURL string `json:"feed_url"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Cookie string `json:"cookie"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Crawler bool `json:"crawler"`
|
||||
Disabled bool `json:"disabled"`
|
||||
NoMediaPlayer bool `json:"no_media_player"`
|
||||
IgnoreHTTPCache bool `json:"ignore_http_cache"`
|
||||
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
ScraperRules string `json:"scraper_rules"`
|
||||
RewriteRules string `json:"rewrite_rules"`
|
||||
BlocklistRules string `json:"blocklist_rules"`
|
||||
KeeplistRules string `json:"keeplist_rules"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
UrlRewriteRules string `json:"urlrewrite_rules"`
|
||||
FeedCreationRequest
|
||||
}
|
||||
|
||||
// FeedModificationRequest represents the request to update a feed.
|
||||
@@ -182,6 +172,7 @@ type FeedModificationRequest struct {
|
||||
FeedURL *string `json:"feed_url"`
|
||||
SiteURL *string `json:"site_url"`
|
||||
Title *string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
ScraperRules *string `json:"scraper_rules"`
|
||||
RewriteRules *string `json:"rewrite_rules"`
|
||||
BlocklistRules *string `json:"blocklist_rules"`
|
||||
@@ -199,6 +190,7 @@ type FeedModificationRequest struct {
|
||||
AllowSelfSignedCertificates *bool `json:"allow_self_signed_certificates"`
|
||||
FetchViaProxy *bool `json:"fetch_via_proxy"`
|
||||
HideGlobally *bool `json:"hide_globally"`
|
||||
DisableHTTP2 *bool `json:"disable_http2"`
|
||||
}
|
||||
|
||||
// Patch updates a feed with modified values.
|
||||
@@ -215,6 +207,10 @@ func (f *FeedModificationRequest) Patch(feed *Feed) {
|
||||
feed.Title = *f.Title
|
||||
}
|
||||
|
||||
if f.Description != nil && *f.Description != "" {
|
||||
feed.Description = *f.Description
|
||||
}
|
||||
|
||||
if f.ScraperRules != nil {
|
||||
feed.ScraperRules = *f.ScraperRules
|
||||
}
|
||||
@@ -282,6 +278,10 @@ func (f *FeedModificationRequest) Patch(feed *Feed) {
|
||||
if f.HideGlobally != nil {
|
||||
feed.HideGlobally = *f.HideGlobally
|
||||
}
|
||||
|
||||
if f.DisableHTTP2 != nil {
|
||||
feed.DisableHTTP2 = *f.DisableHTTP2
|
||||
}
|
||||
}
|
||||
|
||||
// Feeds is a list of feed
|
||||
|
||||
+57
-13
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
const (
|
||||
largeWeeklyCount = 10080
|
||||
noNewTTL = 0
|
||||
noRefreshDelay = 0
|
||||
)
|
||||
|
||||
func TestFeedCategorySetter(t *testing.T) {
|
||||
@@ -76,7 +76,7 @@ func checkTargetInterval(t *testing.T, feed *Feed, targetInterval int, timeBefor
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedScheduleNextCheckDefault(t *testing.T) {
|
||||
func TestFeedScheduleNextCheckRoundRobinDefault(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
var err error
|
||||
@@ -88,15 +88,60 @@ func TestFeedScheduleNextCheckDefault(t *testing.T) {
|
||||
|
||||
timeBefore := time.Now()
|
||||
feed := &Feed{}
|
||||
weeklyCount := 10
|
||||
feed.ScheduleNextCheck(weeklyCount, noNewTTL)
|
||||
feed.ScheduleNextCheck(0, noRefreshDelay)
|
||||
|
||||
if feed.NextCheckAt.IsZero() {
|
||||
t.Error(`The next_check_at must be set`)
|
||||
}
|
||||
|
||||
targetInterval := config.Opts.SchedulerRoundRobinMinInterval()
|
||||
checkTargetInterval(t, feed, targetInterval, timeBefore, "default SchedulerRoundRobinMinInterval")
|
||||
checkTargetInterval(t, feed, targetInterval, timeBefore, "TestFeedScheduleNextCheckRoundRobinDefault")
|
||||
}
|
||||
|
||||
func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMinInterval(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
timeBefore := time.Now()
|
||||
feed := &Feed{}
|
||||
|
||||
feed.ScheduleNextCheck(0, config.Opts.SchedulerRoundRobinMinInterval()+30)
|
||||
|
||||
if feed.NextCheckAt.IsZero() {
|
||||
t.Error(`The next_check_at must be set`)
|
||||
}
|
||||
|
||||
expectedInterval := config.Opts.SchedulerRoundRobinMinInterval() + 30
|
||||
checkTargetInterval(t, feed, expectedInterval, timeBefore, "TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMinInterval")
|
||||
}
|
||||
|
||||
func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayBelowMinInterval(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
timeBefore := time.Now()
|
||||
feed := &Feed{}
|
||||
|
||||
feed.ScheduleNextCheck(0, config.Opts.SchedulerRoundRobinMinInterval()-30)
|
||||
|
||||
if feed.NextCheckAt.IsZero() {
|
||||
t.Error(`The next_check_at must be set`)
|
||||
}
|
||||
|
||||
expectedInterval := config.Opts.SchedulerRoundRobinMinInterval()
|
||||
checkTargetInterval(t, feed, expectedInterval, timeBefore, "TestFeedScheduleNextCheckRoundRobinWithRefreshDelayBelowMinInterval")
|
||||
}
|
||||
|
||||
func TestFeedScheduleNextCheckRoundRobinMinInterval(t *testing.T) {
|
||||
@@ -114,15 +159,14 @@ func TestFeedScheduleNextCheckRoundRobinMinInterval(t *testing.T) {
|
||||
|
||||
timeBefore := time.Now()
|
||||
feed := &Feed{}
|
||||
weeklyCount := 100
|
||||
feed.ScheduleNextCheck(weeklyCount, noNewTTL)
|
||||
feed.ScheduleNextCheck(0, noRefreshDelay)
|
||||
|
||||
if feed.NextCheckAt.IsZero() {
|
||||
t.Error(`The next_check_at must be set`)
|
||||
}
|
||||
|
||||
targetInterval := minInterval
|
||||
checkTargetInterval(t, feed, targetInterval, timeBefore, "round robin min interval")
|
||||
expectedInterval := minInterval
|
||||
checkTargetInterval(t, feed, expectedInterval, timeBefore, "TestFeedScheduleNextCheckRoundRobinMinInterval")
|
||||
}
|
||||
|
||||
func TestFeedScheduleNextCheckEntryFrequencyMaxInterval(t *testing.T) {
|
||||
@@ -144,7 +188,7 @@ func TestFeedScheduleNextCheckEntryFrequencyMaxInterval(t *testing.T) {
|
||||
feed := &Feed{}
|
||||
// Use a very small weekly count to trigger the max interval
|
||||
weeklyCount := 1
|
||||
feed.ScheduleNextCheck(weeklyCount, noNewTTL)
|
||||
feed.ScheduleNextCheck(weeklyCount, noRefreshDelay)
|
||||
|
||||
if feed.NextCheckAt.IsZero() {
|
||||
t.Error(`The next_check_at must be set`)
|
||||
@@ -173,7 +217,7 @@ func TestFeedScheduleNextCheckEntryFrequencyMaxIntervalZeroWeeklyCount(t *testin
|
||||
feed := &Feed{}
|
||||
// Use a very small weekly count to trigger the max interval
|
||||
weeklyCount := 0
|
||||
feed.ScheduleNextCheck(weeklyCount, noNewTTL)
|
||||
feed.ScheduleNextCheck(weeklyCount, noRefreshDelay)
|
||||
|
||||
if feed.NextCheckAt.IsZero() {
|
||||
t.Error(`The next_check_at must be set`)
|
||||
@@ -202,7 +246,7 @@ func TestFeedScheduleNextCheckEntryFrequencyMinInterval(t *testing.T) {
|
||||
feed := &Feed{}
|
||||
// Use a very large weekly count to trigger the min interval
|
||||
weeklyCount := largeWeeklyCount
|
||||
feed.ScheduleNextCheck(weeklyCount, noNewTTL)
|
||||
feed.ScheduleNextCheck(weeklyCount, noRefreshDelay)
|
||||
|
||||
if feed.NextCheckAt.IsZero() {
|
||||
t.Error(`The next_check_at must be set`)
|
||||
@@ -228,7 +272,7 @@ func TestFeedScheduleNextCheckEntryFrequencyFactor(t *testing.T) {
|
||||
timeBefore := time.Now()
|
||||
feed := &Feed{}
|
||||
weeklyCount := 7
|
||||
feed.ScheduleNextCheck(weeklyCount, noNewTTL)
|
||||
feed.ScheduleNextCheck(weeklyCount, noRefreshDelay)
|
||||
|
||||
if feed.NextCheckAt.IsZero() {
|
||||
t.Error(`The next_check_at must be set`)
|
||||
|
||||
@@ -6,6 +6,9 @@ package model // import "miniflux.app/v2/internal/model"
|
||||
// Integration represents user integration settings.
|
||||
type Integration struct {
|
||||
UserID int64
|
||||
BetulaEnabled bool
|
||||
BetulaURL string
|
||||
BetulaToken string
|
||||
PinboardEnabled bool
|
||||
PinboardToken string
|
||||
PinboardTags string
|
||||
@@ -70,6 +73,11 @@ type Integration struct {
|
||||
AppriseEnabled bool
|
||||
AppriseURL string
|
||||
AppriseServicesURL string
|
||||
ReadeckEnabled bool
|
||||
ReadeckURL string
|
||||
ReadeckAPIKey string
|
||||
ReadeckLabels string
|
||||
ReadeckOnlyURL bool
|
||||
ShioriEnabled bool
|
||||
ShioriURL string
|
||||
ShioriUsername string
|
||||
@@ -85,4 +93,17 @@ type Integration struct {
|
||||
OmnivoreEnabled bool
|
||||
OmnivoreAPIKey string
|
||||
OmnivoreURL string
|
||||
RaindropEnabled bool
|
||||
RaindropToken string
|
||||
RaindropCollectionID string
|
||||
RaindropTags string
|
||||
NtfyEnabled bool
|
||||
NtfyTopic string
|
||||
NtfyURL string
|
||||
NtfyAPIToken string
|
||||
NtfyUsername string
|
||||
NtfyPassword string
|
||||
NtfyIconURL string
|
||||
CuboxEnabled bool
|
||||
CuboxAPILink string
|
||||
}
|
||||
|
||||
+11
-17
@@ -3,26 +3,20 @@
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
// OptionalString populates an optional string field.
|
||||
type Number interface {
|
||||
int | int64 | float64
|
||||
}
|
||||
|
||||
func OptionalNumber[T Number](value T) *T {
|
||||
if value > 0 {
|
||||
return &value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func OptionalString(value string) *string {
|
||||
if value != "" {
|
||||
return &value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OptionalInt populates an optional int field.
|
||||
func OptionalInt(value int) *int {
|
||||
if value > 0 {
|
||||
return &value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OptionalInt64 populates an optional int64 field.
|
||||
func OptionalInt64(value int64) *int64 {
|
||||
if value > 0 {
|
||||
return &value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,4 +12,5 @@ type SubscriptionDiscoveryRequest struct {
|
||||
Password string `json:"password"`
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
|
||||
DisableHTTP2 bool `json:"disable_http2"`
|
||||
}
|
||||
|
||||
+82
-46
@@ -11,30 +11,36 @@ import (
|
||||
|
||||
// User represents a user in the system.
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"-"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Theme string `json:"theme"`
|
||||
Language string `json:"language"`
|
||||
Timezone string `json:"timezone"`
|
||||
EntryDirection string `json:"entry_sorting_direction"`
|
||||
EntryOrder string `json:"entry_sorting_order"`
|
||||
Stylesheet string `json:"stylesheet"`
|
||||
GoogleID string `json:"google_id"`
|
||||
OpenIDConnectID string `json:"openid_connect_id"`
|
||||
EntriesPerPage int `json:"entries_per_page"`
|
||||
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
||||
ShowReadingTime bool `json:"show_reading_time"`
|
||||
EntrySwipe bool `json:"entry_swipe"`
|
||||
GestureNav string `json:"gesture_nav"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
DisplayMode string `json:"display_mode"`
|
||||
DefaultReadingSpeed int `json:"default_reading_speed"`
|
||||
CJKReadingSpeed int `json:"cjk_reading_speed"`
|
||||
DefaultHomePage string `json:"default_home_page"`
|
||||
CategoriesSortingOrder string `json:"categories_sorting_order"`
|
||||
MarkReadOnView bool `json:"mark_read_on_view"`
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"-"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Theme string `json:"theme"`
|
||||
Language string `json:"language"`
|
||||
Timezone string `json:"timezone"`
|
||||
EntryDirection string `json:"entry_sorting_direction"`
|
||||
EntryOrder string `json:"entry_sorting_order"`
|
||||
Stylesheet string `json:"stylesheet"`
|
||||
CustomJS string `json:"custom_js"`
|
||||
ExternalFontHosts string `json:"external_font_hosts"`
|
||||
GoogleID string `json:"google_id"`
|
||||
OpenIDConnectID string `json:"openid_connect_id"`
|
||||
EntriesPerPage int `json:"entries_per_page"`
|
||||
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
||||
ShowReadingTime bool `json:"show_reading_time"`
|
||||
EntrySwipe bool `json:"entry_swipe"`
|
||||
GestureNav string `json:"gesture_nav"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
DisplayMode string `json:"display_mode"`
|
||||
DefaultReadingSpeed int `json:"default_reading_speed"`
|
||||
CJKReadingSpeed int `json:"cjk_reading_speed"`
|
||||
DefaultHomePage string `json:"default_home_page"`
|
||||
CategoriesSortingOrder string `json:"categories_sorting_order"`
|
||||
MarkReadOnView bool `json:"mark_read_on_view"`
|
||||
MarkReadOnMediaPlayerCompletion bool `json:"mark_read_on_media_player_completion"`
|
||||
MediaPlaybackRate float64 `json:"media_playback_rate"`
|
||||
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
||||
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
||||
}
|
||||
|
||||
// UserCreationRequest represents the request to create a user.
|
||||
@@ -48,28 +54,34 @@ type UserCreationRequest struct {
|
||||
|
||||
// UserModificationRequest represents the request to update a user.
|
||||
type UserModificationRequest struct {
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
Theme *string `json:"theme"`
|
||||
Language *string `json:"language"`
|
||||
Timezone *string `json:"timezone"`
|
||||
EntryDirection *string `json:"entry_sorting_direction"`
|
||||
EntryOrder *string `json:"entry_sorting_order"`
|
||||
Stylesheet *string `json:"stylesheet"`
|
||||
GoogleID *string `json:"google_id"`
|
||||
OpenIDConnectID *string `json:"openid_connect_id"`
|
||||
EntriesPerPage *int `json:"entries_per_page"`
|
||||
IsAdmin *bool `json:"is_admin"`
|
||||
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
||||
ShowReadingTime *bool `json:"show_reading_time"`
|
||||
EntrySwipe *bool `json:"entry_swipe"`
|
||||
GestureNav *string `json:"gesture_nav"`
|
||||
DisplayMode *string `json:"display_mode"`
|
||||
DefaultReadingSpeed *int `json:"default_reading_speed"`
|
||||
CJKReadingSpeed *int `json:"cjk_reading_speed"`
|
||||
DefaultHomePage *string `json:"default_home_page"`
|
||||
CategoriesSortingOrder *string `json:"categories_sorting_order"`
|
||||
MarkReadOnView *bool `json:"mark_read_on_view"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
Theme *string `json:"theme"`
|
||||
Language *string `json:"language"`
|
||||
Timezone *string `json:"timezone"`
|
||||
EntryDirection *string `json:"entry_sorting_direction"`
|
||||
EntryOrder *string `json:"entry_sorting_order"`
|
||||
Stylesheet *string `json:"stylesheet"`
|
||||
CustomJS *string `json:"custom_js"`
|
||||
ExternalFontHosts *string `json:"external_font_hosts"`
|
||||
GoogleID *string `json:"google_id"`
|
||||
OpenIDConnectID *string `json:"openid_connect_id"`
|
||||
EntriesPerPage *int `json:"entries_per_page"`
|
||||
IsAdmin *bool `json:"is_admin"`
|
||||
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
||||
ShowReadingTime *bool `json:"show_reading_time"`
|
||||
EntrySwipe *bool `json:"entry_swipe"`
|
||||
GestureNav *string `json:"gesture_nav"`
|
||||
DisplayMode *string `json:"display_mode"`
|
||||
DefaultReadingSpeed *int `json:"default_reading_speed"`
|
||||
CJKReadingSpeed *int `json:"cjk_reading_speed"`
|
||||
DefaultHomePage *string `json:"default_home_page"`
|
||||
CategoriesSortingOrder *string `json:"categories_sorting_order"`
|
||||
MarkReadOnView *bool `json:"mark_read_on_view"`
|
||||
MarkReadOnMediaPlayerCompletion *bool `json:"mark_read_on_media_player_completion"`
|
||||
MediaPlaybackRate *float64 `json:"media_playback_rate"`
|
||||
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
|
||||
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
|
||||
}
|
||||
|
||||
// Patch updates the User object with the modification request.
|
||||
@@ -110,6 +122,14 @@ func (u *UserModificationRequest) Patch(user *User) {
|
||||
user.Stylesheet = *u.Stylesheet
|
||||
}
|
||||
|
||||
if u.CustomJS != nil {
|
||||
user.CustomJS = *u.CustomJS
|
||||
}
|
||||
|
||||
if u.ExternalFontHosts != nil {
|
||||
user.ExternalFontHosts = *u.ExternalFontHosts
|
||||
}
|
||||
|
||||
if u.GoogleID != nil {
|
||||
user.GoogleID = *u.GoogleID
|
||||
}
|
||||
@@ -161,6 +181,22 @@ func (u *UserModificationRequest) Patch(user *User) {
|
||||
if u.MarkReadOnView != nil {
|
||||
user.MarkReadOnView = *u.MarkReadOnView
|
||||
}
|
||||
|
||||
if u.MarkReadOnMediaPlayerCompletion != nil {
|
||||
user.MarkReadOnMediaPlayerCompletion = *u.MarkReadOnMediaPlayerCompletion
|
||||
}
|
||||
|
||||
if u.MediaPlaybackRate != nil {
|
||||
user.MediaPlaybackRate = *u.MediaPlaybackRate
|
||||
}
|
||||
|
||||
if u.BlockFilterEntryRules != nil {
|
||||
user.BlockFilterEntryRules = *u.BlockFilterEntryRules
|
||||
}
|
||||
|
||||
if u.KeepFilterEntryRules != nil {
|
||||
user.KeepFilterEntryRules = *u.KeepFilterEntryRules
|
||||
}
|
||||
}
|
||||
|
||||
// UseTimezone converts last login date to the given timezone.
|
||||
|
||||
@@ -21,7 +21,7 @@ type UserSession struct {
|
||||
}
|
||||
|
||||
func (u *UserSession) String() string {
|
||||
return fmt.Sprintf(`ID="%d", UserID="%d", IP="%s", Token="%s"`, u.ID, u.UserID, u.IP, u.Token)
|
||||
return fmt.Sprintf(`ID=%q, UserID=%q, IP=%q, Token=%q`, u.ID, u.UserID, u.IP, u.Token)
|
||||
}
|
||||
|
||||
// UseTimezone converts creation date to the given timezone.
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package proxy // import "miniflux.app/v2/internal/proxy"
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/reader/sanitizer"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type urlProxyRewriter func(router *mux.Router, url string) string
|
||||
|
||||
// ProxyRewriter replaces media URLs with internal proxy URLs.
|
||||
func ProxyRewriter(router *mux.Router, data string) string {
|
||||
return genericProxyRewriter(router, ProxifyURL, data)
|
||||
}
|
||||
|
||||
// AbsoluteProxyRewriter do the same as ProxyRewriter except it uses absolute URLs.
|
||||
func AbsoluteProxyRewriter(router *mux.Router, host, data string) string {
|
||||
proxifyFunction := func(router *mux.Router, url string) string {
|
||||
return AbsoluteProxifyURL(router, host, url)
|
||||
}
|
||||
return genericProxyRewriter(router, proxifyFunction, data)
|
||||
}
|
||||
|
||||
func genericProxyRewriter(router *mux.Router, proxifyFunction urlProxyRewriter, data string) string {
|
||||
proxyOption := config.Opts.ProxyOption()
|
||||
if proxyOption == "none" {
|
||||
return data
|
||||
}
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(data))
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
|
||||
for _, mediaType := range config.Opts.ProxyMediaTypes() {
|
||||
switch mediaType {
|
||||
case "image":
|
||||
doc.Find("img").Each(func(i int, img *goquery.Selection) {
|
||||
if srcAttrValue, ok := img.Attr("src"); ok {
|
||||
if !isDataURL(srcAttrValue) && (proxyOption == "all" || !urllib.IsHTTPS(srcAttrValue)) {
|
||||
img.SetAttr("src", proxifyFunction(router, srcAttrValue))
|
||||
}
|
||||
}
|
||||
|
||||
if srcsetAttrValue, ok := img.Attr("srcset"); ok {
|
||||
proxifySourceSet(img, router, proxifyFunction, proxyOption, srcsetAttrValue)
|
||||
}
|
||||
})
|
||||
|
||||
doc.Find("picture source").Each(func(i int, sourceElement *goquery.Selection) {
|
||||
if srcsetAttrValue, ok := sourceElement.Attr("srcset"); ok {
|
||||
proxifySourceSet(sourceElement, router, proxifyFunction, proxyOption, srcsetAttrValue)
|
||||
}
|
||||
})
|
||||
|
||||
case "audio":
|
||||
doc.Find("audio").Each(func(i int, audio *goquery.Selection) {
|
||||
if srcAttrValue, ok := audio.Attr("src"); ok {
|
||||
if !isDataURL(srcAttrValue) && (proxyOption == "all" || !urllib.IsHTTPS(srcAttrValue)) {
|
||||
audio.SetAttr("src", proxifyFunction(router, srcAttrValue))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
doc.Find("audio source").Each(func(i int, sourceElement *goquery.Selection) {
|
||||
if srcAttrValue, ok := sourceElement.Attr("src"); ok {
|
||||
if !isDataURL(srcAttrValue) && (proxyOption == "all" || !urllib.IsHTTPS(srcAttrValue)) {
|
||||
sourceElement.SetAttr("src", proxifyFunction(router, srcAttrValue))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
case "video":
|
||||
doc.Find("video").Each(func(i int, video *goquery.Selection) {
|
||||
if srcAttrValue, ok := video.Attr("src"); ok {
|
||||
if !isDataURL(srcAttrValue) && (proxyOption == "all" || !urllib.IsHTTPS(srcAttrValue)) {
|
||||
video.SetAttr("src", proxifyFunction(router, srcAttrValue))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
doc.Find("video source").Each(func(i int, sourceElement *goquery.Selection) {
|
||||
if srcAttrValue, ok := sourceElement.Attr("src"); ok {
|
||||
if !isDataURL(srcAttrValue) && (proxyOption == "all" || !urllib.IsHTTPS(srcAttrValue)) {
|
||||
sourceElement.SetAttr("src", proxifyFunction(router, srcAttrValue))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
output, err := doc.Find("body").First().Html()
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func proxifySourceSet(element *goquery.Selection, router *mux.Router, proxifyFunction urlProxyRewriter, proxyOption, srcsetAttrValue string) {
|
||||
imageCandidates := sanitizer.ParseSrcSetAttribute(srcsetAttrValue)
|
||||
|
||||
for _, imageCandidate := range imageCandidates {
|
||||
if !isDataURL(imageCandidate.ImageURL) && (proxyOption == "all" || !urllib.IsHTTPS(imageCandidate.ImageURL)) {
|
||||
imageCandidate.ImageURL = proxifyFunction(router, imageCandidate.ImageURL)
|
||||
}
|
||||
}
|
||||
|
||||
element.SetAttr("srcset", imageCandidates.String())
|
||||
}
|
||||
|
||||
func isDataURL(s string) bool {
|
||||
return strings.HasPrefix(s, "data:")
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package proxy // import "miniflux.app/v2/internal/proxy"
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"miniflux.app/v2/internal/http/route"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
)
|
||||
|
||||
// ProxifyURL generates a relative URL for a proxified resource.
|
||||
func ProxifyURL(router *mux.Router, link string) string {
|
||||
if link != "" {
|
||||
proxyImageUrl := config.Opts.ProxyUrl()
|
||||
|
||||
if proxyImageUrl == "" {
|
||||
mac := hmac.New(sha256.New, config.Opts.ProxyPrivateKey())
|
||||
mac.Write([]byte(link))
|
||||
digest := mac.Sum(nil)
|
||||
return route.Path(router, "proxy", "encodedDigest", base64.URLEncoding.EncodeToString(digest), "encodedURL", base64.URLEncoding.EncodeToString([]byte(link)))
|
||||
}
|
||||
|
||||
proxyUrl, err := url.Parse(proxyImageUrl)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
proxyUrl.Path = path.Join(proxyUrl.Path, base64.URLEncoding.EncodeToString([]byte(link)))
|
||||
return proxyUrl.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// AbsoluteProxifyURL generates an absolute URL for a proxified resource.
|
||||
func AbsoluteProxifyURL(router *mux.Router, host, link string) string {
|
||||
if link != "" {
|
||||
proxyImageUrl := config.Opts.ProxyUrl()
|
||||
|
||||
if proxyImageUrl == "" {
|
||||
mac := hmac.New(sha256.New, config.Opts.ProxyPrivateKey())
|
||||
mac.Write([]byte(link))
|
||||
digest := mac.Sum(nil)
|
||||
path := route.Path(router, "proxy", "encodedDigest", base64.URLEncoding.EncodeToString(digest), "encodedURL", base64.URLEncoding.EncodeToString([]byte(link)))
|
||||
if config.Opts.HTTPS {
|
||||
return "https://" + host + path
|
||||
} else {
|
||||
return "http://" + host + path
|
||||
}
|
||||
}
|
||||
|
||||
proxyUrl, err := url.Parse(proxyImageUrl)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
proxyUrl.Path = path.Join(proxyUrl.Path, base64.URLEncoding.EncodeToString([]byte(link)))
|
||||
return proxyUrl.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
+86
-130
@@ -6,158 +6,114 @@ package atom // import "miniflux.app/v2/internal/reader/atom"
|
||||
import (
|
||||
"encoding/base64"
|
||||
"html"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/crypto"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/reader/date"
|
||||
"miniflux.app/v2/internal/reader/sanitizer"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
)
|
||||
|
||||
// Specs: http://web.archive.org/web/20060811235523/http://www.mnot.net/drafts/draft-nottingham-atom-format-02.html
|
||||
type atom03Feed struct {
|
||||
ID string `xml:"id"`
|
||||
Title atom03Text `xml:"title"`
|
||||
Author atomPerson `xml:"author"`
|
||||
Links atomLinks `xml:"link"`
|
||||
Entries []atom03Entry `xml:"entry"`
|
||||
type Atom03Feed struct {
|
||||
Version string `xml:"version,attr"`
|
||||
|
||||
// The "atom:id" element's content conveys a permanent, globally unique identifier for the feed.
|
||||
// It MUST NOT change over time, even if the feed is relocated. atom:feed elements MAY contain an atom:id element,
|
||||
// but MUST NOT contain more than one. The content of this element, when present, MUST be a URI.
|
||||
ID string `xml:"http://purl.org/atom/ns# id"`
|
||||
|
||||
// The "atom:title" element is a Content construct that conveys a human-readable title for the feed.
|
||||
// atom:feed elements MUST contain exactly one atom:title element.
|
||||
// If the feed describes a Web resource, its content SHOULD be the same as that resource's title.
|
||||
Title Atom03Content `xml:"http://purl.org/atom/ns# title"`
|
||||
|
||||
// The "atom:link" element is a Link construct that conveys a URI associated with the feed.
|
||||
// The nature of the relationship as well as the link itself is determined by the element's content.
|
||||
// atom:feed elements MUST contain at least one atom:link element with a rel attribute value of "alternate".
|
||||
// atom:feed elements MUST NOT contain more than one atom:link element with a rel attribute value of "alternate" that has the same type attribute value.
|
||||
// atom:feed elements MAY contain additional atom:link elements beyond those described above.
|
||||
Links AtomLinks `xml:"http://purl.org/atom/ns# link"`
|
||||
|
||||
// The "atom:author" element is a Person construct that indicates the default author of the feed.
|
||||
// atom:feed elements MUST contain exactly one atom:author element,
|
||||
// UNLESS all of the atom:feed element's child atom:entry elements contain an atom:author element.
|
||||
// atom:feed elements MUST NOT contain more than one atom:author element.
|
||||
Author AtomPerson `xml:"http://purl.org/atom/ns# author"`
|
||||
|
||||
// The "atom:entry" element's represents an individual entry that is contained by the feed.
|
||||
// atom:feed elements MAY contain one or more atom:entry elements.
|
||||
Entries []Atom03Entry `xml:"http://purl.org/atom/ns# entry"`
|
||||
}
|
||||
|
||||
func (a *atom03Feed) Transform(baseURL string) *model.Feed {
|
||||
var err error
|
||||
type Atom03Entry struct {
|
||||
// The "atom:id" element's content conveys a permanent, globally unique identifier for the entry.
|
||||
// It MUST NOT change over time, even if other representations of the entry (such as a web representation pointed to by the entry's atom:link element) are relocated.
|
||||
// If the same entry is syndicated in two atom:feeds published by the same entity, the entry's atom:id MUST be the same in both feeds.
|
||||
ID string `xml:"id"`
|
||||
|
||||
feed := new(model.Feed)
|
||||
// The "atom:title" element is a Content construct that conveys a human-readable title for the entry.
|
||||
// atom:entry elements MUST have exactly one "atom:title" element.
|
||||
// If an entry describes a Web resource, its content SHOULD be the same as that resource's title.
|
||||
Title Atom03Content `xml:"title"`
|
||||
|
||||
feedURL := a.Links.firstLinkWithRelation("self")
|
||||
feed.FeedURL, err = urllib.AbsoluteURL(baseURL, feedURL)
|
||||
if err != nil {
|
||||
feed.FeedURL = feedURL
|
||||
}
|
||||
// The "atom:modified" element is a Date construct that indicates the time that the entry was last modified.
|
||||
// atom:entry elements MUST contain an atom:modified element, but MUST NOT contain more than one.
|
||||
// The content of an atom:modified element MUST have a time zone whose value SHOULD be "UTC".
|
||||
Modified string `xml:"modified"`
|
||||
|
||||
siteURL := a.Links.originalLink()
|
||||
feed.SiteURL, err = urllib.AbsoluteURL(baseURL, siteURL)
|
||||
if err != nil {
|
||||
feed.SiteURL = siteURL
|
||||
}
|
||||
// The "atom:issued" element is a Date construct that indicates the time that the entry was issued.
|
||||
// atom:entry elements MUST contain an atom:issued element, but MUST NOT contain more than one.
|
||||
// The content of an atom:issued element MAY omit a time zone.
|
||||
Issued string `xml:"issued"`
|
||||
|
||||
feed.Title = a.Title.String()
|
||||
if feed.Title == "" {
|
||||
feed.Title = feed.SiteURL
|
||||
}
|
||||
// The "atom:created" element is a Date construct that indicates the time that the entry was created.
|
||||
// atom:entry elements MAY contain an atom:created element, but MUST NOT contain more than one.
|
||||
// The content of an atom:created element MUST have a time zone whose value SHOULD be "UTC".
|
||||
// If atom:created is not present, its content MUST considered to be the same as that of atom:modified.
|
||||
Created string `xml:"created"`
|
||||
|
||||
for _, entry := range a.Entries {
|
||||
item := entry.Transform()
|
||||
entryURL, err := urllib.AbsoluteURL(feed.SiteURL, item.URL)
|
||||
if err == nil {
|
||||
item.URL = entryURL
|
||||
}
|
||||
// The "atom:link" element is a Link construct that conveys a URI associated with the entry.
|
||||
// The nature of the relationship as well as the link itself is determined by the element's content.
|
||||
// atom:entry elements MUST contain at least one atom:link element with a rel attribute value of "alternate".
|
||||
// atom:entry elements MUST NOT contain more than one atom:link element with a rel attribute value of "alternate" that has the same type attribute value.
|
||||
// atom:entry elements MAY contain additional atom:link elements beyond those described above.
|
||||
Links AtomLinks `xml:"link"`
|
||||
|
||||
if item.Author == "" {
|
||||
item.Author = a.Author.String()
|
||||
}
|
||||
// The "atom:summary" element is a Content construct that conveys a short summary, abstract or excerpt of the entry.
|
||||
// atom:entry elements MAY contain an atom:created element, but MUST NOT contain more than one.
|
||||
Summary Atom03Content `xml:"summary"`
|
||||
|
||||
if item.Title == "" {
|
||||
item.Title = sanitizer.TruncateHTML(item.Content, 100)
|
||||
}
|
||||
// The "atom:content" element is a Content construct that conveys the content of the entry.
|
||||
// atom:entry elements MAY contain one or more atom:content elements.
|
||||
Content Atom03Content `xml:"content"`
|
||||
|
||||
if item.Title == "" {
|
||||
item.Title = item.URL
|
||||
}
|
||||
|
||||
feed.Entries = append(feed.Entries, item)
|
||||
}
|
||||
|
||||
return feed
|
||||
// The "atom:author" element is a Person construct that indicates the default author of the entry.
|
||||
// atom:entry elements MUST contain exactly one atom:author element,
|
||||
// UNLESS the atom:feed element containing them contains an atom:author element itself.
|
||||
// atom:entry elements MUST NOT contain more than one atom:author element.
|
||||
Author AtomPerson `xml:"author"`
|
||||
}
|
||||
|
||||
type atom03Entry struct {
|
||||
ID string `xml:"id"`
|
||||
Title atom03Text `xml:"title"`
|
||||
Modified string `xml:"modified"`
|
||||
Issued string `xml:"issued"`
|
||||
Created string `xml:"created"`
|
||||
Links atomLinks `xml:"link"`
|
||||
Summary atom03Text `xml:"summary"`
|
||||
Content atom03Text `xml:"content"`
|
||||
Author atomPerson `xml:"author"`
|
||||
}
|
||||
type Atom03Content struct {
|
||||
// Content constructs MAY have a "type" attribute, whose value indicates the media type of the content.
|
||||
// When present, this attribute's value MUST be a registered media type [RFC2045].
|
||||
// If not present, its value MUST be considered to be "text/plain".
|
||||
Type string `xml:"type,attr"`
|
||||
|
||||
func (a *atom03Entry) Transform() *model.Entry {
|
||||
entry := model.NewEntry()
|
||||
entry.URL = a.Links.originalLink()
|
||||
entry.Date = a.entryDate()
|
||||
entry.Author = a.Author.String()
|
||||
entry.Hash = a.entryHash()
|
||||
entry.Content = a.entryContent()
|
||||
entry.Title = a.entryTitle()
|
||||
return entry
|
||||
}
|
||||
// Content constructs MAY have a "mode" attribute, whose value indicates the method used to encode the content.
|
||||
// When present, this attribute's value MUST be listed below.
|
||||
// If not present, its value MUST be considered to be "xml".
|
||||
//
|
||||
// "xml": A mode attribute with the value "xml" indicates that the element's content is inline xml (for example, namespace-qualified XHTML).
|
||||
//
|
||||
// "escaped": A mode attribute with the value "escaped" indicates that the element's content is an escaped string.
|
||||
// Processors MUST unescape the element's content before considering it as content of the indicated media type.
|
||||
//
|
||||
// "base64": A mode attribute with the value "base64" indicates that the element's content is base64-encoded [RFC2045].
|
||||
// Processors MUST decode the element's content before considering it as content of the the indicated media type.
|
||||
Mode string `xml:"mode,attr"`
|
||||
|
||||
func (a *atom03Entry) entryTitle() string {
|
||||
return sanitizer.StripTags(a.Title.String())
|
||||
}
|
||||
|
||||
func (a *atom03Entry) entryContent() string {
|
||||
content := a.Content.String()
|
||||
if content != "" {
|
||||
return content
|
||||
}
|
||||
|
||||
summary := a.Summary.String()
|
||||
if summary != "" {
|
||||
return summary
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *atom03Entry) entryDate() time.Time {
|
||||
dateText := ""
|
||||
for _, value := range []string{a.Issued, a.Modified, a.Created} {
|
||||
if value != "" {
|
||||
dateText = value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if dateText != "" {
|
||||
result, err := date.Parse(dateText)
|
||||
if err != nil {
|
||||
slog.Debug("Unable to parse date from Atom 0.3 feed",
|
||||
slog.String("date", dateText),
|
||||
slog.String("id", a.ID),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (a *atom03Entry) entryHash() string {
|
||||
for _, value := range []string{a.ID, a.Links.originalLink()} {
|
||||
if value != "" {
|
||||
return crypto.Hash(value)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
type atom03Text struct {
|
||||
Type string `xml:"type,attr"`
|
||||
Mode string `xml:"mode,attr"`
|
||||
CharData string `xml:",chardata"`
|
||||
InnerXML string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func (a *atom03Text) String() string {
|
||||
func (a *Atom03Content) Content() string {
|
||||
content := ""
|
||||
|
||||
switch {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package atom // import "miniflux.app/v2/internal/reader/atom"
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/crypto"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/reader/date"
|
||||
"miniflux.app/v2/internal/reader/sanitizer"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
)
|
||||
|
||||
type Atom03Adapter struct {
|
||||
atomFeed *Atom03Feed
|
||||
}
|
||||
|
||||
func NewAtom03Adapter(atomFeed *Atom03Feed) *Atom03Adapter {
|
||||
return &Atom03Adapter{atomFeed}
|
||||
}
|
||||
|
||||
func (a *Atom03Adapter) BuildFeed(baseURL string) *model.Feed {
|
||||
feed := new(model.Feed)
|
||||
|
||||
// Populate the feed URL.
|
||||
feedURL := a.atomFeed.Links.firstLinkWithRelation("self")
|
||||
if feedURL != "" {
|
||||
if absoluteFeedURL, err := urllib.AbsoluteURL(baseURL, feedURL); err == nil {
|
||||
feed.FeedURL = absoluteFeedURL
|
||||
}
|
||||
} else {
|
||||
feed.FeedURL = baseURL
|
||||
}
|
||||
|
||||
// Populate the site URL.
|
||||
siteURL := a.atomFeed.Links.OriginalLink()
|
||||
if siteURL != "" {
|
||||
if absoluteSiteURL, err := urllib.AbsoluteURL(baseURL, siteURL); err == nil {
|
||||
feed.SiteURL = absoluteSiteURL
|
||||
}
|
||||
} else {
|
||||
feed.SiteURL = baseURL
|
||||
}
|
||||
|
||||
// Populate the feed title.
|
||||
feed.Title = a.atomFeed.Title.Content()
|
||||
if feed.Title == "" {
|
||||
feed.Title = feed.SiteURL
|
||||
}
|
||||
|
||||
for _, atomEntry := range a.atomFeed.Entries {
|
||||
entry := model.NewEntry()
|
||||
|
||||
// Populate the entry URL.
|
||||
entry.URL = atomEntry.Links.OriginalLink()
|
||||
if entry.URL != "" {
|
||||
if absoluteEntryURL, err := urllib.AbsoluteURL(feed.SiteURL, entry.URL); err == nil {
|
||||
entry.URL = absoluteEntryURL
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the entry content.
|
||||
entry.Content = atomEntry.Content.Content()
|
||||
if entry.Content == "" {
|
||||
entry.Content = atomEntry.Summary.Content()
|
||||
}
|
||||
|
||||
// Populate the entry title.
|
||||
entry.Title = atomEntry.Title.Content()
|
||||
if entry.Title == "" {
|
||||
entry.Title = sanitizer.TruncateHTML(entry.Content, 100)
|
||||
}
|
||||
if entry.Title == "" {
|
||||
entry.Title = entry.URL
|
||||
}
|
||||
|
||||
// Populate the entry author.
|
||||
entry.Author = atomEntry.Author.PersonName()
|
||||
if entry.Author == "" {
|
||||
entry.Author = a.atomFeed.Author.PersonName()
|
||||
}
|
||||
|
||||
// Populate the entry date.
|
||||
for _, value := range []string{atomEntry.Issued, atomEntry.Modified, atomEntry.Created} {
|
||||
if parsedDate, err := date.Parse(value); err == nil {
|
||||
entry.Date = parsedDate
|
||||
break
|
||||
} else {
|
||||
slog.Debug("Unable to parse date from Atom 0.3 feed",
|
||||
slog.String("date", value),
|
||||
slog.String("id", atomEntry.ID),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
}
|
||||
}
|
||||
if entry.Date.IsZero() {
|
||||
entry.Date = time.Now()
|
||||
}
|
||||
|
||||
// Generate the entry hash.
|
||||
for _, value := range []string{atomEntry.ID, atomEntry.Links.OriginalLink()} {
|
||||
if value != "" {
|
||||
entry.Hash = crypto.Hash(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
feed.Entries = append(feed.Entries, entry)
|
||||
}
|
||||
|
||||
return feed
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user