Compare commits
254 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d7d554df8 | |||
| 66b269e6cd | |||
| 54abd0a736 | |||
| 5eab4753e8 | |||
| bf466425db | |||
| a62b97bddd | |||
| 1de9cf4241 | |||
| 7912b9b8fb | |||
| 2d24f5d04e | |||
| 20825a92c5 | |||
| 1d1162327e | |||
| 202de7c787 | |||
| b470b186b3 | |||
| 5877cf340a | |||
| d417b0ff12 | |||
| 703f113fbd | |||
| f02213a168 | |||
| 410b43a787 | |||
| d9de9d1852 | |||
| 33d55cc4e9 | |||
| 1825320369 | |||
| d80fb242db | |||
| 4336a0bd85 | |||
| dc81725788 | |||
| 86e2ce6d0b | |||
| 4679691c94 | |||
| 0d5f4a710f | |||
| 92d2ac4f58 | |||
| 1cfee27a50 | |||
| 0e9da3a090 | |||
| 57bd384951 | |||
| fdbd5b08a1 | |||
| f455c18c66 | |||
| 9dea26c923 | |||
| 46adb0ffad | |||
| 61583d53d5 | |||
| 7c42e777ec | |||
| 335dffbb75 | |||
| f0cdfb33dd | |||
| 13ef89f785 | |||
| 32fbb4e882 | |||
| 135ce1d546 | |||
| abed7b11ce | |||
| 2e26f5ca75 | |||
| d6d18a2d61 | |||
| 63891501e5 | |||
| 7107ff985f | |||
| 50d5cb96c8 | |||
| f860daef7f | |||
| e7b98afdbe | |||
| 2cfeefc8d2 | |||
| b48e6472f5 | |||
| 24043ece07 | |||
| a09129d220 | |||
| f864a2ed70 | |||
| 052e8dd0aa | |||
| 7a394b0bf8 | |||
| dcfe0a7d94 | |||
| 33c648825f | |||
| 78c7f66df7 | |||
| f2b805850c | |||
| 915b7b3cf7 | |||
| 8e86004936 | |||
| a8b4e88742 | |||
| 15e4c3a374 | |||
| 69a74c4abf | |||
| 766d4ab834 | |||
| cb617ff6e0 | |||
| 8c3f280f32 | |||
| 8a98926674 | |||
| 435a950d64 | |||
| 89c32d518d | |||
| 2f7b2e7375 | |||
| 6eeccae7cd | |||
| 99c5bcdb01 | |||
| aed99e65c1 | |||
| d1a3f98df9 | |||
| 112494bb66 | |||
| 9f7ecdb75a | |||
| 4e1f836266 | |||
| a68de4ee6a | |||
| c064891314 | |||
| 5129f53d58 | |||
| e60f0fd142 | |||
| 2b26a345cd | |||
| 3de31a1a4d | |||
| 560be66147 | |||
| fcf86e33b9 | |||
| 113f6b8982 | |||
| cbdcf1a56c | |||
| 95eb6c1230 | |||
| 643b89ec89 | |||
| 84ebf1a033 | |||
| 875618d786 | |||
| 1503a5c946 | |||
| 8641f5f2a3 | |||
| 93b17af78b | |||
| 92876a0c61 | |||
| d62df4e02a | |||
| 8fa5041c37 | |||
| dc05965895 | |||
| 109e668ac7 | |||
| 6d58052504 | |||
| 491d51c95f | |||
| db49e41acf | |||
| e6b814199b | |||
| b0a10f02fd | |||
| 9c05c3c493 | |||
| 2a9d91c783 | |||
| cb59944d6b | |||
| c12476c1a9 | |||
| bc6ab44ff2 | |||
| 6282ac1f38 | |||
| 96c0ef4efd | |||
| b139ac4a2c | |||
| c818d5bbb8 | |||
| e366710529 | |||
| 5cff4d7117 | |||
| f31a784eaa | |||
| 7edfcc3cf7 | |||
| fe4b00b9f8 | |||
| 46b159ac58 | |||
| 86c58e11f6 | |||
| 9a1d9593b3 | |||
| 43546976d2 | |||
| 3ab9ca9e4d | |||
| 1af6df7cb9 | |||
| 50dff576b0 | |||
| 6af4d69c39 | |||
| 27015a5e34 | |||
| cdb57b3843 | |||
| 152ef578d2 | |||
| 72486b9bd1 | |||
| 81df0b2a16 | |||
| b296f21e98 | |||
| af15032145 | |||
| 6a6a88d06d | |||
| 8660f5e3c7 | |||
| da4ab4263c | |||
| 237672a62c | |||
| e9d4a130fd | |||
| d291d6a74d | |||
| e0f7e6f2a8 | |||
| ef3dbd3707 | |||
| 32f08053aa | |||
| 117c031f1c | |||
| b95c9023ee | |||
| 3538c4271b | |||
| ac44507af2 | |||
| 44c48d109f | |||
| 40727704c2 | |||
| 8a014c6abc | |||
| 60ad19c427 | |||
| f40c1e7f63 | |||
| a4d16cc5c1 | |||
| 7c857bdc72 | |||
| 0caadf82f2 | |||
| 0086e0b356 | |||
| 70b513b8db | |||
| cecc18420d | |||
| d53fd17e10 | |||
| 21d22d7f0b | |||
| d59990f1dd | |||
| 49085daefe | |||
| 5872710d22 | |||
| d2212dee12 | |||
| 8db637cb39 | |||
| 699deea72c | |||
| c41d189a7a | |||
| adfc38d237 | |||
| a8bb7a48d7 | |||
| f9dce3d10f | |||
| 567e8cfc89 | |||
| a8e73d6875 | |||
| fa7f2b18a0 | |||
| 43d302e768 | |||
| 4e181330d0 | |||
| 0369f03940 | |||
| 4597d9b289 | |||
| 7bfd22aab7 | |||
| 325c505b88 | |||
| bfd8860398 | |||
| ebd65da3b6 | |||
| 83191b0c1d | |||
| 8142268799 | |||
| 5920e02562 | |||
| 09fb05aaaf | |||
| 52b184394f | |||
| 7c8c7c2711 | |||
| 9768eb9fb9 | |||
| b65373db7e | |||
| 596d22c02c | |||
| 4b86570b7c | |||
| e99864a456 | |||
| 225463817c | |||
| 3db6e822cb | |||
| 1c19151925 | |||
| 553c578f2e | |||
| 81ec32a8b6 | |||
| 3818a8a4fb | |||
| 036704b3e4 | |||
| 327d027d38 | |||
| 5ae2cbd943 | |||
| f15d29deb3 | |||
| 828a4334db | |||
| d1dc369bb2 | |||
| a8076e1891 | |||
| 3448d6267c | |||
| 159261f2f8 | |||
| ff2dfe977b | |||
| a5e3719773 | |||
| cdadb87203 | |||
| 5284d61fe3 | |||
| 3de9629a49 | |||
| 8d821dfc3b | |||
| cb775bc79e | |||
| 6cc8d8abf1 | |||
| 50395f13ca | |||
| e8c3435bb9 | |||
| 9a8a8bdca3 | |||
| 63f0a17388 | |||
| 81c7669945 | |||
| 2b000d1022 | |||
| 27253c8a97 | |||
| f68046cce0 | |||
| d33e305af9 | |||
| 764212f37c | |||
| e02b65d4bc | |||
| fe7ec25a09 | |||
| 2959a4d2bf | |||
| 6b70a7dc81 | |||
| 495c6aacd9 | |||
| bc228d0afe | |||
| d139d8a6ce | |||
| 28d0185e79 | |||
| c87c93d85f | |||
| 0ef21e85c2 | |||
| 7438d67061 | |||
| e4399b4f9a | |||
| b4f891705a | |||
| 3d72404ace | |||
| c4e92e6111 | |||
| ef22e95f8b | |||
| 7b344de846 | |||
| c45b51d1f8 | |||
| 0af1a6e121 | |||
| 535fd050b7 | |||
| d20e8a4e2c | |||
| 7514e8a0c1 | |||
| ca3ede3183 | |||
| 038d33600a | |||
| 1a3a174688 | |||
| 6848c759b5 | |||
| 10578f512f |
@@ -0,0 +1,81 @@
|
||||
name: "Documentation Issue"
|
||||
description: "Report issues or suggest improvements for the documentation"
|
||||
title: "[Docs]: "
|
||||
type: "Documentation"
|
||||
labels: ["triage needed"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for helping improve the Miniflux documentation! Clear and accurate documentation helps everyone.
|
||||
|
||||
- type: dropdown
|
||||
id: issue_type
|
||||
attributes:
|
||||
label: "Documentation Issue Type"
|
||||
description: "What kind of documentation issue are you reporting?"
|
||||
options:
|
||||
- "Missing Information"
|
||||
- "Incorrect Information"
|
||||
- "Outdated Information"
|
||||
- "Unclear Explanation"
|
||||
- "Formatting/Structural Issue"
|
||||
- "Typo/Grammar Error"
|
||||
- "Documentation Request"
|
||||
- "Other"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: summary
|
||||
attributes:
|
||||
label: "Summary"
|
||||
description: "Briefly describe the documentation issue."
|
||||
placeholder: "e.g., The API authentication section is outdated"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: location
|
||||
attributes:
|
||||
label: "Location"
|
||||
description: "Where is the documentation you're referring to? Provide URLs, file paths, or section names."
|
||||
placeholder: "e.g., README.md, docs/api.md, Installation section of the website"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: "Detailed Description"
|
||||
description: "Provide a detailed description of the issue or improvement."
|
||||
placeholder: "e.g., The API authentication section doesn't mention the new token-based authentication method introduced in version 2.0.5."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: current_content
|
||||
attributes:
|
||||
label: "Current Content (if applicable)"
|
||||
description: "What does the current documentation say?"
|
||||
placeholder: "Paste the current documentation text here."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: suggested_content
|
||||
attributes:
|
||||
label: "Suggested Changes"
|
||||
description: "If you have specific suggestions for how to improve the documentation, please provide them here."
|
||||
placeholder: "e.g., Add a new section about token-based authentication with these details..."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: "Version"
|
||||
description: "Which version of Miniflux does this documentation issue relate to?"
|
||||
placeholder: "e.g., 2.2.6, or 'all versions'"
|
||||
validations:
|
||||
required: false
|
||||
@@ -63,3 +63,5 @@ body:
|
||||
required: true
|
||||
- label: "I understand that feature requests are not guaranteed to be implemented."
|
||||
required: true
|
||||
- label: "I agree to follow the project's contribution guidelines."
|
||||
required: true
|
||||
|
||||
@@ -84,3 +84,5 @@ body:
|
||||
required: true
|
||||
- label: "I agree to provide follow-up updates and maintain discussion on this proposal."
|
||||
required: true
|
||||
- label: "I agree to follow the project's contribution guidelines."
|
||||
required: true
|
||||
|
||||
@@ -4,52 +4,28 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
reviewers:
|
||||
- "fguillot"
|
||||
assignees:
|
||||
- "fguillot"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/packaging/docker/alpine"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
reviewers:
|
||||
- "fguillot"
|
||||
assignees:
|
||||
- "fguillot"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/packaging/docker/distroless"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
reviewers:
|
||||
- "fguillot"
|
||||
assignees:
|
||||
- "fguillot"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "packaging/debian"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
reviewers:
|
||||
- "fguillot"
|
||||
assignees:
|
||||
- "fguillot"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "packaging/rpm"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
reviewers:
|
||||
- "fguillot"
|
||||
assignees:
|
||||
- "fguillot"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
reviewers:
|
||||
- "fguillot"
|
||||
assignees:
|
||||
- "fguillot"
|
||||
|
||||
@@ -4,4 +4,4 @@ Have you followed these guidelines?
|
||||
- [ ] There are no breaking changes
|
||||
- [ ] I have thoroughly tested my changes and verified there are no regressions
|
||||
- [ ] My commit messages follow the [Conventional Commits specification](https://www.conventionalcommits.org/)
|
||||
- [ ] I have read this document: https://miniflux.app/faq.html#pull-request
|
||||
- [ ] I have read and understood the [contribution guidelines](https://github.com/miniflux/v2/blob/main/CONTRIBUTING.md)
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24.x"
|
||||
- uses: golangci/golangci-lint-action@v7
|
||||
- uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
args: >
|
||||
--timeout 10m
|
||||
@@ -39,19 +39,16 @@ jobs:
|
||||
run: gofmt -d -e .
|
||||
|
||||
commitlint:
|
||||
if: github.event_name == 'pull_request'
|
||||
name: Commit Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
- name: Install commitlint
|
||||
run: |
|
||||
npm install --save-dev @commitlint/config-conventional @commitlint/cli
|
||||
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
|
||||
python-version: '3.13'
|
||||
- name: Validate PR commits
|
||||
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
|
||||
run: python3 .github/workflows/scripts/commit-checker.py --base ${{ github.event.pull_request.base.sha }} --head ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
import argparse
|
||||
from typing import Match
|
||||
|
||||
# Conventional commit pattern (including Git revert messages)
|
||||
CONVENTIONAL_COMMIT_PATTERN: str = (
|
||||
r"^((build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9-]+\))?!?: .{1,100}|Revert .+)"
|
||||
)
|
||||
|
||||
|
||||
def get_commit_message(commit_hash: str) -> str:
|
||||
"""Get the commit message for a given commit hash."""
|
||||
try:
|
||||
result: subprocess.CompletedProcess = subprocess.run(
|
||||
["git", "show", "-s", "--format=%B", commit_hash],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error retrieving commit message: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def check_commit_message(message: str, pattern: str = CONVENTIONAL_COMMIT_PATTERN) -> bool:
|
||||
"""Check if commit message follows conventional commit format."""
|
||||
first_line: str = message.split("\n")[0]
|
||||
match: Match[str] | None = re.match(pattern, first_line)
|
||||
return bool(match)
|
||||
|
||||
|
||||
def check_commit_range(base_ref: str, head_ref: str) -> list[dict[str, str]]:
|
||||
"""Check all commits in a range for compliance."""
|
||||
try:
|
||||
result: subprocess.CompletedProcess = subprocess.run(
|
||||
["git", "log", "--format=%H", f"{base_ref}..{head_ref}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
commit_hashes: list[str] = result.stdout.strip().split("\n")
|
||||
|
||||
# Filter out empty lines
|
||||
commit_hashes = [hash for hash in commit_hashes if hash]
|
||||
|
||||
non_compliant: list[dict[str, str]] = []
|
||||
for commit_hash in commit_hashes:
|
||||
message: str = get_commit_message(commit_hash)
|
||||
if not check_commit_message(message):
|
||||
non_compliant.append({"hash": commit_hash, "message": message.split("\n")[0]})
|
||||
|
||||
return non_compliant
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error checking commit range: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser: argparse.ArgumentParser = argparse.ArgumentParser(description="Check conventional commit compliance")
|
||||
parser.add_argument("--base", required=True, help="Base ref (starting commit, exclusive)")
|
||||
parser.add_argument("--head", required=True, help="Head ref (ending commit, inclusive)")
|
||||
args: argparse.Namespace = parser.parse_args()
|
||||
|
||||
non_compliant: list[dict[str, str]] = check_commit_range(args.base, args.head)
|
||||
|
||||
if non_compliant:
|
||||
print("The following commits do not follow the conventional commit format:")
|
||||
for commit in non_compliant:
|
||||
print(f"- {commit['hash'][:8]}: {commit['message']}")
|
||||
print("\nPlease ensure your commit messages follow the format:")
|
||||
print("type(scope): subject")
|
||||
print("\nWhere type is one of: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("All commits follow the conventional commit format!")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
./*.sha256
|
||||
./miniflux
|
||||
/miniflux
|
||||
.idea
|
||||
.vscode
|
||||
*.deb
|
||||
*.rpm
|
||||
miniflux-*
|
||||
miniflux-*
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
# Contributing to Miniflux
|
||||
|
||||
This document outlines how to contribute effectively to Miniflux.
|
||||
|
||||
## Philosophy
|
||||
|
||||
Miniflux follows a **minimalist philosophy**. The feature set is intentionally kept limited to avoid bloatware. Before contributing, please understand that:
|
||||
|
||||
- **Improving existing features takes priority over adding new ones**
|
||||
- **Quality over quantity** - well-implemented, focused features are preferred
|
||||
- **Simplicity is key** - complex solutions are discouraged in favor of simple, maintainable code
|
||||
|
||||
## Before You Start
|
||||
|
||||
### Feature Requests
|
||||
|
||||
Before implementing a new feature:
|
||||
|
||||
- Check if it aligns with Miniflux's philosophy
|
||||
- Consider if the feature could be implemented differently to maintain simplicity
|
||||
- Remember that developing software takes significant time, and this is a volunteer-driven project
|
||||
- If you need a specific feature, the best approach is to contribute it yourself
|
||||
|
||||
### Bug Reports
|
||||
|
||||
When reporting bugs:
|
||||
|
||||
- Search existing issues first to avoid duplicates
|
||||
- Provide clear reproduction steps
|
||||
- Include relevant system information (OS, browser, Miniflux version)
|
||||
- Include error messages, screenshots, and logs when applicable
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Requirements
|
||||
|
||||
- **Git**
|
||||
- **Go >= 1.24**
|
||||
- **PostgreSQL**
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. **Fork the repository** on GitHub
|
||||
2. **Clone your fork locally:**
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/miniflux.git
|
||||
cd miniflux
|
||||
```
|
||||
|
||||
3. **Build the application binary:**
|
||||
```bash
|
||||
make miniflux
|
||||
```
|
||||
|
||||
4. **Run locally in debug mode:**
|
||||
```bash
|
||||
make run
|
||||
```
|
||||
|
||||
### Database Setup
|
||||
|
||||
For development and testing, you can run a local PostgreSQL database with Docker:
|
||||
|
||||
```bash
|
||||
# Start PostgreSQL container
|
||||
docker run --rm --name miniflux2-db -p 5432:5432 \
|
||||
-e POSTGRES_DB=miniflux2 \
|
||||
-e POSTGRES_USER=postgres \
|
||||
-e POSTGRES_PASSWORD=postgres \
|
||||
postgres
|
||||
```
|
||||
|
||||
You can also use an existing PostgreSQL instance. Make sure to set the `DATABASE_URL` environment variable accordingly.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Code Quality
|
||||
|
||||
1. **Run the linter:**
|
||||
```bash
|
||||
make lint
|
||||
```
|
||||
Requires `staticcheck` and `golangci-lint` to be installed.
|
||||
|
||||
2. **Run unit tests:**
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
3. **Run integration tests:**
|
||||
```bash
|
||||
make integration-test
|
||||
make clean-integration-test
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
- **Current platform:** `make miniflux`
|
||||
- **All platforms:** `make build`
|
||||
- **Specific platforms:** `make linux-amd64`, `make darwin-arm64`, etc.
|
||||
- **Docker image:** `make docker-image`
|
||||
|
||||
### Cross-Platform Support
|
||||
|
||||
Miniflux supports multiple architectures. When making changes, ensure compatibility across:
|
||||
- Linux (amd64, arm64, armv7, armv6, armv5)
|
||||
- macOS (amd64, arm64)
|
||||
- FreeBSD, OpenBSD, Windows (amd64)
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
### What Is Preferred
|
||||
|
||||
✅ **Good Pull Requests:**
|
||||
|
||||
- Focus on a single issue or feature
|
||||
- Include tests for new functionality
|
||||
- Maintain or improve performance
|
||||
- Follow existing code style and patterns
|
||||
- The commit messages follow the [conventional commit format](https://www.conventionalcommits.org/) (e.g., `feat: add new feature`, `fix: resolve bug`)
|
||||
- Update documentation when necessary
|
||||
|
||||
### What to Avoid
|
||||
|
||||
❌ **Pull Requests That Cannot Be Accepted:**
|
||||
|
||||
- **Too many changes** - makes review difficult
|
||||
- **Breaking changes** - disrupts existing functionality
|
||||
- **New bugs or regressions** - reduces software quality
|
||||
- **Unnecessary dependencies** - conflicts with minimalist approach
|
||||
- **Performance degradation** - slows down the software
|
||||
- **Poor-quality code** - hard to maintain
|
||||
- **Dependent PRs** - creates review complexity
|
||||
- **Radical UI changes** - disrupts user experience
|
||||
- **Conflicts with philosophy** - doesn't align with minimalist approach
|
||||
|
||||
### Pull Request Template
|
||||
|
||||
When creating a pull request, please include:
|
||||
|
||||
- **Description:** What does this PR do?
|
||||
- **Motivation:** Why is this change needed?
|
||||
- **Testing:** How was this tested?
|
||||
- **Breaking Changes:** Are there any breaking changes?
|
||||
- **Related Issues:** Link to any related issues
|
||||
|
||||
## Code Style
|
||||
|
||||
- Follow Go conventions and best practices
|
||||
- Use `gofmt` to format your Go code, and `jshint` for JavaScript
|
||||
- Write clear, descriptive variable and function names
|
||||
- Include comments for complex logic
|
||||
- Keep functions small and focused
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
- Write unit tests for new functions and methods
|
||||
- Ensure tests are fast and don't require external dependencies
|
||||
- Aim for good test coverage
|
||||
|
||||
### Integration Tests
|
||||
- Add integration tests for new API endpoints
|
||||
- Tests run against a real PostgreSQL database
|
||||
- Ensure tests clean up after themselves
|
||||
|
||||
## Communication
|
||||
|
||||
- **Discussions:** Use GitHub Discussions for general questions and community interaction
|
||||
- **Issues:** Use GitHub issues for bug reports and feature requests
|
||||
- **Pull Requests:** Use PR comments for code-specific discussions
|
||||
- **Philosophy Questions:** Refer to the FAQ for common questions about project direction
|
||||
|
||||
## Questions?
|
||||
|
||||
- Check the [FAQ](https://miniflux.app/faq.html) for common questions
|
||||
- Review the [development documentation](https://miniflux.app/docs/development.html) and [internationalization guide](https://miniflux.app/docs/i18n.html)
|
||||
- Look at existing issues and pull requests for examples
|
||||
@@ -1,3 +1,217 @@
|
||||
Version 2.2.11 (July 26, 2025)
|
||||
------------------------------
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
* **TLS support for Unix sockets**: Miniflux can now serve TLS over Unix domain sockets using `CERT_FILE` and `KEY_FILE` ([#fcf86e3](https://github.com/miniflux/v2/commit/fcf86e3)).
|
||||
* **RSS fallback**: If a feed entry has no URL, Miniflux now uses the enclosure URL as a fallback ([#d9de9d1](https://github.com/miniflux/v2/commit/d9de9d1)).
|
||||
* **Bearer token for Linkwarden**: The Linkwarden integration now uses Bearer token authorization instead of cookies ([#1d11623](https://github.com/miniflux/v2/commit/1d11623)).
|
||||
* **Cookie policy improvement**: `SameSiteStrictMode` is enforced for cookies when OAuth2/OIDC is not used ([#135ce1d](https://github.com/miniflux/v2/commit/135ce1d)).
|
||||
* **Readability engine**: Avoid removing elements with the `content` class during readability parsing ([#66b269e](https://github.com/miniflux/v2/commit/66b269e)).
|
||||
|
||||
### 🛠️ Improvements
|
||||
|
||||
* **Massive readability engine refactoring** and performance optimizations:
|
||||
|
||||
* Improved performance of `getClassWeight`, `getLinkDensity`, and `transformMisusedDivsIntoParagraphs`.
|
||||
* Simplified and optimized internal logic of `removeUnlikelyCandidates`, `getSelectionLength`, and `getArticle`.
|
||||
* Reduced memory allocation in sanitizer and readability components.
|
||||
* **Storage optimization**: Strings are now truncated on the Go side to respect `tsvector` limits, reducing DB load and ensuring valid UTF-8 ([#703f113](https://github.com/miniflux/v2/commit/703f113)).
|
||||
* **Simplified and clarified internal code structure**:
|
||||
|
||||
* Major cleanup and size optimization of internal structs (`Feed`, `FeedCreationRequest`, etc.).
|
||||
* Reduced memory use and improved CPU cache locality.
|
||||
* Numerous refactors across `config`, `template`, `locale`, `subscription`, and `fetcher` modules.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* Fixed an issue with feeds having excessive leading whitespace causing parser buffer issues ([#54abd0a](https://github.com/miniflux/v2/commit/54abd0a)).
|
||||
* Properly preserve UTF-8 when truncating strings for full-text search ([#703f113](https://github.com/miniflux/v2/commit/703f113)).
|
||||
* Fixed logic error in enclosure type detection ([#50d5cb9](https://github.com/miniflux/v2/commit/50d5cb9)).
|
||||
* Fixed incorrect filter rule parsing of Windows-style newlines ([#dc81725](https://github.com/miniflux/v2/commit/dc81725)).
|
||||
* Fixed a panic in `startAutoCertTLSServer` function when using Let's Encrypt automatic certificates ([#f7a6b02](https://github.com/miniflux/v2/commit/f7a6b02))
|
||||
* Improved UI spacing consistency around header/footer ([#32fbb4e](https://github.com/miniflux/v2/commit/32fbb4e)).
|
||||
|
||||
### ⚠️ Breaking Changes
|
||||
|
||||
* **Windows binary no longer distributed**: Windows is no longer a supported platform for binary distribution. Users must build from source if needed ([#b470b18](https://github.com/miniflux/v2/commit/b470b18)).
|
||||
|
||||
### 🧪 Tests & CI
|
||||
|
||||
* Test coverage significantly increased for modules like `readability`, `sanitizer`, `processor`, `locale`, and `storage`.
|
||||
* Commit linter updated to support new Git revert message format.
|
||||
|
||||
### 🐘 Docker & Environment
|
||||
|
||||
* Base Docker image updated to Alpine 3.22.
|
||||
* PostgreSQL Docker example updated to use the latest version.
|
||||
|
||||
### 🌐 Localization
|
||||
|
||||
* Updated Chinese and German translations.
|
||||
|
||||
### 🔒 Dependency Updates
|
||||
|
||||
* Bumped `github.com/go-webauthn/webauthn` to `0.13.4`
|
||||
* Bumped `github.com/tdewolff/minify/v2` to `2.23.10`
|
||||
* Bumped `golang.org/x/*` modules: `image`, `net`, `term`, `crypto`
|
||||
* Bumped `github.com/andybalholm/brotli` to `1.2.0`
|
||||
|
||||
Version 2.2.10 (June 23, 2025)
|
||||
------------------------------
|
||||
|
||||
* test(sanitizer): add unit test for 0x0 pixel tracker
|
||||
* test(sanitizer): add test case to cover Vimeo iframe rewrite without query string
|
||||
* refactor(youtube): Remove a regex and make use of `fetchWatchTime`
|
||||
* refactor(youtube): initialize two maps to the proper length
|
||||
* refactor(tests): use `b.Loop()` instead of for range `b.N`
|
||||
* refactor(server): avoid double call to `Sprintf`
|
||||
* refactor(sanitizer): use global variables to avoid recreating slices on every call
|
||||
* refactor(sanitizer): use a map for iframe allow list
|
||||
* refactor(sanitizer): remove two useless `www.` prefixes
|
||||
* refactor(sanitizer): make `isValidAttribute()` check O(1)
|
||||
* refactor(rewrite): rename `Rewriter` function to `ApplyContentRewriteRules`
|
||||
* refactor(processor): simplify Bilibili processing
|
||||
* refactor(processor): remove a useless type declaration
|
||||
* refactor(processor): remove a duplicated function call
|
||||
* refactor(processor): refactor common code into a `fetchWatchTime` function
|
||||
* refactor(processor): move filters to a `filter` package
|
||||
* refactor(processor): move `FilterEntryMaxAgeDays` filter to filter package
|
||||
* refactor(processor): move `RewriteEntryURL` function to `rewrite` package
|
||||
* refactor(processor): minor simplification of a loop
|
||||
* refactor(internal): add an `urllib.DomainWithoutWWW` function
|
||||
* refactor(http): rename package from `httpd` to `server` for consistency
|
||||
* refactor(http): Don't hardcode TLS configuration
|
||||
* refactor(filter): avoid code duplication between `IsBlockedEntry` and `IsAllowedEntry` functions
|
||||
* refactor(database): drop 3 columns in a single transaction
|
||||
* refactor(crypto): use `rand.Text()` instead of a custom implementation
|
||||
* refactor(config): remove deprecated config options
|
||||
* refactor(appjs): no need to check if always present elements are always present
|
||||
* perf(xml): optimized `NewXMLDecoder`
|
||||
* perf(xml): optimize XML filtering
|
||||
* perf(validator): slightly optimize a regex
|
||||
* perf(timezone): cache `getLocation`'s results
|
||||
* perf(storage): pre-allocate a slice in `RefreshFeedEntries`
|
||||
* perf(storage): optimize away two `Sprintf` calls
|
||||
* perf(sanitizer): use a switch-case instead of a map
|
||||
* perf(sanitizer): minor simplifications of the sanitizer
|
||||
* perf(sanitizer): extract a call to `url.Parse` and make intensive use of it
|
||||
* perf(rss): optimize a bit `BuildFeed`
|
||||
* perf(rss): early return when looking for an item's author
|
||||
* perf(rewrite): make `getPredefinedRewriteRules` O(1)
|
||||
* perf(reader): use a non-cryptographic hash when possible
|
||||
* perf(reader): optimize `RemoveTrackingParameters`
|
||||
* perf(readability): minor regex improvement
|
||||
* perf(media): minor regex simplification
|
||||
* perf(fetcher): pre-allocate the cipherSuites
|
||||
* perf(database): use `TRUNCATE` instead of `DELETE FROM` in migrations
|
||||
* perf(database): marginally speeds migrations up
|
||||
* perf(api): use `math/rand/v2` instead of `math/rand` for better performance
|
||||
* fix(readability): do not remove elements within code blocks
|
||||
* fix(karakeep): correct method name and improve error handling in `SaveURL`
|
||||
* fix(filter): skip invalid rules instead of exiting the loop
|
||||
* feat(ui): display external link in single entry view because the URL was not visible on mobile (no mouse over)
|
||||
* feat(ui): avoid showing an excessive number of tags
|
||||
* feat(ui): add user setting to control `target="_blank"` on links
|
||||
* feat(sanitizer): validate MathML XML namespace
|
||||
* feat(sanitizer): consider images of size 0x0 as pixel trackers
|
||||
* feat(sanitizer): add validation for empty `width` and `height` attributes in img tags
|
||||
* feat(sanitizer): add support for `fetchpriority` and `decoding` attributes in img tags
|
||||
* feat(rewrite): add support for YouTube Shorts video URL pattern
|
||||
* feat(rewrite): add `parkablogs.com` to the referer override list
|
||||
* feat(oidc): use `preferred_username` first instead of `email` claim
|
||||
* feat(locale): update Polish translations
|
||||
* feat(locale): update locales using machine translation
|
||||
* feat(locale): update Indonesian translations
|
||||
* feat(locale): update German translations
|
||||
* feat(locale): update Chinese translations
|
||||
* feat(integration)!: remove Pocket integration (Pocket will no longer be available after July 8, 2025)
|
||||
* feat(filter): add `EntryDate=max-age:duration` filter
|
||||
* feat(css): add margin-bottom to input for consistent spacing
|
||||
* feat(config)!: remove `SERVER_TIMING_HEADER` config option
|
||||
* feat: Allow multiple listen addresses
|
||||
* feat: adding support for saving entries to Karakeep
|
||||
* feat: add entry filters at the feed level
|
||||
* docs(readme): document a couple of nifty features
|
||||
* docs: add `CONTRIBUTING.md` file
|
||||
* chore(template): remove `X-UA-Compatible` meta tag specific to Internet Explorer
|
||||
* build(go): bump to go 1.24
|
||||
* build(deps): bump `library/alpine` in `/packaging/docker/alpine`
|
||||
* build(deps): bump `golang.org/x/net` from `0.40.0` to `0.41.0`
|
||||
* build(deps): bump `golang.org/x/image` from `0.27.0` to `0.28.0`
|
||||
* build(deps): bump `golang.org/x/crypto` from `0.38.0` to `0.39.0`
|
||||
|
||||
Version 2.2.9 (May 26, 2025)
|
||||
----------------------------
|
||||
|
||||
* refactor(googlereader): remove redundant log message
|
||||
* refactor(googlereader): move constants to separate files
|
||||
* fix(webauthn): correct argument in debug log
|
||||
* fix(sanitizer): MathML tags are not fully supported by `golang.org/x/net/html`
|
||||
* fix(migrations): prevent failure at version 45 with long entry URLs
|
||||
* fix(locale): localize Git commit label in about page
|
||||
* fix(googlereader): return a 400 instead of 500 for invalid edit requests
|
||||
* fix(googlereader): handle various item ID formats
|
||||
* fix(googlereader): avoid panic for inexisting feed or category
|
||||
* fix(googlereader): `/items/contents` should accept short form item IDs
|
||||
* feat(webauthn): prefer creation of a client-side discoverable credential
|
||||
* feat(urlcleaner): remove the `ref` parameter from url
|
||||
* feat(settings): replace `div.panel` with paragraph tags for OAuth2 links
|
||||
* feat(settings): add validation for entry order and categories sorting order
|
||||
* feat(settings): add option to always open articles externally
|
||||
* feat(server): add liveness and readiness probes
|
||||
* feat(sanitizer): add MathML tags to the sanitizer
|
||||
* feat(sanitized): allow Spotify iframes
|
||||
* feat(rssbridge): support authentication token for RSS-Bridge
|
||||
* feat(response): change error response content type to plain text and escape HTML
|
||||
* feat(reader): populate feed description automatically
|
||||
* feat(locale): update Russian translation
|
||||
* feat(locale): update Polish translation
|
||||
* feat(locale): update French translation
|
||||
* feat(googlereader): avoid SQL query to fetch username in streamItemContentsHandler
|
||||
* feat(googlereader): add `mark-all-as-read` endpoint
|
||||
* feat(api): add new endpoints to manage API keys
|
||||
* ci: remove deprecated `reviewers` field from `dependantbot.yml`
|
||||
* chore(gitignore): ignore miniflux binary in root directory
|
||||
* build(deps): bump `golangci/golangci-lint-action` from `7` to `8`
|
||||
* build(deps): bump `golang.org/x/oauth2` from `0.29.0` to `0.30.0`
|
||||
* build(deps): bump `golang.org/x/net` from `0.39.0` to `0.40.0`
|
||||
* build(deps): bump `golang.org/x/image` from `0.26.0` to `0.27.0`
|
||||
* build(deps): bump `golang.org/x/crypto` from `0.37.0` to `0.38.0`
|
||||
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.23.3` to `2.23.8`
|
||||
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.23.1` to `2.23.3`
|
||||
* build(deps): bump `github.com/go-webauthn/webauthn` from `0.12.3` to `0.13.0`
|
||||
|
||||
Version 2.2.8 (April 22, 2025)
|
||||
------------------------------
|
||||
|
||||
* refactor(js): replace `DomHelper` methods with standalone functions
|
||||
* refactor: avoid logging twice the feed errors in the background worker
|
||||
* fix(api): `hide_globally` categories field should be a boolean
|
||||
* fix(ui): add missing `await` when calling `navigator.share()` method
|
||||
* fix(ui): replace share link with a form button for better accessibility
|
||||
* feat(telegrambot): replace "Go to website" button with "Go to Miniflux"
|
||||
* feat(locale): update Polish translation
|
||||
* feat(locale): update German translation
|
||||
* feat(locale): update Chinese translation
|
||||
* feat(config): add `SCHEDULER_ROUND_ROBIN_MAX_INTERVAL` option
|
||||
* feat(cli): add `-reset-feed-next-check-at` argument
|
||||
* feat(api): add `update_content` query parameter to `/entries/{entryID}/fetch-content` endpoint
|
||||
* feat: use `Cache-Control` max-age and `Expires` headers to calculate next check
|
||||
* feat: implement proxy URL per feed
|
||||
* feat: add proxy rotation functionality
|
||||
* ci(linter): replace commitlint with a Python script
|
||||
* ci: add documentation issue template
|
||||
* build(deps): bump `golang.org/x/oauth2` from `0.28.0` to `0.29.0`
|
||||
* build(deps): bump `golang.org/x/net` from `0.38.0` to `0.39.0`
|
||||
* build(deps): bump `golang.org/x/image` from `0.25.0` to `0.26.0`
|
||||
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.22.4` to `2.23.1`
|
||||
* build(deps): bump `github.com/PuerkitoBio/goquery` from `1.10.2` to `1.10.3`
|
||||
* build(deps): bump `github.com/prometheus/client_golang`
|
||||
* build(deps): bump `github.com/mattn/go-sqlite3` from `1.14.24` to `1.14.28`
|
||||
* build(deps): bump `github.com/go-webauthn/webauthn` from `0.12.2` to `0.12.3`
|
||||
* build(deps): bump `github.com/coreos/go-oidc/v3` from `3.13.0` to `3.14.1`
|
||||
|
||||
Version 2.2.7 (April 1, 2025)
|
||||
-----------------------------
|
||||
|
||||
|
||||
@@ -22,16 +22,12 @@ export PGPASSWORD := postgres
|
||||
darwin-amd64 \
|
||||
darwin-arm64 \
|
||||
freebsd-amd64 \
|
||||
freebsd-x86 \
|
||||
openbsd-amd64 \
|
||||
openbsd-x86 \
|
||||
netbsd-x86 \
|
||||
netbsd-amd64 \
|
||||
windows-amd64 \
|
||||
windows-x86 \
|
||||
build \
|
||||
run \
|
||||
clean \
|
||||
add-string \
|
||||
test \
|
||||
lint \
|
||||
integration-test \
|
||||
@@ -85,30 +81,7 @@ 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)-$@.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
|
||||
|
||||
# NOTE: unsupported targets
|
||||
netbsd-amd64:
|
||||
@ CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
|
||||
linux-x86:
|
||||
@ CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
|
||||
freebsd-x86:
|
||||
@ CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
|
||||
netbsd-x86:
|
||||
@ CGO_ENABLED=0 GOOS=netbsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||
|
||||
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)-$@.exe main.go
|
||||
build: linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-armv5 darwin-amd64 darwin-arm64 freebsd-amd64 openbsd-amd64
|
||||
|
||||
run:
|
||||
@ LOG_DATE_TIME=1 LOG_LEVEL=debug RUN_MIGRATIONS=1 CREATE_ADMIN=1 ADMIN_USERNAME=admin ADMIN_PASSWORD=test123 go run main.go
|
||||
@@ -116,7 +89,6 @@ run:
|
||||
clean:
|
||||
@ rm -f $(APP)-* $(APP) $(APP)*.rpm $(APP)*.deb $(APP)*.exe $(APP)*.sha256
|
||||
|
||||
.PHONY: add-string
|
||||
add-string:
|
||||
cd internal/locale/translations && \
|
||||
for file in *.json; do \
|
||||
@@ -125,7 +97,6 @@ add-string:
|
||||
mv tmp "$$file"; \
|
||||
done
|
||||
|
||||
|
||||
test:
|
||||
go test -cover -race -count=1 ./...
|
||||
|
||||
@@ -144,7 +115,7 @@ integration-test:
|
||||
ADMIN_PASSWORD=test123 \
|
||||
CREATE_ADMIN=1 \
|
||||
RUN_MIGRATIONS=1 \
|
||||
DEBUG=1 \
|
||||
LOG_LEVEL=debug \
|
||||
./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
|
||||
|
||||
while ! nc -z localhost 8080; do sleep 1; done
|
||||
|
||||
@@ -22,7 +22,7 @@ Features
|
||||
- Provides full-text search (powered by Postgres).
|
||||
- Available in 20 languages: Portuguese (Brazilian), Chinese (Simplified and Traditional), Dutch, English (US), Finnish, French, German, Greek, Hindi, Indonesian, Italian, Japanese, Polish, Romanian, Russian, Taiwanese POJ, Ukrainian, Spanish, and Turkish.
|
||||
|
||||
### Privacy
|
||||
### Privacy and Security
|
||||
|
||||
- Removes pixel trackers.
|
||||
- Strips tracking parameters from URLs (e.g., `utm_source`, `utm_medium`, `utm_campaign`, `fbclid`, etc.).
|
||||
@@ -33,6 +33,8 @@ Features
|
||||
- Plays YouTube videos via the privacy-focused domain `youtube-nocookie.com`.
|
||||
- Supports alternative YouTube video players such as [Invidious](https://invidio.us).
|
||||
- Blocks external JavaScript to prevent tracking and enhance security.
|
||||
- Sanitizes external content before rendering it.
|
||||
- Enforces a [Content Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) and a [Trusted Types Policy](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API) to only application JavaScript and blocks inline scripts and styles.
|
||||
|
||||
### Bot Protection Bypass Mechanisms
|
||||
|
||||
@@ -70,7 +72,7 @@ Features
|
||||
|
||||
### Integrations
|
||||
|
||||
- 25+ integrations with third-party services: [Apprise](https://github.com/caronc/apprise), [Betula](https://sr.ht/~bouncepaw/betula/), [Cubox](https://cubox.cc/), [Discord](https://discord.com/), [Espial](https://github.com/jonschoning/espial), [Instapaper](https://www.instapaper.com/), [LinkAce](https://www.linkace.org/), [Linkding](https://github.com/sissbruecker/linkding), [LinkWarden](https://linkwarden.app/), [Matrix](https://matrix.org), [Notion](https://www.notion.com/), [Ntfy](https://ntfy.sh/), [Nunux Keeper](https://keeper.nunux.org/), [Pinboard](https://pinboard.in/), [Pocket](https://getpocket.com/), [Pushover](https://pushover.net), [RainDrop](https://raindrop.io/), [Readeck](https://readeck.org/en/), [Readwise Reader](https://readwise.io/read), [RssBridge](https://rss-bridge.org/), [Shaarli](https://github.com/shaarli/Shaarli), [Shiori](https://github.com/go-shiori/shiori), [Slack](https://slack.com/), [Telegram](https://telegram.org), [Wallabag](https://www.wallabag.org/), etc.
|
||||
- 25+ integrations with third-party services: [Apprise](https://github.com/caronc/apprise), [Betula](https://sr.ht/~bouncepaw/betula/), [Cubox](https://cubox.cc/), [Discord](https://discord.com/), [Espial](https://github.com/jonschoning/espial), [Instapaper](https://www.instapaper.com/), [LinkAce](https://www.linkace.org/), [Linkding](https://github.com/sissbruecker/linkding), [LinkWarden](https://linkwarden.app/), [Matrix](https://matrix.org), [Notion](https://www.notion.com/), [Ntfy](https://ntfy.sh/), [Nunux Keeper](https://keeper.nunux.org/), [Pinboard](https://pinboard.in/), [Pushover](https://pushover.net), [RainDrop](https://raindrop.io/), [Readeck](https://readeck.org/en/), [Readwise Reader](https://readwise.io/read), [RssBridge](https://rss-bridge.org/), [Shaarli](https://github.com/shaarli/Shaarli), [Shiori](https://github.com/go-shiori/shiori), [Slack](https://slack.com/), [Telegram](https://telegram.org), [Wallabag](https://www.wallabag.org/), etc.
|
||||
- Bookmarklet for subscribing to websites directly from any web browser.
|
||||
- Webhooks for real-time notifications or custom integrations.
|
||||
- Compatibility with existing mobile applications using the Fever or Google Reader API.
|
||||
@@ -97,13 +99,15 @@ Features
|
||||
- Allows the use of custom <abbr title="Secure Sockets Layer">SSL</abbr> certificates.
|
||||
- Supports [HTTP/2](https://en.wikipedia.org/wiki/HTTP/2) when TLS is enabled.
|
||||
- Updates feeds in the background using an internal scheduler or a traditional cron job.
|
||||
- Sanitizes external content before rendering it.
|
||||
- Enforces a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) that permits only application JavaScript and blocks inline scripts and styles.
|
||||
- Uses native lazy loading for images and iframes.
|
||||
- Compatible only with modern browsers.
|
||||
- Adheres to the [Twelve-Factor App](https://12factor.net/) methodology.
|
||||
- Provides official Debian/RPM packages and pre-built binaries.
|
||||
- Publishes a Docker image to Docker Hub, GitHub Registry, and Quay.io Registry, with ARM architecture support.
|
||||
- Uses a limited amount of third-party go dependencies
|
||||
- Has a comprehensive testsuite, with both unit tests and integration tests.
|
||||
- Only uses a couple of MB of memory and a negligible amount of CPU, even with several hundreds of feeds.
|
||||
- Respects/sends Last-Modified, If-Modified-Since, If-None-Match, Cache-Control, Expires and ETags headers, and has a default polling interval of 1h.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
+74
-4
@@ -180,6 +180,45 @@ func (c *Client) DeleteUser(userID int64) error {
|
||||
return c.request.Delete(fmt.Sprintf("/v1/users/%d", userID))
|
||||
}
|
||||
|
||||
// APIKeys returns all API keys for the authenticated user.
|
||||
func (c *Client) APIKeys() (APIKeys, error) {
|
||||
body, err := c.request.Get("/v1/api-keys")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var apiKeys APIKeys
|
||||
if err := json.NewDecoder(body).Decode(&apiKeys); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return apiKeys, nil
|
||||
}
|
||||
|
||||
// CreateAPIKey creates a new API key for the authenticated user.
|
||||
func (c *Client) CreateAPIKey(description string) (*APIKey, error) {
|
||||
body, err := c.request.Post("/v1/api-keys", &APIKeyCreationRequest{
|
||||
Description: description,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var apiKey *APIKey
|
||||
if err := json.NewDecoder(body).Decode(&apiKey); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return apiKey, nil
|
||||
}
|
||||
|
||||
// DeleteAPIKey removes an API key for the authenticated user.
|
||||
func (c *Client) DeleteAPIKey(apiKeyID int64) error {
|
||||
return c.request.Delete(fmt.Sprintf("/v1/api-keys/%d", apiKeyID))
|
||||
}
|
||||
|
||||
// MarkAllAsRead marks all unread entries as read for a given user.
|
||||
func (c *Client) MarkAllAsRead(userID int64) error {
|
||||
_, err := c.request.Put(fmt.Sprintf("/v1/users/%d/mark-all-as-read", userID), nil)
|
||||
@@ -239,8 +278,8 @@ func (c *Client) Categories() (Categories, error) {
|
||||
|
||||
// CreateCategory creates a new category.
|
||||
func (c *Client) CreateCategory(title string) (*Category, error) {
|
||||
body, err := c.request.Post("/v1/categories", map[string]interface{}{
|
||||
"title": title,
|
||||
body, err := c.request.Post("/v1/categories", &CategoryCreationRequest{
|
||||
Title: title,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -255,10 +294,25 @@ func (c *Client) CreateCategory(title string) (*Category, error) {
|
||||
return category, nil
|
||||
}
|
||||
|
||||
// CreateCategoryWithOptions creates a new category with options.
|
||||
func (c *Client) CreateCategoryWithOptions(createRequest *CategoryCreationRequest) (*Category, error) {
|
||||
body, err := c.request.Post("/v1/categories", createRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var category *Category
|
||||
if err := json.NewDecoder(body).Decode(&category); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
return category, nil
|
||||
}
|
||||
|
||||
// UpdateCategory updates a category.
|
||||
func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, error) {
|
||||
body, err := c.request.Put(fmt.Sprintf("/v1/categories/%d", categoryID), map[string]interface{}{
|
||||
"title": title,
|
||||
body, err := c.request.Put(fmt.Sprintf("/v1/categories/%d", categoryID), &CategoryModificationRequest{
|
||||
Title: SetOptionalField(title),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -273,6 +327,22 @@ func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, erro
|
||||
return category, nil
|
||||
}
|
||||
|
||||
// UpdateCategoryWithOptions updates a category with options.
|
||||
func (c *Client) UpdateCategoryWithOptions(categoryID int64, categoryChanges *CategoryModificationRequest) (*Category, error) {
|
||||
body, err := c.request.Put(fmt.Sprintf("/v1/categories/%d", categoryID), categoryChanges)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
var category *Category
|
||||
if err := json.NewDecoder(body).Decode(&category); err != nil {
|
||||
return nil, fmt.Errorf("miniflux: response error (%v)", err)
|
||||
}
|
||||
|
||||
return category, nil
|
||||
}
|
||||
|
||||
// MarkCategoryAsRead marks all unread entries in a category as read.
|
||||
func (c *Client) MarkCategoryAsRead(categoryID int64) error {
|
||||
_, err := c.request.Put(fmt.Sprintf("/v1/categories/%d/mark-all-as-read", categoryID), nil)
|
||||
|
||||
+108
-59
@@ -17,35 +17,37 @@ const (
|
||||
|
||||
// User represents a user in the system.
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
AlwaysOpenExternalLinks bool `json:"always_open_external_links"`
|
||||
OpenExternalLinksInNewTab bool `json:"open_external_links_in_new_tab"`
|
||||
}
|
||||
|
||||
func (u User) String() string {
|
||||
@@ -63,33 +65,35 @@ 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"`
|
||||
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"`
|
||||
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"`
|
||||
AlwaysOpenExternalLinks *bool `json:"always_open_external_links"`
|
||||
OpenExternalLinksInNewTab *bool `json:"open_external_links_in_new_tab"`
|
||||
}
|
||||
|
||||
// Users represents a list of users.
|
||||
@@ -97,9 +101,12 @@ type Users []User
|
||||
|
||||
// Category represents a feed category.
|
||||
type Category struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
UserID int64 `json:"user_id,omitempty"`
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
UserID int64 `json:"user_id,omitempty"`
|
||||
HideGlobally bool `json:"hide_globally,omitempty"`
|
||||
FeedCount *int `json:"feed_count,omitempty"`
|
||||
TotalUnread *int `json:"total_unread,omitempty"`
|
||||
}
|
||||
|
||||
func (c Category) String() string {
|
||||
@@ -109,6 +116,18 @@ func (c Category) String() string {
|
||||
// Categories represents a list of categories.
|
||||
type Categories []*Category
|
||||
|
||||
// CategoryCreationRequest represents the request to create a category.
|
||||
type CategoryCreationRequest struct {
|
||||
Title string `json:"title"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
}
|
||||
|
||||
// CategoryModificationRequest represents the request to update a category.
|
||||
type CategoryModificationRequest struct {
|
||||
Title *string `json:"title"`
|
||||
HideGlobally *bool `json:"hide_globally"`
|
||||
}
|
||||
|
||||
// Subscription represents a feed subscription.
|
||||
type Subscription struct {
|
||||
Title string `json:"title"`
|
||||
@@ -141,8 +160,11 @@ type Feed struct {
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
ScraperRules string `json:"scraper_rules"`
|
||||
RewriteRules string `json:"rewrite_rules"`
|
||||
UrlRewriteRules string `json:"urlrewrite_rules"`
|
||||
BlocklistRules string `json:"blocklist_rules"`
|
||||
KeeplistRules string `json:"keeplist_rules"`
|
||||
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
||||
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
||||
Crawler bool `json:"crawler"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Cookie string `json:"cookie"`
|
||||
@@ -151,6 +173,7 @@ type Feed struct {
|
||||
Category *Category `json:"category,omitempty"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
DisableHTTP2 bool `json:"disable_http2"`
|
||||
ProxyURL string `json:"proxy_url"`
|
||||
}
|
||||
|
||||
// FeedCreationRequest represents the request to create a feed.
|
||||
@@ -168,10 +191,14 @@ type FeedCreationRequest struct {
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
ScraperRules string `json:"scraper_rules"`
|
||||
RewriteRules string `json:"rewrite_rules"`
|
||||
UrlRewriteRules string `json:"urlrewrite_rules"`
|
||||
BlocklistRules string `json:"blocklist_rules"`
|
||||
KeeplistRules string `json:"keeplist_rules"`
|
||||
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
||||
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
DisableHTTP2 bool `json:"disable_http2"`
|
||||
ProxyURL string `json:"proxy_url"`
|
||||
}
|
||||
|
||||
// FeedModificationRequest represents the request to update a feed.
|
||||
@@ -181,8 +208,11 @@ type FeedModificationRequest struct {
|
||||
Title *string `json:"title"`
|
||||
ScraperRules *string `json:"scraper_rules"`
|
||||
RewriteRules *string `json:"rewrite_rules"`
|
||||
UrlRewriteRules *string `json:"urlrewrite_rules"`
|
||||
BlocklistRules *string `json:"blocklist_rules"`
|
||||
KeeplistRules *string `json:"keeplist_rules"`
|
||||
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
|
||||
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
|
||||
Crawler *bool `json:"crawler"`
|
||||
UserAgent *string `json:"user_agent"`
|
||||
Cookie *string `json:"cookie"`
|
||||
@@ -195,6 +225,7 @@ type FeedModificationRequest struct {
|
||||
FetchViaProxy *bool `json:"fetch_via_proxy"`
|
||||
HideGlobally *bool `json:"hide_globally"`
|
||||
DisableHTTP2 *bool `json:"disable_http2"`
|
||||
ProxyURL *string `json:"proxy_url"`
|
||||
}
|
||||
|
||||
// FeedIcon represents the feed icon.
|
||||
@@ -307,6 +338,24 @@ type VersionResponse struct {
|
||||
OS string `json:"os"`
|
||||
}
|
||||
|
||||
// APIKey represents an application API key.
|
||||
type APIKey struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Token string `json:"token"`
|
||||
Description string `json:"description"`
|
||||
LastUsedAt *time.Time `json:"last_used_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// APIKeys represents a collection of API keys.
|
||||
type APIKeys []*APIKey
|
||||
|
||||
// APIKeyCreationRequest represents the request to create an API key.
|
||||
type APIKeyCreationRequest struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func SetOptionalField[T any](value T) *T {
|
||||
return &value
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ services:
|
||||
# healthcheck:
|
||||
# test: ["CMD", "/usr/bin/miniflux", "-healthcheck", "auto"]
|
||||
db:
|
||||
image: postgres:15
|
||||
image: postgres:latest
|
||||
container_name: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=miniflux
|
||||
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
- ADMIN_PASSWORD=test123
|
||||
- BASE_URL=https://miniflux.example.org
|
||||
db:
|
||||
image: postgres:15
|
||||
image: postgres:latest
|
||||
container_name: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=miniflux
|
||||
|
||||
@@ -37,7 +37,7 @@ services:
|
||||
- "traefik.http.routers.miniflux.entrypoints=websecure"
|
||||
- "traefik.http.routers.miniflux.tls.certresolver=myresolver"
|
||||
db:
|
||||
image: postgres:15
|
||||
image: postgres:latest
|
||||
container_name: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=miniflux
|
||||
|
||||
@@ -1,50 +1,48 @@
|
||||
module miniflux.app/v2
|
||||
|
||||
// +heroku goVersion go1.23
|
||||
// +heroku goVersion go1.24
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.2
|
||||
github.com/andybalholm/brotli v1.1.1
|
||||
github.com/coreos/go-oidc/v3 v3.13.0
|
||||
github.com/go-webauthn/webauthn v0.12.2
|
||||
github.com/PuerkitoBio/goquery v1.10.3
|
||||
github.com/andybalholm/brotli v1.2.0
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/go-webauthn/webauthn v0.13.4
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/prometheus/client_golang v1.21.1
|
||||
github.com/tdewolff/minify/v2 v2.22.4
|
||||
golang.org/x/crypto v0.36.0
|
||||
golang.org/x/image v0.25.0
|
||||
golang.org/x/net v0.38.0
|
||||
golang.org/x/oauth2 v0.28.0
|
||||
golang.org/x/term v0.30.0
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/tdewolff/minify/v2 v2.23.10
|
||||
golang.org/x/crypto v0.40.0
|
||||
golang.org/x/image v0.29.0
|
||||
golang.org/x/net v0.42.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/term v0.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-webauthn/x v0.1.19 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/google/go-tpm v0.9.3 // indirect
|
||||
github.com/go-webauthn/x v0.1.23 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
|
||||
github.com/google/go-tpm v0.9.5 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.7.21 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
)
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
@@ -1,51 +1,50 @@
|
||||
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
|
||||
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-oidc/v3 v3.13.0 h1:M66zd0pcc5VxvBNM4pB331Wrsanby+QomQYjN8HamW8=
|
||||
github.com/coreos/go-oidc/v3 v3.13.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
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.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-webauthn/webauthn v0.12.2 h1:yLaNPgBUEXDQtWnOjhsGhMMCEWbXwjg/aNkC8riJQI8=
|
||||
github.com/go-webauthn/webauthn v0.12.2/go.mod h1:Q8SZPPj4sZ469fNTcQXxRpzJOdb30jQrn/36FX8jilA=
|
||||
github.com/go-webauthn/x v0.1.19 h1:IUfdHiBRoTdujpBA/14qbrMXQ3LGzYe/PRGWdZcmudg=
|
||||
github.com/go-webauthn/x v0.1.19/go.mod h1:C5arLuTQ3pVHKPw89v7CDGnqAZSZJj+4Jnr40dsn7tk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/go-webauthn/webauthn v0.13.4 h1:q68qusWPcqHbg9STSxBLBHnsKaLxNO0RnVKaAqMuAuQ=
|
||||
github.com/go-webauthn/webauthn v0.13.4/go.mod h1:MglN6OH9ECxvhDqoq1wMoF6P6JRYDiQpC9nc5OomQmI=
|
||||
github.com/go-webauthn/x v0.1.23 h1:9lEO0s+g8iTyz5Vszlg/rXTGrx3CjcD0RZQ1GPZCaxI=
|
||||
github.com/go-webauthn/x v0.1.23/go.mod h1:AJd3hI7NfEp/4fI6T4CHD753u91l510lglU7/NMN6+E=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
|
||||
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
|
||||
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/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.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
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/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
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.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
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.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
@@ -54,10 +53,10 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tdewolff/minify/v2 v2.22.4 h1:0/8K2fheOuYr5B4e5oCE1hGBVX6DQHLP0EGzdsDlYeg=
|
||||
github.com/tdewolff/minify/v2 v2.22.4/go.mod h1:K/R8TT7aivpcU8QCNUU1UdR6etfnFPr7L11TO/X7shk=
|
||||
github.com/tdewolff/parse/v2 v2.7.21 h1:OCuPFtGr4mXdnfKikQlUb0n654ROJANhBqCk+wioJ/A=
|
||||
github.com/tdewolff/parse/v2 v2.7.21/go.mod h1:I7TXO37t3aSG9SlPUBefAhgIF8nt7yYUwVGgETIoBcA=
|
||||
github.com/tdewolff/minify/v2 v2.23.10 h1:puzRCH00Im+KDf+PxuuSmJykMTVd8Pp1HzTCxVutNmI=
|
||||
github.com/tdewolff/minify/v2 v2.23.10/go.mod h1:VW3ISUd3gDOZuQ/jwZr4sCzsuX+Qvsx87FDMjk6Rvno=
|
||||
github.com/tdewolff/parse/v2 v2.8.1 h1:J5GSHru6o3jF1uLlEKVXkDxxcVx6yzOlIVIotK4w2po=
|
||||
github.com/tdewolff/parse/v2 v2.8.1/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
|
||||
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
@@ -71,10 +70,10 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
|
||||
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
@@ -89,10 +88,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
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=
|
||||
@@ -111,8 +110,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -122,8 +121,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@@ -133,8 +132,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
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=
|
||||
@@ -142,7 +141,7 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -76,6 +76,9 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
|
||||
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)
|
||||
sr.HandleFunc("/api-keys", handler.createAPIKey).Methods(http.MethodPost)
|
||||
sr.HandleFunc("/api-keys", handler.getAPIKeys).Methods(http.MethodGet)
|
||||
sr.HandleFunc("/api-keys/{apiKeyID}", handler.deleteAPIKey).Methods(http.MethodDelete)
|
||||
}
|
||||
|
||||
func (h *handler) versionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"math/rand/v2"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -729,6 +729,116 @@ func TestRegularUsersCannotUpdateOtherUsers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIKeysEndpoint(t *testing.T) {
|
||||
testConfig := newIntegrationTestConfig()
|
||||
if !testConfig.isConfigured() {
|
||||
t.Skip(skipIntegrationTestsMessage)
|
||||
}
|
||||
|
||||
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
|
||||
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer adminClient.DeleteUser(regularTestUser.ID)
|
||||
|
||||
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
|
||||
apiKeys, err := regularUserClient.APIKeys()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(apiKeys) != 0 {
|
||||
t.Fatalf(`Expected no API keys, got %d`, len(apiKeys))
|
||||
}
|
||||
|
||||
// Create an API key for the user.
|
||||
apiKey, err := regularUserClient.CreateAPIKey("Test API Key")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if apiKey.ID == 0 {
|
||||
t.Fatalf(`Invalid API key ID, got "%v"`, apiKey.ID)
|
||||
}
|
||||
if apiKey.UserID != regularTestUser.ID {
|
||||
t.Fatalf(`Invalid user ID for API key, got "%v" instead of "%v"`, apiKey.UserID, regularTestUser.ID)
|
||||
}
|
||||
if apiKey.Token == "" {
|
||||
t.Fatalf(`Invalid API key token, got "%v"`, apiKey.Token)
|
||||
}
|
||||
if apiKey.Description != "Test API Key" {
|
||||
t.Fatalf(`Invalid API key description, got "%v" instead of "Test API Key"`, apiKey.Description)
|
||||
}
|
||||
|
||||
// Create a duplicate API key with the same description.
|
||||
if _, err := regularUserClient.CreateAPIKey("Test API Key"); err == nil {
|
||||
t.Fatal(`Creating a duplicate API key with the same description should raise an error`)
|
||||
}
|
||||
|
||||
// Fetch the API keys again.
|
||||
apiKeys, err = regularUserClient.APIKeys()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(apiKeys) != 1 {
|
||||
t.Fatalf(`Expected 1 API key, got %d`, len(apiKeys))
|
||||
}
|
||||
if apiKeys[0].ID != apiKey.ID {
|
||||
t.Fatalf(`Invalid API key ID, got "%v" instead of "%v"`, apiKeys[0].ID, apiKey.ID)
|
||||
}
|
||||
if apiKeys[0].UserID != regularTestUser.ID {
|
||||
t.Fatalf(`Invalid user ID for API key, got "%v" instead of "%v"`, apiKeys[0].UserID, regularTestUser.ID)
|
||||
}
|
||||
if apiKeys[0].Token != apiKey.Token {
|
||||
t.Fatalf(`Invalid API key token, got "%v" instead of "%v"`, apiKeys[0].Token, apiKey.Token)
|
||||
}
|
||||
if apiKeys[0].Description != "Test API Key" {
|
||||
t.Fatalf(`Invalid API key description, got "%v" instead of "Test API Key"`, apiKeys[0].Description)
|
||||
}
|
||||
|
||||
// Create a new client using the API key.
|
||||
apiKeyClient := miniflux.NewClient(testConfig.testBaseURL, apiKey.Token)
|
||||
|
||||
// Fetch the user using the API key client.
|
||||
user, err := apiKeyClient.Me()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify the user matches the regular test user.
|
||||
if user.ID != regularTestUser.ID {
|
||||
t.Fatalf(`Expected user ID %d, got %d`, regularTestUser.ID, user.ID)
|
||||
}
|
||||
|
||||
// Delete the API key.
|
||||
if err := regularUserClient.DeleteAPIKey(apiKey.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify the API key is deleted.
|
||||
apiKeys, err = regularUserClient.APIKeys()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(apiKeys) != 0 {
|
||||
t.Fatalf(`Expected no API keys after deletion, got %d`, len(apiKeys))
|
||||
}
|
||||
|
||||
// Try to delete the API key again, it should return an error.
|
||||
err = regularUserClient.DeleteAPIKey(apiKey.ID)
|
||||
if err == nil {
|
||||
t.Fatal(`Deleting a non-existent API key should raise an error`)
|
||||
}
|
||||
if !errors.Is(err, miniflux.ErrNotFound) {
|
||||
t.Fatalf(`Expected "not found" error, got %v`, err)
|
||||
}
|
||||
|
||||
// Try to create an API key with an empty description.
|
||||
if _, err := regularUserClient.CreateAPIKey(""); err == nil {
|
||||
t.Fatal(`Creating an API key with an empty description should raise an error`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkUserAsReadEndpoint(t *testing.T) {
|
||||
testConfig := newIntegrationTestConfig()
|
||||
if !testConfig.isConfigured() {
|
||||
@@ -824,6 +934,10 @@ func TestCreateCategoryEndpoint(t *testing.T) {
|
||||
if category.Title != categoryName {
|
||||
t.Errorf(`Invalid title, got "%v" instead of "%v"`, category.Title, categoryName)
|
||||
}
|
||||
|
||||
if category.HideGlobally {
|
||||
t.Errorf(`Invalid hide globally value, got "%v"`, category.HideGlobally)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCategoryWithEmptyTitle(t *testing.T) {
|
||||
@@ -865,7 +979,49 @@ func TestCannotCreateDuplicatedCategory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCatgoryEndpoint(t *testing.T) {
|
||||
func TestCreateCategoryWithOptions(t *testing.T) {
|
||||
testConfig := newIntegrationTestConfig()
|
||||
if !testConfig.isConfigured() {
|
||||
t.Skip(skipIntegrationTestsMessage)
|
||||
}
|
||||
|
||||
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
|
||||
|
||||
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer adminClient.DeleteUser(regularTestUser.ID)
|
||||
|
||||
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
|
||||
|
||||
newCategory, err := regularUserClient.CreateCategoryWithOptions(&miniflux.CategoryCreationRequest{
|
||||
Title: "My category",
|
||||
HideGlobally: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf(`Creating a category with options should not raise an error: %v`, err)
|
||||
}
|
||||
|
||||
categories, err := regularUserClient.Categories()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, category := range categories {
|
||||
if category.ID == newCategory.ID {
|
||||
if category.Title != newCategory.Title {
|
||||
t.Errorf(`Invalid title, got %q instead of %q`, category.Title, newCategory.Title)
|
||||
}
|
||||
if category.HideGlobally != true {
|
||||
t.Errorf(`Invalid hide globally value, got "%v"`, category.HideGlobally)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCategoryEndpoint(t *testing.T) {
|
||||
testConfig := newIntegrationTestConfig()
|
||||
if !testConfig.isConfigured() {
|
||||
t.Skip(skipIntegrationTestsMessage)
|
||||
@@ -903,6 +1059,91 @@ func TestUpdateCatgoryEndpoint(t *testing.T) {
|
||||
if updatedCategory.Title != "new title" {
|
||||
t.Errorf(`Invalid title, got "%v" instead of "%v"`, updatedCategory.Title, "new title")
|
||||
}
|
||||
|
||||
if updatedCategory.HideGlobally {
|
||||
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCategoryWithOptions(t *testing.T) {
|
||||
testConfig := newIntegrationTestConfig()
|
||||
if !testConfig.isConfigured() {
|
||||
t.Skip(skipIntegrationTestsMessage)
|
||||
}
|
||||
|
||||
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
|
||||
|
||||
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer adminClient.DeleteUser(regularTestUser.ID)
|
||||
|
||||
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
|
||||
|
||||
newCategory, err := regularUserClient.CreateCategoryWithOptions(&miniflux.CategoryCreationRequest{
|
||||
Title: "My category",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf(`Creating a category with options should not raise an error: %v`, err)
|
||||
}
|
||||
|
||||
updatedCategory, err := regularUserClient.UpdateCategoryWithOptions(newCategory.ID, &miniflux.CategoryModificationRequest{
|
||||
Title: miniflux.SetOptionalField("new title"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if updatedCategory.ID != newCategory.ID {
|
||||
t.Errorf(`Invalid categoryID, got "%v"`, updatedCategory.ID)
|
||||
}
|
||||
|
||||
if updatedCategory.Title != "new title" {
|
||||
t.Errorf(`Invalid title, got "%v" instead of "%v"`, updatedCategory.Title, "new title")
|
||||
}
|
||||
|
||||
if updatedCategory.HideGlobally {
|
||||
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
|
||||
}
|
||||
|
||||
updatedCategory, err = regularUserClient.UpdateCategoryWithOptions(newCategory.ID, &miniflux.CategoryModificationRequest{
|
||||
HideGlobally: miniflux.SetOptionalField(true),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if updatedCategory.ID != newCategory.ID {
|
||||
t.Errorf(`Invalid categoryID, got "%v"`, updatedCategory.ID)
|
||||
}
|
||||
|
||||
if updatedCategory.Title != "new title" {
|
||||
t.Errorf(`Invalid title, got "%v" instead of "%v"`, updatedCategory.Title, "new title")
|
||||
}
|
||||
|
||||
if !updatedCategory.HideGlobally {
|
||||
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
|
||||
}
|
||||
|
||||
updatedCategory, err = regularUserClient.UpdateCategoryWithOptions(newCategory.ID, &miniflux.CategoryModificationRequest{
|
||||
HideGlobally: miniflux.SetOptionalField(false),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if updatedCategory.ID != newCategory.ID {
|
||||
t.Errorf(`Invalid categoryID, got %q`, updatedCategory.ID)
|
||||
}
|
||||
|
||||
if updatedCategory.Title != "new title" {
|
||||
t.Errorf(`Invalid title, got %q instead of %q`, updatedCategory.Title, "new title")
|
||||
}
|
||||
|
||||
if updatedCategory.HideGlobally {
|
||||
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateInexistingCategory(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
"miniflux.app/v2/internal/validator"
|
||||
)
|
||||
|
||||
func (h *handler) createAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
|
||||
var apiKeyCreationRequest model.APIKeyCreationRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&apiKeyCreationRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateAPIKeyCreation(h.store, userID, &apiKeyCreationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
apiKey, err := h.store.CreateAPIKey(userID, apiKeyCreationRequest.Description)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.Created(w, r, apiKey)
|
||||
}
|
||||
|
||||
func (h *handler) getAPIKeys(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
apiKeys, err := h.store.APIKeys(userID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
json.OK(w, r, apiKeys)
|
||||
}
|
||||
|
||||
func (h *handler) deleteAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
apiKeyID := request.RouteInt64Param(r, "apiKeyID")
|
||||
|
||||
if err := h.store.DeleteAPIKey(userID, apiKeyID); err != nil {
|
||||
if errors.Is(err, storage.ErrAPIKeyNotFound) {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
json.NoContent(w, r)
|
||||
}
|
||||
+10
-10
@@ -19,18 +19,18 @@ import (
|
||||
func (h *handler) createCategory(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
|
||||
var categoryRequest model.CategoryRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&categoryRequest); err != nil {
|
||||
var categoryCreationRequest model.CategoryCreationRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&categoryCreationRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateCategoryCreation(h.store, userID, &categoryRequest); validationErr != nil {
|
||||
if validationErr := validator.ValidateCategoryCreation(h.store, userID, &categoryCreationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
category, err := h.store.CreateCategory(userID, &categoryRequest)
|
||||
category, err := h.store.CreateCategory(userID, &categoryCreationRequest)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
@@ -54,20 +54,20 @@ func (h *handler) updateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var categoryRequest model.CategoryRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&categoryRequest); err != nil {
|
||||
var categoryModificationRequest model.CategoryModificationRequest
|
||||
if err := json_parser.NewDecoder(r.Body).Decode(&categoryModificationRequest); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if validationErr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryRequest); validationErr != nil {
|
||||
if validationErr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryModificationRequest); validationErr != nil {
|
||||
json.BadRequest(w, r, validationErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
categoryRequest.Patch(category)
|
||||
err = h.store.UpdateCategory(category)
|
||||
if err != nil {
|
||||
categoryModificationRequest.Patch(category)
|
||||
|
||||
if err := h.store.UpdateCategory(category); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
json_parser "encoding/json"
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/model"
|
||||
@@ -33,7 +34,7 @@ func (h *handler) getEnclosureByID(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
enclosure.ProxifyEnclosureURL(h.router)
|
||||
enclosure.ProxifyEnclosureURL(h.router, config.Opts.MediaProxyMode(), config.Opts.MediaProxyResourceTypes())
|
||||
|
||||
json.OK(w, r, enclosure)
|
||||
}
|
||||
|
||||
+14
-2
@@ -10,6 +10,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/integration"
|
||||
@@ -34,8 +35,7 @@ func (h *handler) getEntryFromBuilder(w http.ResponseWriter, r *http.Request, b
|
||||
}
|
||||
|
||||
entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content)
|
||||
|
||||
entry.Enclosures.ProxifyEnclosureURL(h.router)
|
||||
entry.Enclosures.ProxifyEnclosureURL(h.router, config.Opts.MediaProxyMode(), config.Opts.MediaProxyResourceTypes())
|
||||
|
||||
json.OK(w, r, entry)
|
||||
}
|
||||
@@ -331,6 +331,18 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
shouldUpdateContent := request.QueryBoolParam(r, "update_content", false)
|
||||
if shouldUpdateContent {
|
||||
if err := h.store.UpdateEntryTitleAndContent(entry); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
json.OK(w, r, map[string]interface{}{"content": mediaproxy.RewriteDocumentWithRelativeProxyURL(h.router, entry.Content), "reading_time": entry.ReadingTime})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
json.OK(w, r, map[string]string{"content": entry.Content})
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/proxyrotator"
|
||||
"miniflux.app/v2/internal/reader/fetcher"
|
||||
"miniflux.app/v2/internal/reader/subscription"
|
||||
"miniflux.app/v2/internal/validator"
|
||||
@@ -29,24 +30,29 @@ func (h *handler) discoverSubscriptions(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
var rssbridgeURL string
|
||||
var rssbridgeToken string
|
||||
intg, err := h.store.Integration(request.UserID(r))
|
||||
if err == nil && intg != nil && intg.RSSBridgeEnabled {
|
||||
rssbridgeURL = intg.RSSBridgeURL
|
||||
rssbridgeToken = intg.RSSBridgeToken
|
||||
}
|
||||
|
||||
requestBuilder := fetcher.NewRequestBuilder()
|
||||
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
|
||||
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
|
||||
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
|
||||
requestBuilder.WithCustomFeedProxyURL(subscriptionDiscoveryRequest.ProxyURL)
|
||||
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
|
||||
requestBuilder.UseCustomApplicationProxyURL(subscriptionDiscoveryRequest.FetchViaProxy)
|
||||
requestBuilder.WithUserAgent(subscriptionDiscoveryRequest.UserAgent, config.Opts.HTTPClientUserAgent())
|
||||
requestBuilder.WithCookie(subscriptionDiscoveryRequest.Cookie)
|
||||
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,
|
||||
rssbridgeURL,
|
||||
rssbridgeToken,
|
||||
)
|
||||
|
||||
if localizedError != nil {
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
json_parser "encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
"miniflux.app/v2/internal/http/response/json"
|
||||
@@ -84,18 +82,6 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
+51
-30
@@ -13,46 +13,49 @@ import (
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/database"
|
||||
"miniflux.app/v2/internal/proxyrotator"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
"miniflux.app/v2/internal/ui/static"
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const (
|
||||
flagInfoHelp = "Show build information"
|
||||
flagVersionHelp = "Show application version"
|
||||
flagMigrateHelp = "Run SQL migrations"
|
||||
flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
|
||||
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"
|
||||
flagConfigFileHelp = "Load configuration file"
|
||||
flagConfigDumpHelp = "Print parsed configuration values"
|
||||
flagHealthCheckHelp = `Perform a health check on the given endpoint (the value "auto" try to guess the health check endpoint).`
|
||||
flagRefreshFeedsHelp = "Refresh a batch of feeds and exit"
|
||||
flagRunCleanupTasksHelp = "Run cleanup tasks (delete old sessions and archives old entries)"
|
||||
flagExportUserFeedsHelp = "Export user feeds (provide the username as argument)"
|
||||
flagInfoHelp = "Show build information"
|
||||
flagVersionHelp = "Show application version"
|
||||
flagMigrateHelp = "Run SQL migrations"
|
||||
flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
|
||||
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"
|
||||
flagConfigFileHelp = "Load configuration file"
|
||||
flagConfigDumpHelp = "Print parsed configuration values"
|
||||
flagHealthCheckHelp = `Perform a health check on the given endpoint (the value "auto" try to guess the health check endpoint).`
|
||||
flagRefreshFeedsHelp = "Refresh a batch of feeds and exit"
|
||||
flagRunCleanupTasksHelp = "Run cleanup tasks (delete old sessions and archives old entries)"
|
||||
flagExportUserFeedsHelp = "Export user feeds (provide the username as argument)"
|
||||
flagResetNextCheckAtHelp = "Reset the next check time for all feeds"
|
||||
)
|
||||
|
||||
// Parse parses command line arguments.
|
||||
func Parse() {
|
||||
var (
|
||||
err error
|
||||
flagInfo bool
|
||||
flagVersion bool
|
||||
flagMigrate bool
|
||||
flagFlushSessions bool
|
||||
flagCreateAdmin bool
|
||||
flagResetPassword bool
|
||||
flagResetFeedErrors bool
|
||||
flagDebugMode bool
|
||||
flagConfigFile string
|
||||
flagConfigDump bool
|
||||
flagHealthCheck string
|
||||
flagRefreshFeeds bool
|
||||
flagRunCleanupTasks bool
|
||||
flagExportUserFeeds string
|
||||
err error
|
||||
flagInfo bool
|
||||
flagVersion bool
|
||||
flagMigrate bool
|
||||
flagFlushSessions bool
|
||||
flagCreateAdmin bool
|
||||
flagResetPassword bool
|
||||
flagResetFeedErrors bool
|
||||
flagResetFeedNextCheckAt bool
|
||||
flagDebugMode bool
|
||||
flagConfigFile string
|
||||
flagConfigDump bool
|
||||
flagHealthCheck string
|
||||
flagRefreshFeeds bool
|
||||
flagRunCleanupTasks bool
|
||||
flagExportUserFeeds string
|
||||
)
|
||||
|
||||
flag.BoolVar(&flagInfo, "info", false, flagInfoHelp)
|
||||
@@ -64,6 +67,7 @@ func Parse() {
|
||||
flag.BoolVar(&flagCreateAdmin, "create-admin", false, flagCreateAdminHelp)
|
||||
flag.BoolVar(&flagResetPassword, "reset-password", false, flagResetPasswordHelp)
|
||||
flag.BoolVar(&flagResetFeedErrors, "reset-feed-errors", false, flagResetFeedErrorsHelp)
|
||||
flag.BoolVar(&flagResetFeedNextCheckAt, "reset-feed-next-check-at", false, flagResetNextCheckAtHelp)
|
||||
flag.BoolVar(&flagDebugMode, "debug", false, flagDebugModeHelp)
|
||||
flag.StringVar(&flagConfigFile, "config-file", "", flagConfigFileHelp)
|
||||
flag.StringVar(&flagConfigFile, "c", "", flagConfigFileHelp)
|
||||
@@ -189,7 +193,16 @@ func Parse() {
|
||||
}
|
||||
|
||||
if flagResetFeedErrors {
|
||||
store.ResetFeedErrors()
|
||||
if err := store.ResetFeedErrors(); err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if flagResetFeedNextCheckAt {
|
||||
if err := store.ResetNextCheckAt(); err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -228,6 +241,14 @@ func Parse() {
|
||||
createAdminUserFromEnvironmentVariables(store)
|
||||
}
|
||||
|
||||
if config.Opts.HasHTTPClientProxiesConfigured() {
|
||||
slog.Info("Initializing proxy rotation", slog.Int("proxies_count", len(config.Opts.HTTPClientProxies())))
|
||||
proxyrotator.ProxyRotatorInstance, err = proxyrotator.NewProxyRotator(config.Opts.HTTPClientProxies())
|
||||
if err != nil {
|
||||
printErrorAndExit(fmt.Errorf("unable to initialize proxy rotator: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
if flagRefreshFeeds {
|
||||
refreshFeeds(store)
|
||||
return
|
||||
|
||||
+15
-5
@@ -13,7 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
httpd "miniflux.app/v2/internal/http/server"
|
||||
"miniflux.app/v2/internal/http/server"
|
||||
"miniflux.app/v2/internal/metric"
|
||||
"miniflux.app/v2/internal/storage"
|
||||
"miniflux.app/v2/internal/systemd"
|
||||
@@ -33,9 +33,9 @@ func startDaemon(store *storage.Storage) {
|
||||
runScheduler(store, pool)
|
||||
}
|
||||
|
||||
var httpServer *http.Server
|
||||
var httpServers []*http.Server
|
||||
if config.Opts.HasHTTPService() {
|
||||
httpServer = httpd.StartWebServer(store, pool)
|
||||
httpServers = server.StartWebServer(store, pool)
|
||||
}
|
||||
|
||||
if config.Opts.HasMetricsCollector() {
|
||||
@@ -78,8 +78,18 @@ func startDaemon(store *storage.Storage) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if httpServer != nil {
|
||||
httpServer.Shutdown(ctx)
|
||||
if len(httpServers) > 0 {
|
||||
slog.Debug("Shutting down HTTP servers...")
|
||||
for _, server := range httpServers {
|
||||
if server != nil {
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
slog.Error("HTTP server shutdown error", slog.Any("error", err), slog.String("addr", server.Addr))
|
||||
}
|
||||
}
|
||||
}
|
||||
slog.Debug("All HTTP servers shut down.")
|
||||
} else {
|
||||
slog.Debug("No HTTP servers to shut down.")
|
||||
}
|
||||
|
||||
slog.Debug("Process gracefully stopped")
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
func doHealthCheck(healthCheckEndpoint string) {
|
||||
if healthCheckEndpoint == "auto" {
|
||||
healthCheckEndpoint = "http://" + config.Opts.ListenAddr() + config.Opts.BasePath() + "/healthcheck"
|
||||
healthCheckEndpoint = "http://" + config.Opts.ListenAddr()[0] + config.Opts.BasePath() + "/healthcheck"
|
||||
}
|
||||
|
||||
slog.Debug("Executing health check request", slog.String("endpoint", healthCheckEndpoint))
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
package config // import "miniflux.app/v2/internal/config"
|
||||
|
||||
// Opts holds parsed configuration options.
|
||||
var Opts *Options
|
||||
var Opts *options
|
||||
|
||||
+145
-227
@@ -6,6 +6,7 @@ package config // import "miniflux.app/v2/internal/config"
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -184,35 +185,6 @@ func TestLogFormatWithInvalidValue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugModeOn(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("DEBUG", "1")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.LogLevel() != "debug" {
|
||||
t.Fatalf(`Unexpected debug mode value, got %q`, opts.LogLevel())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebugModeOff(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
if opts.LogLevel() != "info" {
|
||||
t.Fatalf(`Unexpected debug mode value, got %q`, opts.LogLevel())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomBaseURL(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("BASE_URL", "http://example.org")
|
||||
@@ -457,18 +429,18 @@ func TestListenAddr(t *testing.T) {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := "foobar"
|
||||
expected := []string{"foobar"}
|
||||
result := opts.ListenAddr()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected LISTEN_ADDR value, got %q instead of %q`, result, expected)
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Fatalf(`Unexpected LISTEN_ADDR value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenAddrWithPortDefined(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PORT", "3000")
|
||||
os.Setenv("LISTEN_ADDR", "foobar")
|
||||
os.Setenv("LISTEN_ADDR", "foobar") // This should be overridden by PORT
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
@@ -476,11 +448,11 @@ func TestListenAddrWithPortDefined(t *testing.T) {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := ":3000"
|
||||
expected := []string{":3000"}
|
||||
result := opts.ListenAddr()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected LISTEN_ADDR value, got %q instead of %q`, result, expected)
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Fatalf(`Unexpected LISTEN_ADDR value when PORT is set, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,11 +465,11 @@ func TestDefaultListenAddrValue(t *testing.T) {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := defaultListenAddr
|
||||
expected := []string{defaultListenAddr}
|
||||
result := opts.ListenAddr()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected LISTEN_ADDR value, got %q instead of %q`, result, expected)
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Fatalf(`Unexpected default LISTEN_ADDR value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1028,6 +1000,41 @@ func TestSchedulerRoundRobin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultSchedulerRoundRobinMaxIntervalValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := defaultSchedulerRoundRobinMaxInterval
|
||||
result := opts.SchedulerRoundRobinMaxInterval()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MAX_INTERVAL value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchedulerRoundRobinMaxInterval(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("SCHEDULER_ROUND_ROBIN_MAX_INTERVAL", "150")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := 150
|
||||
result := opts.SchedulerRoundRobinMaxInterval()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MAX_INTERVAL value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPollingParsingErrorLimit(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("POLLING_PARSING_ERROR_LIMIT", "100")
|
||||
@@ -1431,41 +1438,6 @@ func TestCreateAdmin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPocketConsumerKeyFromEnvVariable(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("POCKET_CONSUMER_KEY", "something")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := "something"
|
||||
result := opts.PocketConsumerKey("default")
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected POCKET_CONSUMER_KEY value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPocketConsumerKeyFromUserPrefs(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := "default"
|
||||
result := opts.PocketConsumerKey("default")
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected POCKET_CONSUMER_KEY value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaProxyMode(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
@@ -1652,147 +1624,6 @@ func TestMediaProxyPrivateKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyImagesOptionForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGES", "all")
|
||||
|
||||
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 PROXY_IMAGES 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 PROXY_IMAGES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
expectedProxyOption := "all"
|
||||
result := opts.MediaProxyMode()
|
||||
if result != expectedProxyOption {
|
||||
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expectedProxyOption)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyImageURLForBackwardCompatibility(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_IMAGE_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_IMAGE_URL value, got %q instead of %q`, result, 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.MediaProxyResourceTypes() {
|
||||
resultMap[mediaType] = true
|
||||
}
|
||||
|
||||
for _, mediaType := range expected {
|
||||
if !resultMap[mediaType] {
|
||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
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 PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
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 := []byte("foobar")
|
||||
result := opts.MediaProxyPrivateKey()
|
||||
if !bytes.Equal(result, expected) {
|
||||
t.Fatalf(`Unexpected PROXY_PRIVATE_KEY value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPSOff(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
@@ -1931,9 +1762,7 @@ func TestParseConfigFile(t *testing.T) {
|
||||
content := []byte(`
|
||||
# This is a comment
|
||||
|
||||
DEBUG = yes
|
||||
|
||||
POCKET_CONSUMER_KEY= >#1234
|
||||
LOG_LEVEL = debug
|
||||
|
||||
Invalid text
|
||||
`)
|
||||
@@ -1956,13 +1785,7 @@ Invalid text
|
||||
}
|
||||
|
||||
if opts.LogLevel() != "debug" {
|
||||
t.Errorf(`Unexpected debug mode value, got %q`, opts.LogLevel())
|
||||
}
|
||||
|
||||
expected := ">#1234"
|
||||
result := opts.PocketConsumerKey("default")
|
||||
if result != expected {
|
||||
t.Errorf(`Unexpected POCKET_CONSUMER_KEY value, got %q instead of %q`, result, expected)
|
||||
t.Errorf(`Unexpected log level value, got %q`, opts.LogLevel())
|
||||
}
|
||||
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
@@ -2134,12 +1957,33 @@ func TestYouTubeApiKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultYouTubeEmbedUrl(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
opts, err := NewParser().ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := "https://www.youtube-nocookie.com/embed/"
|
||||
result := opts.YouTubeEmbedUrlOverride()
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected default value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
|
||||
expected = "www.youtube-nocookie.com"
|
||||
result = opts.YouTubeEmbedDomain()
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected YouTube embed domain, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestYouTubeEmbedUrlOverride(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
opts, err := NewParser().ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
@@ -2150,6 +1994,12 @@ func TestYouTubeEmbedUrlOverride(t *testing.T) {
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected YOUTUBE_EMBED_URL_OVERRIDE value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
|
||||
expected = "invidious.custom"
|
||||
result = opts.YouTubeEmbedDomain()
|
||||
if result != expected {
|
||||
t.Fatalf(`Unexpected YouTube embed domain, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConfigDumpOutput(t *testing.T) {
|
||||
@@ -2186,3 +2036,71 @@ func TestParseConfigDumpOutput(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClientProxies(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("HTTP_CLIENT_PROXIES", "http://proxy1.example.com,http://proxy2.example.com")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := []string{"http://proxy1.example.com", "http://proxy2.example.com"}
|
||||
result := opts.HTTPClientProxies()
|
||||
|
||||
if len(expected) != len(result) {
|
||||
t.Fatalf(`Unexpected HTTP_CLIENT_PROXIES value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
|
||||
for i, proxy := range expected {
|
||||
if result[i] != proxy {
|
||||
t.Fatalf(`Unexpected HTTP_CLIENT_PROXIES value at index %d, got %q instead of %q`, i, result[i], proxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultHTTPClientProxiesValue(t *testing.T) {
|
||||
os.Clearenv()
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := []string{}
|
||||
result := opts.HTTPClientProxies()
|
||||
|
||||
if len(expected) != len(result) {
|
||||
t.Fatalf(`Unexpected default HTTP_CLIENT_PROXIES value, got %v instead of %v`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClientProxy(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("HTTP_CLIENT_PROXY", "http://proxy.example.com")
|
||||
|
||||
parser := NewParser()
|
||||
opts, err := parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
expected := "http://proxy.example.com"
|
||||
if opts.HTTPClientProxyURL() == nil || opts.HTTPClientProxyURL().String() != expected {
|
||||
t.Fatalf(`Unexpected HTTP_CLIENT_PROXY value, got %v instead of %v`, opts.HTTPClientProxyURL(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidHTTPClientProxy(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("HTTP_CLIENT_PROXY", "sche|me://invalid-proxy-url")
|
||||
|
||||
parser := NewParser()
|
||||
_, err := parser.ParseEnvironmentVariables()
|
||||
if err == nil {
|
||||
t.Fatalf(`Expected error for invalid HTTP_CLIENT_PROXY value, but got none`)
|
||||
}
|
||||
}
|
||||
|
||||
+164
-132
@@ -5,7 +5,9 @@ package config // import "miniflux.app/v2/internal/config"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"maps"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -22,8 +24,6 @@ const (
|
||||
defaultHSTS = true
|
||||
defaultHTTPService = true
|
||||
defaultSchedulerService = true
|
||||
defaultDebug = false
|
||||
defaultTiming = false
|
||||
defaultBaseURL = "http://localhost"
|
||||
defaultRootURL = "http://localhost"
|
||||
defaultBasePath = ""
|
||||
@@ -36,6 +36,7 @@ const (
|
||||
defaultSchedulerEntryFrequencyMaxInterval = 24 * 60
|
||||
defaultSchedulerEntryFrequencyFactor = 1
|
||||
defaultSchedulerRoundRobinMinInterval = 60
|
||||
defaultSchedulerRoundRobinMaxInterval = 1440
|
||||
defaultPollingParsingErrorLimit = 3
|
||||
defaultRunMigrations = false
|
||||
defaultDatabaseURL = "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
|
||||
@@ -73,7 +74,6 @@ const (
|
||||
defaultOauth2OidcProviderName = "OpenID Connect"
|
||||
defaultOAuth2Provider = ""
|
||||
defaultDisableLocalAuth = false
|
||||
defaultPocketConsumerKey = ""
|
||||
defaultHTTPClientTimeout = 20
|
||||
defaultHTTPClientMaxBodySize = 15
|
||||
defaultHTTPClientProxy = ""
|
||||
@@ -94,14 +94,14 @@ const (
|
||||
|
||||
var defaultHTTPClientUserAgent = "Mozilla/5.0 (compatible; Miniflux/" + version.Version + "; +https://miniflux.app)"
|
||||
|
||||
// Option contains a key to value map of a single option. It may be used to output debug strings.
|
||||
type Option struct {
|
||||
// option contains a key to value map of a single option. It may be used to output debug strings.
|
||||
type option struct {
|
||||
Key string
|
||||
Value interface{}
|
||||
Value any
|
||||
}
|
||||
|
||||
// Options contains configuration options.
|
||||
type Options struct {
|
||||
// options contains configuration options.
|
||||
type options struct {
|
||||
HTTPS bool
|
||||
logFile string
|
||||
logDateTime bool
|
||||
@@ -110,7 +110,6 @@ type Options struct {
|
||||
hsts bool
|
||||
httpService bool
|
||||
schedulerService bool
|
||||
serverTimingHeader bool
|
||||
baseURL string
|
||||
rootURL string
|
||||
basePath string
|
||||
@@ -119,7 +118,7 @@ type Options struct {
|
||||
databaseMinConns int
|
||||
databaseConnectionLifetime int
|
||||
runMigrations bool
|
||||
listenAddr string
|
||||
listenAddr []string
|
||||
certFile string
|
||||
certDomain string
|
||||
certKeyFile string
|
||||
@@ -136,6 +135,7 @@ type Options struct {
|
||||
schedulerEntryFrequencyMaxInterval int
|
||||
schedulerEntryFrequencyFactor int
|
||||
schedulerRoundRobinMinInterval int
|
||||
schedulerRoundRobinMaxInterval int
|
||||
pollingParsingErrorLimit int
|
||||
workerPoolSize int
|
||||
createAdmin bool
|
||||
@@ -152,6 +152,7 @@ type Options struct {
|
||||
filterEntryMaxAgeDays int
|
||||
youTubeApiKey string
|
||||
youTubeEmbedUrlOverride string
|
||||
youTubeEmbedDomain string
|
||||
oauth2UserCreationAllowed bool
|
||||
oauth2ClientID string
|
||||
oauth2ClientSecret string
|
||||
@@ -160,10 +161,10 @@ type Options struct {
|
||||
oidcProviderName string
|
||||
oauth2Provider string
|
||||
disableLocalAuth bool
|
||||
pocketConsumerKey string
|
||||
httpClientTimeout int
|
||||
httpClientMaxBodySize int64
|
||||
httpClientProxy string
|
||||
httpClientProxyURL *url.URL
|
||||
httpClientProxies []string
|
||||
httpClientUserAgent string
|
||||
httpServerTimeout int
|
||||
authProxyHeader string
|
||||
@@ -182,8 +183,8 @@ type Options struct {
|
||||
}
|
||||
|
||||
// NewOptions returns Options with default values.
|
||||
func NewOptions() *Options {
|
||||
return &Options{
|
||||
func NewOptions() *options {
|
||||
return &options{
|
||||
HTTPS: defaultHTTPS,
|
||||
logFile: defaultLogFile,
|
||||
logDateTime: defaultLogDateTime,
|
||||
@@ -192,7 +193,6 @@ func NewOptions() *Options {
|
||||
hsts: defaultHSTS,
|
||||
httpService: defaultHTTPService,
|
||||
schedulerService: defaultSchedulerService,
|
||||
serverTimingHeader: defaultTiming,
|
||||
baseURL: defaultBaseURL,
|
||||
rootURL: defaultRootURL,
|
||||
basePath: defaultBasePath,
|
||||
@@ -201,7 +201,7 @@ func NewOptions() *Options {
|
||||
databaseMinConns: defaultDatabaseMinConns,
|
||||
databaseConnectionLifetime: defaultDatabaseConnectionLifetime,
|
||||
runMigrations: defaultRunMigrations,
|
||||
listenAddr: defaultListenAddr,
|
||||
listenAddr: []string{defaultListenAddr},
|
||||
certFile: defaultCertFile,
|
||||
certDomain: defaultCertDomain,
|
||||
certKeyFile: defaultKeyFile,
|
||||
@@ -218,6 +218,7 @@ func NewOptions() *Options {
|
||||
schedulerEntryFrequencyMaxInterval: defaultSchedulerEntryFrequencyMaxInterval,
|
||||
schedulerEntryFrequencyFactor: defaultSchedulerEntryFrequencyFactor,
|
||||
schedulerRoundRobinMinInterval: defaultSchedulerRoundRobinMinInterval,
|
||||
schedulerRoundRobinMaxInterval: defaultSchedulerRoundRobinMaxInterval,
|
||||
pollingParsingErrorLimit: defaultPollingParsingErrorLimit,
|
||||
workerPoolSize: defaultWorkerPoolSize,
|
||||
createAdmin: defaultCreateAdmin,
|
||||
@@ -240,10 +241,10 @@ func NewOptions() *Options {
|
||||
oidcProviderName: defaultOauth2OidcProviderName,
|
||||
oauth2Provider: defaultOAuth2Provider,
|
||||
disableLocalAuth: defaultDisableLocalAuth,
|
||||
pocketConsumerKey: defaultPocketConsumerKey,
|
||||
httpClientTimeout: defaultHTTPClientTimeout,
|
||||
httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024,
|
||||
httpClientProxy: defaultHTTPClientProxy,
|
||||
httpClientProxyURL: nil,
|
||||
httpClientProxies: []string{},
|
||||
httpClientUserAgent: defaultHTTPClientUserAgent,
|
||||
httpServerTimeout: defaultHTTPServerTimeout,
|
||||
authProxyHeader: defaultAuthProxyHeader,
|
||||
@@ -262,408 +263,444 @@ func NewOptions() *Options {
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Options) LogFile() string {
|
||||
func (o *options) LogFile() string {
|
||||
return o.logFile
|
||||
}
|
||||
|
||||
// LogDateTime returns true if the date/time should be displayed in log messages.
|
||||
func (o *Options) LogDateTime() bool {
|
||||
func (o *options) LogDateTime() bool {
|
||||
return o.logDateTime
|
||||
}
|
||||
|
||||
// LogFormat returns the log format.
|
||||
func (o *Options) LogFormat() string {
|
||||
func (o *options) LogFormat() string {
|
||||
return o.logFormat
|
||||
}
|
||||
|
||||
// LogLevel returns the log level.
|
||||
func (o *Options) LogLevel() string {
|
||||
func (o *options) LogLevel() string {
|
||||
return o.logLevel
|
||||
}
|
||||
|
||||
// SetLogLevel sets the log level.
|
||||
func (o *Options) SetLogLevel(level string) {
|
||||
func (o *options) SetLogLevel(level string) {
|
||||
o.logLevel = level
|
||||
}
|
||||
|
||||
// HasMaintenanceMode returns true if maintenance mode is enabled.
|
||||
func (o *Options) HasMaintenanceMode() bool {
|
||||
func (o *options) HasMaintenanceMode() bool {
|
||||
return o.maintenanceMode
|
||||
}
|
||||
|
||||
// MaintenanceMessage returns maintenance message.
|
||||
func (o *Options) MaintenanceMessage() string {
|
||||
func (o *options) MaintenanceMessage() string {
|
||||
return o.maintenanceMessage
|
||||
}
|
||||
|
||||
// HasServerTimingHeader returns true if server-timing headers enabled.
|
||||
func (o *Options) HasServerTimingHeader() bool {
|
||||
return o.serverTimingHeader
|
||||
}
|
||||
|
||||
// BaseURL returns the application base URL with path.
|
||||
func (o *Options) BaseURL() string {
|
||||
func (o *options) BaseURL() string {
|
||||
return o.baseURL
|
||||
}
|
||||
|
||||
// RootURL returns the base URL without path.
|
||||
func (o *Options) RootURL() string {
|
||||
func (o *options) RootURL() string {
|
||||
return o.rootURL
|
||||
}
|
||||
|
||||
// BasePath returns the application base path according to the base URL.
|
||||
func (o *Options) BasePath() string {
|
||||
func (o *options) BasePath() string {
|
||||
return o.basePath
|
||||
}
|
||||
|
||||
// IsDefaultDatabaseURL returns true if the default database URL is used.
|
||||
func (o *Options) IsDefaultDatabaseURL() bool {
|
||||
func (o *options) IsDefaultDatabaseURL() bool {
|
||||
return o.databaseURL == defaultDatabaseURL
|
||||
}
|
||||
|
||||
// DatabaseURL returns the database URL.
|
||||
func (o *Options) DatabaseURL() string {
|
||||
func (o *options) DatabaseURL() string {
|
||||
return o.databaseURL
|
||||
}
|
||||
|
||||
// DatabaseMaxConns returns the maximum number of database connections.
|
||||
func (o *Options) DatabaseMaxConns() int {
|
||||
func (o *options) DatabaseMaxConns() int {
|
||||
return o.databaseMaxConns
|
||||
}
|
||||
|
||||
// DatabaseMinConns returns the minimum number of database connections.
|
||||
func (o *Options) DatabaseMinConns() int {
|
||||
func (o *options) DatabaseMinConns() int {
|
||||
return o.databaseMinConns
|
||||
}
|
||||
|
||||
// DatabaseConnectionLifetime returns the maximum amount of time a connection may be reused.
|
||||
func (o *Options) DatabaseConnectionLifetime() time.Duration {
|
||||
func (o *options) DatabaseConnectionLifetime() time.Duration {
|
||||
return time.Duration(o.databaseConnectionLifetime) * time.Minute
|
||||
}
|
||||
|
||||
// ListenAddr returns the listen address for the HTTP server.
|
||||
func (o *Options) ListenAddr() string {
|
||||
func (o *options) ListenAddr() []string {
|
||||
return o.listenAddr
|
||||
}
|
||||
|
||||
// CertFile returns the SSL certificate filename if any.
|
||||
func (o *Options) CertFile() string {
|
||||
func (o *options) CertFile() string {
|
||||
return o.certFile
|
||||
}
|
||||
|
||||
// CertKeyFile returns the private key filename for custom SSL certificate.
|
||||
func (o *Options) CertKeyFile() string {
|
||||
func (o *options) CertKeyFile() string {
|
||||
return o.certKeyFile
|
||||
}
|
||||
|
||||
// CertDomain returns the domain to use for Let's Encrypt certificate.
|
||||
func (o *Options) CertDomain() string {
|
||||
func (o *options) CertDomain() string {
|
||||
return o.certDomain
|
||||
}
|
||||
|
||||
// CleanupFrequencyHours returns the interval in hours for cleanup jobs.
|
||||
func (o *Options) CleanupFrequencyHours() int {
|
||||
func (o *options) CleanupFrequencyHours() int {
|
||||
return o.cleanupFrequencyHours
|
||||
}
|
||||
|
||||
// CleanupArchiveReadDays returns the number of days after which marking read items as removed.
|
||||
func (o *Options) CleanupArchiveReadDays() int {
|
||||
func (o *options) CleanupArchiveReadDays() int {
|
||||
return o.cleanupArchiveReadDays
|
||||
}
|
||||
|
||||
// CleanupArchiveUnreadDays returns the number of days after which marking unread items as removed.
|
||||
func (o *Options) CleanupArchiveUnreadDays() int {
|
||||
func (o *options) CleanupArchiveUnreadDays() int {
|
||||
return o.cleanupArchiveUnreadDays
|
||||
}
|
||||
|
||||
// CleanupArchiveBatchSize returns the number of entries to archive for each interval.
|
||||
func (o *Options) CleanupArchiveBatchSize() int {
|
||||
func (o *options) CleanupArchiveBatchSize() int {
|
||||
return o.cleanupArchiveBatchSize
|
||||
}
|
||||
|
||||
// CleanupRemoveSessionsDays returns the number of days after which to remove sessions.
|
||||
func (o *Options) CleanupRemoveSessionsDays() int {
|
||||
func (o *options) CleanupRemoveSessionsDays() int {
|
||||
return o.cleanupRemoveSessionsDays
|
||||
}
|
||||
|
||||
// WorkerPoolSize returns the number of background worker.
|
||||
func (o *Options) WorkerPoolSize() int {
|
||||
func (o *options) WorkerPoolSize() int {
|
||||
return o.workerPoolSize
|
||||
}
|
||||
|
||||
// PollingFrequency returns the interval to refresh feeds in the background.
|
||||
func (o *Options) PollingFrequency() int {
|
||||
func (o *options) PollingFrequency() int {
|
||||
return o.pollingFrequency
|
||||
}
|
||||
|
||||
// ForceRefreshInterval returns the force refresh interval
|
||||
func (o *Options) ForceRefreshInterval() int {
|
||||
func (o *options) ForceRefreshInterval() int {
|
||||
return o.forceRefreshInterval
|
||||
}
|
||||
|
||||
// BatchSize returns the number of feeds to send for background processing.
|
||||
func (o *Options) BatchSize() int {
|
||||
func (o *options) BatchSize() int {
|
||||
return o.batchSize
|
||||
}
|
||||
|
||||
// PollingScheduler returns the scheduler used for polling feeds.
|
||||
func (o *Options) PollingScheduler() string {
|
||||
func (o *options) PollingScheduler() string {
|
||||
return o.pollingScheduler
|
||||
}
|
||||
|
||||
// SchedulerEntryFrequencyMaxInterval returns the maximum interval in minutes for the entry frequency scheduler.
|
||||
func (o *Options) SchedulerEntryFrequencyMaxInterval() int {
|
||||
func (o *options) SchedulerEntryFrequencyMaxInterval() int {
|
||||
return o.schedulerEntryFrequencyMaxInterval
|
||||
}
|
||||
|
||||
// SchedulerEntryFrequencyMinInterval returns the minimum interval in minutes for the entry frequency scheduler.
|
||||
func (o *Options) SchedulerEntryFrequencyMinInterval() int {
|
||||
func (o *options) SchedulerEntryFrequencyMinInterval() int {
|
||||
return o.schedulerEntryFrequencyMinInterval
|
||||
}
|
||||
|
||||
// SchedulerEntryFrequencyFactor returns the factor for the entry frequency scheduler.
|
||||
func (o *Options) SchedulerEntryFrequencyFactor() int {
|
||||
func (o *options) SchedulerEntryFrequencyFactor() int {
|
||||
return o.schedulerEntryFrequencyFactor
|
||||
}
|
||||
|
||||
func (o *Options) SchedulerRoundRobinMinInterval() int {
|
||||
func (o *options) SchedulerRoundRobinMinInterval() int {
|
||||
return o.schedulerRoundRobinMinInterval
|
||||
}
|
||||
|
||||
func (o *options) SchedulerRoundRobinMaxInterval() int {
|
||||
return o.schedulerRoundRobinMaxInterval
|
||||
}
|
||||
|
||||
// PollingParsingErrorLimit returns the limit of errors when to stop polling.
|
||||
func (o *Options) PollingParsingErrorLimit() int {
|
||||
func (o *options) PollingParsingErrorLimit() int {
|
||||
return o.pollingParsingErrorLimit
|
||||
}
|
||||
|
||||
// IsOAuth2UserCreationAllowed returns true if user creation is allowed for OAuth2 users.
|
||||
func (o *Options) IsOAuth2UserCreationAllowed() bool {
|
||||
func (o *options) IsOAuth2UserCreationAllowed() bool {
|
||||
return o.oauth2UserCreationAllowed
|
||||
}
|
||||
|
||||
// OAuth2ClientID returns the OAuth2 Client ID.
|
||||
func (o *Options) OAuth2ClientID() string {
|
||||
func (o *options) OAuth2ClientID() string {
|
||||
return o.oauth2ClientID
|
||||
}
|
||||
|
||||
// OAuth2ClientSecret returns the OAuth2 client secret.
|
||||
func (o *Options) OAuth2ClientSecret() string {
|
||||
func (o *options) OAuth2ClientSecret() string {
|
||||
return o.oauth2ClientSecret
|
||||
}
|
||||
|
||||
// OAuth2RedirectURL returns the OAuth2 redirect URL.
|
||||
func (o *Options) OAuth2RedirectURL() string {
|
||||
func (o *options) OAuth2RedirectURL() string {
|
||||
return o.oauth2RedirectURL
|
||||
}
|
||||
|
||||
// OIDCDiscoveryEndpoint returns the OAuth2 OIDC discovery endpoint.
|
||||
func (o *Options) OIDCDiscoveryEndpoint() string {
|
||||
func (o *options) OIDCDiscoveryEndpoint() string {
|
||||
return o.oidcDiscoveryEndpoint
|
||||
}
|
||||
|
||||
// OIDCProviderName returns the OAuth2 OIDC provider's display name
|
||||
func (o *Options) OIDCProviderName() string {
|
||||
func (o *options) OIDCProviderName() string {
|
||||
return o.oidcProviderName
|
||||
}
|
||||
|
||||
// OAuth2Provider returns the name of the OAuth2 provider configured.
|
||||
func (o *Options) OAuth2Provider() string {
|
||||
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 {
|
||||
func (o *options) DisableLocalAuth() bool {
|
||||
return o.disableLocalAuth
|
||||
}
|
||||
|
||||
// HasHSTS returns true if HTTP Strict Transport Security is enabled.
|
||||
func (o *Options) HasHSTS() bool {
|
||||
func (o *options) HasHSTS() bool {
|
||||
return o.hsts
|
||||
}
|
||||
|
||||
// RunMigrations returns true if the environment variable RUN_MIGRATIONS is not empty.
|
||||
func (o *Options) RunMigrations() bool {
|
||||
func (o *options) RunMigrations() bool {
|
||||
return o.runMigrations
|
||||
}
|
||||
|
||||
// CreateAdmin returns true if the environment variable CREATE_ADMIN is not empty.
|
||||
func (o *Options) CreateAdmin() bool {
|
||||
func (o *options) CreateAdmin() bool {
|
||||
return o.createAdmin
|
||||
}
|
||||
|
||||
// AdminUsername returns the admin username if defined.
|
||||
func (o *Options) AdminUsername() string {
|
||||
func (o *options) AdminUsername() string {
|
||||
return o.adminUsername
|
||||
}
|
||||
|
||||
// AdminPassword returns the admin password if defined.
|
||||
func (o *Options) AdminPassword() string {
|
||||
func (o *options) AdminPassword() string {
|
||||
return o.adminPassword
|
||||
}
|
||||
|
||||
// FetchYouTubeWatchTime returns true if the YouTube video duration
|
||||
// should be fetched and used as a reading time.
|
||||
func (o *Options) FetchYouTubeWatchTime() bool {
|
||||
func (o *options) FetchYouTubeWatchTime() bool {
|
||||
return o.fetchYouTubeWatchTime
|
||||
}
|
||||
|
||||
// YouTubeApiKey returns the YouTube API key if defined.
|
||||
func (o *Options) YouTubeApiKey() string {
|
||||
func (o *options) YouTubeApiKey() string {
|
||||
return o.youTubeApiKey
|
||||
}
|
||||
|
||||
// YouTubeEmbedUrlOverride returns YouTube URL which will be used for embeds
|
||||
func (o *Options) YouTubeEmbedUrlOverride() string {
|
||||
// YouTubeEmbedUrlOverride returns the YouTube embed URL override if defined.
|
||||
func (o *options) YouTubeEmbedUrlOverride() string {
|
||||
return o.youTubeEmbedUrlOverride
|
||||
}
|
||||
|
||||
// YouTubeEmbedDomain returns the domain used for YouTube embeds.
|
||||
func (o *options) YouTubeEmbedDomain() string {
|
||||
if o.youTubeEmbedDomain != "" {
|
||||
return o.youTubeEmbedDomain
|
||||
}
|
||||
return "www.youtube-nocookie.com"
|
||||
}
|
||||
|
||||
// FetchNebulaWatchTime returns true if the Nebula video duration
|
||||
// should be fetched and used as a reading time.
|
||||
func (o *Options) FetchNebulaWatchTime() bool {
|
||||
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 {
|
||||
func (o *options) FetchOdyseeWatchTime() bool {
|
||||
return o.fetchOdyseeWatchTime
|
||||
}
|
||||
|
||||
// FetchBilibiliWatchTime returns true if the Bilibili video duration
|
||||
// should be fetched and used as a reading time.
|
||||
func (o *Options) FetchBilibiliWatchTime() bool {
|
||||
func (o *options) FetchBilibiliWatchTime() bool {
|
||||
return o.fetchBilibiliWatchTime
|
||||
}
|
||||
|
||||
// MediaProxyMode returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
|
||||
func (o *Options) MediaProxyMode() string {
|
||||
func (o *options) MediaProxyMode() string {
|
||||
return o.mediaProxyMode
|
||||
}
|
||||
|
||||
// MediaProxyResourceTypes returns a slice of resource types to proxy.
|
||||
func (o *Options) MediaProxyResourceTypes() []string {
|
||||
func (o *options) MediaProxyResourceTypes() []string {
|
||||
return o.mediaProxyResourceTypes
|
||||
}
|
||||
|
||||
// MediaCustomProxyURL returns the custom proxy URL for medias.
|
||||
func (o *Options) MediaCustomProxyURL() string {
|
||||
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 {
|
||||
func (o *options) MediaProxyHTTPClientTimeout() int {
|
||||
return o.mediaProxyHTTPClientTimeout
|
||||
}
|
||||
|
||||
// MediaProxyPrivateKey returns the private key used by the media proxy.
|
||||
func (o *Options) MediaProxyPrivateKey() []byte {
|
||||
func (o *options) MediaProxyPrivateKey() []byte {
|
||||
return o.mediaProxyPrivateKey
|
||||
}
|
||||
|
||||
// HasHTTPService returns true if the HTTP service is enabled.
|
||||
func (o *Options) HasHTTPService() bool {
|
||||
func (o *options) HasHTTPService() bool {
|
||||
return o.httpService
|
||||
}
|
||||
|
||||
// HasSchedulerService returns true if the scheduler service is enabled.
|
||||
func (o *Options) HasSchedulerService() bool {
|
||||
func (o *options) HasSchedulerService() bool {
|
||||
return o.schedulerService
|
||||
}
|
||||
|
||||
// PocketConsumerKey returns the Pocket Consumer Key if configured.
|
||||
func (o *Options) PocketConsumerKey(defaultValue string) string {
|
||||
if o.pocketConsumerKey != "" {
|
||||
return o.pocketConsumerKey
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// HTTPClientTimeout returns the time limit in seconds before the HTTP client cancel the request.
|
||||
func (o *Options) HTTPClientTimeout() int {
|
||||
func (o *options) HTTPClientTimeout() int {
|
||||
return o.httpClientTimeout
|
||||
}
|
||||
|
||||
// HTTPClientMaxBodySize returns the number of bytes allowed for the HTTP client to transfer.
|
||||
func (o *Options) HTTPClientMaxBodySize() int64 {
|
||||
func (o *options) HTTPClientMaxBodySize() int64 {
|
||||
return o.httpClientMaxBodySize
|
||||
}
|
||||
|
||||
// HTTPClientProxy returns the proxy URL for HTTP client.
|
||||
func (o *Options) HTTPClientProxy() string {
|
||||
return o.httpClientProxy
|
||||
// HTTPClientProxyURL returns the client HTTP proxy URL if configured.
|
||||
func (o *options) HTTPClientProxyURL() *url.URL {
|
||||
return o.httpClientProxyURL
|
||||
}
|
||||
|
||||
// HasHTTPClientProxyURLConfigured returns true if the client HTTP proxy URL if configured.
|
||||
func (o *options) HasHTTPClientProxyURLConfigured() bool {
|
||||
return o.httpClientProxyURL != nil
|
||||
}
|
||||
|
||||
// HTTPClientProxies returns the list of proxies.
|
||||
func (o *options) HTTPClientProxies() []string {
|
||||
return o.httpClientProxies
|
||||
}
|
||||
|
||||
// HTTPClientProxiesString returns true if the list of rotating proxies are configured.
|
||||
func (o *options) HasHTTPClientProxiesConfigured() bool {
|
||||
return len(o.httpClientProxies) > 0
|
||||
}
|
||||
|
||||
// HTTPServerTimeout returns the time limit in seconds before the HTTP server cancel the request.
|
||||
func (o *Options) HTTPServerTimeout() int {
|
||||
func (o *options) HTTPServerTimeout() int {
|
||||
return o.httpServerTimeout
|
||||
}
|
||||
|
||||
// HasHTTPClientProxyConfigured returns true if the HTTP proxy is configured.
|
||||
func (o *Options) HasHTTPClientProxyConfigured() bool {
|
||||
return o.httpClientProxy != ""
|
||||
}
|
||||
|
||||
// AuthProxyHeader returns an HTTP header name that contains username for
|
||||
// authentication using auth proxy.
|
||||
func (o *Options) AuthProxyHeader() string {
|
||||
func (o *options) AuthProxyHeader() string {
|
||||
return o.authProxyHeader
|
||||
}
|
||||
|
||||
// IsAuthProxyUserCreationAllowed returns true if user creation is allowed for
|
||||
// users authenticated using auth proxy.
|
||||
func (o *Options) IsAuthProxyUserCreationAllowed() bool {
|
||||
func (o *options) IsAuthProxyUserCreationAllowed() bool {
|
||||
return o.authProxyUserCreation
|
||||
}
|
||||
|
||||
// HasMetricsCollector returns true if metrics collection is enabled.
|
||||
func (o *Options) HasMetricsCollector() bool {
|
||||
func (o *options) HasMetricsCollector() bool {
|
||||
return o.metricsCollector
|
||||
}
|
||||
|
||||
// MetricsRefreshInterval returns the refresh interval in seconds.
|
||||
func (o *Options) MetricsRefreshInterval() int {
|
||||
func (o *options) MetricsRefreshInterval() int {
|
||||
return o.metricsRefreshInterval
|
||||
}
|
||||
|
||||
// MetricsAllowedNetworks returns the list of networks allowed to connect to the metrics endpoint.
|
||||
func (o *Options) MetricsAllowedNetworks() []string {
|
||||
func (o *options) MetricsAllowedNetworks() []string {
|
||||
return o.metricsAllowedNetworks
|
||||
}
|
||||
|
||||
func (o *Options) MetricsUsername() string {
|
||||
func (o *options) MetricsUsername() string {
|
||||
return o.metricsUsername
|
||||
}
|
||||
|
||||
func (o *Options) MetricsPassword() string {
|
||||
func (o *options) MetricsPassword() string {
|
||||
return o.metricsPassword
|
||||
}
|
||||
|
||||
// HTTPClientUserAgent returns the global User-Agent header for miniflux.
|
||||
func (o *Options) HTTPClientUserAgent() string {
|
||||
func (o *options) HTTPClientUserAgent() string {
|
||||
return o.httpClientUserAgent
|
||||
}
|
||||
|
||||
// HasWatchdog returns true if the systemd watchdog is enabled.
|
||||
func (o *Options) HasWatchdog() bool {
|
||||
func (o *options) HasWatchdog() bool {
|
||||
return o.watchdog
|
||||
}
|
||||
|
||||
// InvidiousInstance returns the invidious instance used by miniflux
|
||||
func (o *Options) InvidiousInstance() string {
|
||||
func (o *options) InvidiousInstance() string {
|
||||
return o.invidiousInstance
|
||||
}
|
||||
|
||||
// WebAuthn returns true if WebAuthn logins are supported
|
||||
func (o *Options) WebAuthn() bool {
|
||||
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 {
|
||||
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 {
|
||||
func (o *options) SortedOptions(redactSecret bool) []*option {
|
||||
var clientProxyURLRedacted string
|
||||
if o.httpClientProxyURL != nil {
|
||||
if redactSecret {
|
||||
clientProxyURLRedacted = o.httpClientProxyURL.Redacted()
|
||||
} else {
|
||||
clientProxyURLRedacted = o.httpClientProxyURL.String()
|
||||
}
|
||||
}
|
||||
|
||||
var clientProxyURLsRedacted string
|
||||
if len(o.httpClientProxies) > 0 {
|
||||
if redactSecret {
|
||||
var proxyURLs []string
|
||||
for range o.httpClientProxies {
|
||||
proxyURLs = append(proxyURLs, "<redacted>")
|
||||
}
|
||||
clientProxyURLsRedacted = strings.Join(proxyURLs, ",")
|
||||
} else {
|
||||
clientProxyURLsRedacted = strings.Join(o.httpClientProxies, ",")
|
||||
}
|
||||
}
|
||||
|
||||
var mediaProxyPrivateKeyValue string
|
||||
if len(o.mediaProxyPrivateKey) > 0 {
|
||||
mediaProxyPrivateKeyValue = "<binary-data>"
|
||||
}
|
||||
|
||||
var keyValues = map[string]interface{}{
|
||||
"ADMIN_PASSWORD": redactSecretValue(o.adminPassword, redactSecret),
|
||||
"ADMIN_USERNAME": o.adminUsername,
|
||||
@@ -694,14 +731,15 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
||||
"FETCH_BILIBILI_WATCH_TIME": o.fetchBilibiliWatchTime,
|
||||
"HTTPS": o.HTTPS,
|
||||
"HTTP_CLIENT_MAX_BODY_SIZE": o.httpClientMaxBodySize,
|
||||
"HTTP_CLIENT_PROXY": o.httpClientProxy,
|
||||
"HTTP_CLIENT_PROXIES": clientProxyURLsRedacted,
|
||||
"HTTP_CLIENT_PROXY": clientProxyURLRedacted,
|
||||
"HTTP_CLIENT_TIMEOUT": o.httpClientTimeout,
|
||||
"HTTP_CLIENT_USER_AGENT": o.httpClientUserAgent,
|
||||
"HTTP_SERVER_TIMEOUT": o.httpServerTimeout,
|
||||
"HTTP_SERVICE": o.httpService,
|
||||
"INVIDIOUS_INSTANCE": o.invidiousInstance,
|
||||
"KEY_FILE": o.certKeyFile,
|
||||
"LISTEN_ADDR": o.listenAddr,
|
||||
"LISTEN_ADDR": strings.Join(o.listenAddr, ","),
|
||||
"LOG_FILE": o.logFile,
|
||||
"LOG_DATE_TIME": o.logDateTime,
|
||||
"LOG_FORMAT": o.logFormat,
|
||||
@@ -721,7 +759,6 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
||||
"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,
|
||||
@@ -729,7 +766,7 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
||||
"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_PRIVATE_KEY": mediaProxyPrivateKeyValue,
|
||||
"MEDIA_PROXY_CUSTOM_URL": o.mediaProxyCustomURL,
|
||||
"ROOT_URL": o.rootURL,
|
||||
"RUN_MIGRATIONS": o.runMigrations,
|
||||
@@ -737,8 +774,8 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
||||
"SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": o.schedulerEntryFrequencyMinInterval,
|
||||
"SCHEDULER_ENTRY_FREQUENCY_FACTOR": o.schedulerEntryFrequencyFactor,
|
||||
"SCHEDULER_ROUND_ROBIN_MIN_INTERVAL": o.schedulerRoundRobinMinInterval,
|
||||
"SCHEDULER_ROUND_ROBIN_MAX_INTERVAL": o.schedulerRoundRobinMaxInterval,
|
||||
"SCHEDULER_SERVICE": o.schedulerService,
|
||||
"SERVER_TIMING_HEADER": o.serverTimingHeader,
|
||||
"WATCHDOG": o.watchdog,
|
||||
"WORKER_POOL_SIZE": o.workerPoolSize,
|
||||
"YOUTUBE_API_KEY": redactSecretValue(o.youTubeApiKey, redactSecret),
|
||||
@@ -746,20 +783,15 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
||||
"WEBAUTHN": o.webAuthn,
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(keyValues))
|
||||
for key := range keyValues {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var sortedOptions []*Option
|
||||
for _, key := range keys {
|
||||
sortedOptions = append(sortedOptions, &Option{Key: key, Value: keyValues[key]})
|
||||
sortedKeys := slices.Sorted(maps.Keys(keyValues))
|
||||
var sortedOptions = make([]*option, 0, len(sortedKeys))
|
||||
for _, key := range sortedKeys {
|
||||
sortedOptions = append(sortedOptions, &option{Key: key, Value: keyValues[key]})
|
||||
}
|
||||
return sortedOptions
|
||||
}
|
||||
|
||||
func (o *Options) String() string {
|
||||
func (o *options) String() string {
|
||||
var builder strings.Builder
|
||||
|
||||
for _, option := range o.SortedOptions(false) {
|
||||
|
||||
+40
-65
@@ -10,27 +10,26 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Parser handles configuration parsing.
|
||||
type Parser struct {
|
||||
opts *Options
|
||||
// parser handles configuration parsing.
|
||||
type parser struct {
|
||||
opts *options
|
||||
}
|
||||
|
||||
// NewParser returns a new Parser.
|
||||
func NewParser() *Parser {
|
||||
return &Parser{
|
||||
func NewParser() *parser {
|
||||
return &parser{
|
||||
opts: NewOptions(),
|
||||
}
|
||||
}
|
||||
|
||||
// ParseEnvironmentVariables loads configuration values from environment variables.
|
||||
func (p *Parser) ParseEnvironmentVariables() (*Options, error) {
|
||||
func (p *parser) ParseEnvironmentVariables() (*options, error) {
|
||||
err := p.parseLines(os.Environ())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -39,7 +38,7 @@ func (p *Parser) ParseEnvironmentVariables() (*Options, error) {
|
||||
}
|
||||
|
||||
// ParseFile loads configuration values from a local file.
|
||||
func (p *Parser) ParseFile(filename string) (*Options, error) {
|
||||
func (p *parser) ParseFile(filename string) (*options, error) {
|
||||
fp, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -53,7 +52,7 @@ func (p *Parser) ParseFile(filename string) (*Options, error) {
|
||||
return p.opts, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseFileContent(r io.Reader) (lines []string) {
|
||||
func (p *parser) parseFileContent(r io.Reader) (lines []string) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
@@ -64,13 +63,15 @@ func (p *Parser) parseFileContent(r io.Reader) (lines []string) {
|
||||
return lines
|
||||
}
|
||||
|
||||
func (p *Parser) parseLines(lines []string) (err error) {
|
||||
func (p *parser) parseLines(lines []string) (err error) {
|
||||
var port string
|
||||
|
||||
for _, line := range lines {
|
||||
fields := strings.SplitN(line, "=", 2)
|
||||
key := strings.TrimSpace(fields[0])
|
||||
value := strings.TrimSpace(fields[1])
|
||||
for lineNum, line := range lines {
|
||||
key, value, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
return fmt.Errorf("config: unable to parse configuration, invalid format on line %d", lineNum)
|
||||
}
|
||||
key, value = strings.TrimSpace(key), strings.TrimSpace(value)
|
||||
|
||||
switch key {
|
||||
case "LOG_FILE":
|
||||
@@ -87,14 +88,6 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
if parsedValue == "json" || parsedValue == "text" {
|
||||
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"
|
||||
}
|
||||
case "SERVER_TIMING_HEADER":
|
||||
p.opts.serverTimingHeader = parseBool(value, defaultTiming)
|
||||
case "BASE_URL":
|
||||
p.opts.baseURL, p.opts.rootURL, p.opts.basePath, err = parseBaseURL(value)
|
||||
if err != nil {
|
||||
@@ -103,7 +96,7 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
case "PORT":
|
||||
port = value
|
||||
case "LISTEN_ADDR":
|
||||
p.opts.listenAddr = parseString(value, defaultListenAddr)
|
||||
p.opts.listenAddr = parseStringList(value, []string{defaultListenAddr})
|
||||
case "DATABASE_URL":
|
||||
p.opts.databaseURL = parseString(value, defaultDatabaseURL)
|
||||
case "DATABASE_URL_FILE":
|
||||
@@ -160,39 +153,16 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
p.opts.schedulerEntryFrequencyFactor = parseInt(value, defaultSchedulerEntryFrequencyFactor)
|
||||
case "SCHEDULER_ROUND_ROBIN_MIN_INTERVAL":
|
||||
p.opts.schedulerRoundRobinMinInterval = parseInt(value, defaultSchedulerRoundRobinMinInterval)
|
||||
case "SCHEDULER_ROUND_ROBIN_MAX_INTERVAL":
|
||||
p.opts.schedulerRoundRobinMaxInterval = parseInt(value, defaultSchedulerRoundRobinMaxInterval)
|
||||
case "POLLING_PARSING_ERROR_LIMIT":
|
||||
p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit)
|
||||
case "PROXY_IMAGES":
|
||||
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":
|
||||
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":
|
||||
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":
|
||||
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":
|
||||
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":
|
||||
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)
|
||||
if _, err := rand.Read(randomKey); err != nil {
|
||||
return fmt.Errorf("config: unable to generate random key: %w", err)
|
||||
}
|
||||
p.opts.mediaProxyPrivateKey = parseBytes(value, randomKey)
|
||||
case "MEDIA_PROXY_PRIVATE_KEY":
|
||||
randomKey := make([]byte, 16)
|
||||
if _, err := rand.Read(randomKey); err != nil {
|
||||
@@ -211,10 +181,6 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
p.opts.adminPassword = parseString(value, defaultAdminPassword)
|
||||
case "ADMIN_PASSWORD_FILE":
|
||||
p.opts.adminPassword = readSecretFile(value, defaultAdminPassword)
|
||||
case "POCKET_CONSUMER_KEY":
|
||||
p.opts.pocketConsumerKey = parseString(value, defaultPocketConsumerKey)
|
||||
case "POCKET_CONSUMER_KEY_FILE":
|
||||
p.opts.pocketConsumerKey = readSecretFile(value, defaultPocketConsumerKey)
|
||||
case "OAUTH2_USER_CREATION":
|
||||
p.opts.oauth2UserCreationAllowed = parseBool(value, defaultOAuth2UserCreation)
|
||||
case "OAUTH2_CLIENT_ID":
|
||||
@@ -240,7 +206,12 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
case "HTTP_CLIENT_MAX_BODY_SIZE":
|
||||
p.opts.httpClientMaxBodySize = int64(parseInt(value, defaultHTTPClientMaxBodySize) * 1024 * 1024)
|
||||
case "HTTP_CLIENT_PROXY":
|
||||
p.opts.httpClientProxy = parseString(value, defaultHTTPClientProxy)
|
||||
p.opts.httpClientProxyURL, err = url.Parse(parseString(value, defaultHTTPClientProxy))
|
||||
if err != nil {
|
||||
return fmt.Errorf("config: invalid HTTP_CLIENT_PROXY value: %w", err)
|
||||
}
|
||||
case "HTTP_CLIENT_PROXIES":
|
||||
p.opts.httpClientProxies = parseStringList(value, []string{})
|
||||
case "HTTP_CLIENT_USER_AGENT":
|
||||
p.opts.httpClientUserAgent = parseString(value, defaultHTTPClientUserAgent)
|
||||
case "HTTP_SERVER_TIMEOUT":
|
||||
@@ -289,8 +260,15 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
||||
}
|
||||
|
||||
if port != "" {
|
||||
p.opts.listenAddr = ":" + port
|
||||
p.opts.listenAddr = []string{":" + port}
|
||||
}
|
||||
|
||||
youtubeEmbedURL, err := url.Parse(p.opts.youTubeEmbedUrlOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("config: invalid YOUTUBE_EMBED_URL_OVERRIDE value: %w", err)
|
||||
}
|
||||
p.opts.youTubeEmbedDomain = youtubeEmbedURL.Hostname()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -299,9 +277,7 @@ func parseBaseURL(value string) (string, string, string, error) {
|
||||
return defaultBaseURL, defaultRootURL, "", nil
|
||||
}
|
||||
|
||||
if value[len(value)-1:] == "/" {
|
||||
value = value[:len(value)-1]
|
||||
}
|
||||
value = strings.TrimSuffix(value, "/")
|
||||
|
||||
parsedURL, err := url.Parse(value)
|
||||
if err != nil {
|
||||
@@ -357,15 +333,14 @@ func parseStringList(value string, fallback []string) []string {
|
||||
}
|
||||
|
||||
var strList []string
|
||||
strMap := make(map[string]bool)
|
||||
present := make(map[string]bool)
|
||||
|
||||
items := strings.Split(value, ",")
|
||||
for _, item := range items {
|
||||
itemValue := strings.TrimSpace(item)
|
||||
|
||||
if _, found := strMap[itemValue]; !found {
|
||||
strMap[itemValue] = true
|
||||
strList = append(strList, itemValue)
|
||||
for item := range strings.SplitSeq(value, ",") {
|
||||
if itemValue := strings.TrimSpace(item); itemValue != "" {
|
||||
if !present[itemValue] {
|
||||
present[itemValue] = true
|
||||
strList = append(strList, itemValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package config // import "miniflux.app/v2/internal/config"
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -58,3 +59,108 @@ func TestParseIntValue(t *testing.T) {
|
||||
t.Errorf(`Defined variables should returns the specified value`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseListenAddr(t *testing.T) {
|
||||
defaultExpected := []string{defaultListenAddr}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
listenAddr string
|
||||
port string
|
||||
expected []string
|
||||
lines []string // Used for direct lines parsing instead of individual env vars
|
||||
isLineOriented bool // Flag to indicate if we use lines
|
||||
}{
|
||||
{
|
||||
name: "Single LISTEN_ADDR",
|
||||
listenAddr: "127.0.0.1:8080",
|
||||
expected: []string{"127.0.0.1:8080"},
|
||||
},
|
||||
{
|
||||
name: "Multiple LISTEN_ADDR comma-separated",
|
||||
listenAddr: "127.0.0.1:8080,:8081,/tmp/miniflux.sock",
|
||||
expected: []string{"127.0.0.1:8080", ":8081", "/tmp/miniflux.sock"},
|
||||
},
|
||||
{
|
||||
name: "Multiple LISTEN_ADDR with spaces around commas",
|
||||
listenAddr: "127.0.0.1:8080 , :8081",
|
||||
expected: []string{"127.0.0.1:8080", ":8081"},
|
||||
},
|
||||
{
|
||||
name: "Empty LISTEN_ADDR",
|
||||
listenAddr: "",
|
||||
expected: defaultExpected,
|
||||
},
|
||||
{
|
||||
name: "PORT overrides LISTEN_ADDR",
|
||||
listenAddr: "127.0.0.1:8000",
|
||||
port: "8082",
|
||||
expected: []string{":8082"},
|
||||
},
|
||||
{
|
||||
name: "PORT overrides empty LISTEN_ADDR",
|
||||
listenAddr: "",
|
||||
port: "8083",
|
||||
expected: []string{":8083"},
|
||||
},
|
||||
{
|
||||
name: "LISTEN_ADDR with empty segment (comma)",
|
||||
listenAddr: "127.0.0.1:8080,,:8081",
|
||||
expected: []string{"127.0.0.1:8080", ":8081"},
|
||||
},
|
||||
{
|
||||
name: "PORT override with lines parsing",
|
||||
isLineOriented: true,
|
||||
lines: []string{"LISTEN_ADDR=127.0.0.1:8000", "PORT=8082"},
|
||||
expected: []string{":8082"},
|
||||
},
|
||||
{
|
||||
name: "LISTEN_ADDR only with lines parsing (comma)",
|
||||
isLineOriented: true,
|
||||
lines: []string{"LISTEN_ADDR=10.0.0.1:9090,10.0.0.2:9091"},
|
||||
expected: []string{"10.0.0.1:9090", "10.0.0.2:9091"},
|
||||
},
|
||||
{
|
||||
name: "Empty LISTEN_ADDR with lines parsing (default)",
|
||||
isLineOriented: true,
|
||||
lines: []string{"LISTEN_ADDR="},
|
||||
expected: defaultExpected,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := NewParser()
|
||||
var err error
|
||||
|
||||
if tt.isLineOriented {
|
||||
err = parser.parseLines(tt.lines)
|
||||
} else {
|
||||
// Simulate os.Environ() behaviour for individual var testing
|
||||
var envLines []string
|
||||
if tt.listenAddr != "" {
|
||||
envLines = append(envLines, "LISTEN_ADDR="+tt.listenAddr)
|
||||
}
|
||||
if tt.port != "" {
|
||||
envLines = append(envLines, "PORT="+tt.port)
|
||||
}
|
||||
// Add a dummy var if both are empty to avoid empty lines slice if not intended
|
||||
if tt.listenAddr == "" && tt.port == "" && tt.name == "Empty LISTEN_ADDR" {
|
||||
// This case specifically tests empty LISTEN_ADDR resulting in default
|
||||
// So, we pass LISTEN_ADDR=
|
||||
envLines = append(envLines, "LISTEN_ADDR=")
|
||||
}
|
||||
err = parser.parseLines(envLines)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("parseLines() error = %v", err)
|
||||
}
|
||||
|
||||
opts := parser.opts
|
||||
if !reflect.DeepEqual(opts.ListenAddr(), tt.expected) {
|
||||
t.Errorf("ListenAddr() got = %v, want %v", opts.ListenAddr(), tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+10
-15
@@ -8,38 +8,33 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// HashFromBytes returns a SHA-256 checksum of the input.
|
||||
// HashFromBytes returns a non-cryptographic checksum of the input.
|
||||
func HashFromBytes(value []byte) string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256(value))
|
||||
h := fnv.New128a()
|
||||
h.Write(value)
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// Hash returns a SHA-256 checksum of a string.
|
||||
func Hash(value string) string {
|
||||
return HashFromBytes([]byte(value))
|
||||
// SHA256 returns a SHA-256 checksum of a string.
|
||||
func SHA256(value string) string {
|
||||
h := sha256.Sum256([]byte(value))
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
// GenerateRandomBytes returns random bytes.
|
||||
func GenerateRandomBytes(size int) []byte {
|
||||
b := make([]byte, size)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rand.Read(b)
|
||||
return b
|
||||
}
|
||||
|
||||
// GenerateRandomString returns a random string.
|
||||
func GenerateRandomString(size int) string {
|
||||
return base64.URLEncoding.EncodeToString(GenerateRandomBytes(size))
|
||||
}
|
||||
|
||||
// GenerateRandomStringHex returns a random hexadecimal string.
|
||||
func GenerateRandomStringHex(size int) string {
|
||||
return hex.EncodeToString(GenerateRandomBytes(size))
|
||||
|
||||
@@ -14,11 +14,9 @@ func Migrate(db *sql.DB) error {
|
||||
var currentVersion int
|
||||
db.QueryRow(`SELECT version FROM schema_version`).Scan(¤tVersion)
|
||||
|
||||
driver := getDriverStr()
|
||||
slog.Info("Running database migrations",
|
||||
slog.Int("current_version", currentVersion),
|
||||
slog.Int("latest_version", schemaVersion),
|
||||
slog.String("driver", driver),
|
||||
)
|
||||
|
||||
for version := currentVersion; version < schemaVersion; version++ {
|
||||
@@ -29,12 +27,12 @@ func Migrate(db *sql.DB) error {
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
|
||||
if err := migrations[version](tx, driver); err != nil {
|
||||
if err := migrations[version](tx); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`DELETE FROM schema_version`); err != nil {
|
||||
if _, err := tx.Exec(`TRUNCATE schema_version`); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
|
||||
}
|
||||
|
||||
+322
-249
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,3 @@
|
||||
//go:build !sqlite
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
@@ -25,7 +23,3 @@ func NewConnectionPool(dsn string, minConnections, maxConnections int, connectio
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func getDriverStr() string {
|
||||
return "postgresql"
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
//go:build sqlite
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package database // import "miniflux.app/v2/internal/database"
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// NewConnectionPool configures the database connection pool.
|
||||
func NewConnectionPool(dsn string, _, _ int, _ time.Duration) (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite3", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func getDriverStr() string {
|
||||
return "sqlite3"
|
||||
}
|
||||
+204
-372
@@ -9,7 +9,6 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
@@ -20,6 +19,7 @@ import (
|
||||
"miniflux.app/v2/internal/integration"
|
||||
"miniflux.app/v2/internal/mediaproxy"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/proxyrotator"
|
||||
"miniflux.app/v2/internal/reader/fetcher"
|
||||
mff "miniflux.app/v2/internal/reader/handler"
|
||||
mfs "miniflux.app/v2/internal/reader/subscription"
|
||||
@@ -34,178 +34,12 @@ type handler struct {
|
||||
router *mux.Router
|
||||
}
|
||||
|
||||
const (
|
||||
// StreamPrefix is the prefix for astreams (read/starred/reading list and so on)
|
||||
StreamPrefix = "user/-/state/com.google/"
|
||||
// UserStreamPrefix is the user specific prefix for streams (read/starred/reading list and so on)
|
||||
UserStreamPrefix = "user/%d/state/com.google/"
|
||||
// LabelPrefix is the prefix for a label stream
|
||||
LabelPrefix = "user/-/label/"
|
||||
// UserLabelPrefix is the user specific prefix prefix for a label stream
|
||||
UserLabelPrefix = "user/%d/label/"
|
||||
// FeedPrefix is the prefix for a feed stream
|
||||
FeedPrefix = "feed/"
|
||||
// Read is the suffix for read stream
|
||||
Read = "read"
|
||||
// Starred is the suffix for starred stream
|
||||
Starred = "starred"
|
||||
// ReadingList is the suffix for reading list stream
|
||||
ReadingList = "reading-list"
|
||||
// KeptUnread is the suffix for kept unread stream
|
||||
KeptUnread = "kept-unread"
|
||||
// Broadcast is the suffix for broadcast stream
|
||||
Broadcast = "broadcast"
|
||||
// BroadcastFriends is the suffix for broadcast friends stream
|
||||
BroadcastFriends = "broadcast-friends"
|
||||
// Like is the suffix for like stream
|
||||
Like = "like"
|
||||
// EntryIDLong is the long entry id representation
|
||||
EntryIDLong = "tag:google.com,2005:reader/item/%016x"
|
||||
var (
|
||||
errEmptyFeedTitle = errors.New("googlereader: empty feed title")
|
||||
errFeedNotFound = errors.New("googlereader: feed not found")
|
||||
errCategoryNotFound = errors.New("googlereader: category not found")
|
||||
)
|
||||
|
||||
const (
|
||||
// ParamItemIDs - name of the parameter with the item ids
|
||||
ParamItemIDs = "i"
|
||||
// ParamStreamID - name of the parameter containing the stream to be included
|
||||
ParamStreamID = "s"
|
||||
// ParamStreamExcludes - name of the parameter containing streams to be excluded
|
||||
ParamStreamExcludes = "xt"
|
||||
// ParamStreamFilters - name of the parameter containing streams to be included
|
||||
ParamStreamFilters = "it"
|
||||
// ParamStreamMaxItems - name of the parameter containing number of items per page/max items returned
|
||||
ParamStreamMaxItems = "n"
|
||||
// ParamStreamOrder - name of the parameter containing the sort criteria
|
||||
ParamStreamOrder = "r"
|
||||
// ParamStreamStartTime - name of the parameter containing epoch timestamp, filtering items older than
|
||||
ParamStreamStartTime = "ot"
|
||||
// ParamStreamStopTime - name of the parameter containing epoch timestamp, filtering items newer than
|
||||
ParamStreamStopTime = "nt"
|
||||
// ParamTagsRemove - name of the parameter containing tags (streams) to be removed
|
||||
ParamTagsRemove = "r"
|
||||
// ParamTagsAdd - name of the parameter containing tags (streams) to be added
|
||||
ParamTagsAdd = "a"
|
||||
// ParamSubscribeAction - name of the parameter indicating the action to take for subscription/edit
|
||||
ParamSubscribeAction = "ac"
|
||||
// ParamTitle - name of the parameter for the title of the subscription
|
||||
ParamTitle = "t"
|
||||
// ParamQuickAdd - name of the parameter for a URL being quick subscribed to
|
||||
ParamQuickAdd = "quickadd"
|
||||
// ParamDestination - name of the parameter for the new name of a tag
|
||||
ParamDestination = "dest"
|
||||
// ParamContinuation - name of the parameter for callers to pass to receive the next page of results
|
||||
ParamContinuation = "c"
|
||||
)
|
||||
|
||||
// StreamType represents the possible stream types
|
||||
type StreamType int
|
||||
|
||||
const (
|
||||
// NoStream - no stream type
|
||||
NoStream StreamType = iota
|
||||
// ReadStream - read stream type
|
||||
ReadStream
|
||||
// StarredStream - starred stream type
|
||||
StarredStream
|
||||
// ReadingListStream - reading list stream type
|
||||
ReadingListStream
|
||||
// KeptUnreadStream - kept unread stream type
|
||||
KeptUnreadStream
|
||||
// BroadcastStream - broadcast stream type
|
||||
BroadcastStream
|
||||
// BroadcastFriendsStream - broadcast friends stream type
|
||||
BroadcastFriendsStream
|
||||
// LabelStream - label stream type
|
||||
LabelStream
|
||||
// FeedStream - feed stream type
|
||||
FeedStream
|
||||
// LikeStream - like stream type
|
||||
LikeStream
|
||||
)
|
||||
|
||||
// Stream defines a stream type and its ID.
|
||||
type Stream struct {
|
||||
Type StreamType
|
||||
ID string
|
||||
}
|
||||
|
||||
func (s Stream) String() string {
|
||||
return fmt.Sprintf("%v - '%s'", s.Type, s.ID)
|
||||
}
|
||||
|
||||
func (st StreamType) String() string {
|
||||
switch st {
|
||||
case NoStream:
|
||||
return "NoStream"
|
||||
case ReadStream:
|
||||
return "ReadStream"
|
||||
case StarredStream:
|
||||
return "StarredStream"
|
||||
case ReadingListStream:
|
||||
return "ReadingListStream"
|
||||
case KeptUnreadStream:
|
||||
return "KeptUnreadStream"
|
||||
case BroadcastStream:
|
||||
return "BroadcastStream"
|
||||
case BroadcastFriendsStream:
|
||||
return "BroadcastFriendsStream"
|
||||
case LabelStream:
|
||||
return "LabelStream"
|
||||
case FeedStream:
|
||||
return "FeedStream"
|
||||
case LikeStream:
|
||||
return "LikeStream"
|
||||
default:
|
||||
return st.String()
|
||||
}
|
||||
}
|
||||
|
||||
// RequestModifiers are the parsed request parameters.
|
||||
type RequestModifiers struct {
|
||||
ExcludeTargets []Stream
|
||||
FilterTargets []Stream
|
||||
Streams []Stream
|
||||
Count int
|
||||
Offset int
|
||||
SortDirection string
|
||||
StartTime int64
|
||||
StopTime int64
|
||||
ContinuationToken string
|
||||
UserID int64
|
||||
}
|
||||
|
||||
func (r RequestModifiers) String() string {
|
||||
var results []string
|
||||
|
||||
results = append(results, fmt.Sprintf("UserID: %d", r.UserID))
|
||||
|
||||
var streamStr []string
|
||||
for _, s := range r.Streams {
|
||||
streamStr = append(streamStr, s.String())
|
||||
}
|
||||
results = append(results, fmt.Sprintf("Streams: [%s]", strings.Join(streamStr, ", ")))
|
||||
|
||||
var exclusions []string
|
||||
for _, s := range r.ExcludeTargets {
|
||||
exclusions = append(exclusions, s.String())
|
||||
}
|
||||
results = append(results, fmt.Sprintf("Exclusions: [%s]", strings.Join(exclusions, ", ")))
|
||||
|
||||
var filters []string
|
||||
for _, s := range r.FilterTargets {
|
||||
filters = append(filters, s.String())
|
||||
}
|
||||
results = append(results, fmt.Sprintf("Filters: [%s]", strings.Join(filters, ", ")))
|
||||
|
||||
results = append(results, fmt.Sprintf("Count: %d", r.Count))
|
||||
results = append(results, fmt.Sprintf("Offset: %d", r.Offset))
|
||||
results = append(results, fmt.Sprintf("Sort Direction: %s", r.SortDirection))
|
||||
results = append(results, fmt.Sprintf("Continuation Token: %s", r.ContinuationToken))
|
||||
results = append(results, fmt.Sprintf("Start Time: %d", r.StartTime))
|
||||
results = append(results, fmt.Sprintf("Stop Time: %d", r.StopTime))
|
||||
|
||||
return strings.Join(results, "; ")
|
||||
}
|
||||
|
||||
// Serve handles Google Reader API calls.
|
||||
func Serve(router *mux.Router, store *storage.Storage) {
|
||||
handler := &handler{store, router}
|
||||
@@ -227,102 +61,22 @@ func Serve(router *mux.Router, store *storage.Storage) {
|
||||
sr.HandleFunc("/subscription/quickadd", handler.quickAddHandler).Methods(http.MethodPost).Name("QuickAdd")
|
||||
sr.HandleFunc("/stream/items/ids", handler.streamItemIDsHandler).Methods(http.MethodGet).Name("StreamItemIDs")
|
||||
sr.HandleFunc("/stream/items/contents", handler.streamItemContentsHandler).Methods(http.MethodPost).Name("StreamItemsContents")
|
||||
sr.HandleFunc("/mark-all-as-read", handler.markAllAsReadHandler).Methods(http.MethodPost).Name("MarkAllAsRead")
|
||||
sr.PathPrefix("/").HandlerFunc(handler.serveHandler).Methods(http.MethodPost, http.MethodGet).Name("GoogleReaderApiEndpoint")
|
||||
}
|
||||
|
||||
func getStreamFilterModifiers(r *http.Request) (RequestModifiers, error) {
|
||||
userID := request.UserID(r)
|
||||
|
||||
result := RequestModifiers{
|
||||
SortDirection: "desc",
|
||||
UserID: userID,
|
||||
}
|
||||
streamOrder := request.QueryStringParam(r, ParamStreamOrder, "d")
|
||||
if streamOrder == "o" {
|
||||
result.SortDirection = "asc"
|
||||
}
|
||||
var err error
|
||||
result.Streams, err = getStreams(request.QueryStringParamList(r, ParamStreamID), userID)
|
||||
if err != nil {
|
||||
return RequestModifiers{}, err
|
||||
}
|
||||
result.ExcludeTargets, err = getStreams(request.QueryStringParamList(r, ParamStreamExcludes), userID)
|
||||
if err != nil {
|
||||
return RequestModifiers{}, err
|
||||
}
|
||||
|
||||
result.FilterTargets, err = getStreams(request.QueryStringParamList(r, ParamStreamFilters), userID)
|
||||
if err != nil {
|
||||
return RequestModifiers{}, err
|
||||
}
|
||||
|
||||
result.Count = request.QueryIntParam(r, ParamStreamMaxItems, 0)
|
||||
result.Offset = request.QueryIntParam(r, ParamContinuation, 0)
|
||||
result.StartTime = request.QueryInt64Param(r, ParamStreamStartTime, int64(0))
|
||||
result.StopTime = request.QueryInt64Param(r, ParamStreamStopTime, int64(0))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getStream(streamID string, userID int64) (Stream, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(streamID, FeedPrefix):
|
||||
return Stream{Type: FeedStream, ID: strings.TrimPrefix(streamID, FeedPrefix)}, nil
|
||||
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 {
|
||||
case Read:
|
||||
return Stream{ReadStream, ""}, nil
|
||||
case Starred:
|
||||
return Stream{StarredStream, ""}, nil
|
||||
case ReadingList:
|
||||
return Stream{ReadingListStream, ""}, nil
|
||||
case KeptUnread:
|
||||
return Stream{KeptUnreadStream, ""}, nil
|
||||
case Broadcast:
|
||||
return Stream{BroadcastStream, ""}, nil
|
||||
case BroadcastFriends:
|
||||
return Stream{BroadcastFriendsStream, ""}, nil
|
||||
case Like:
|
||||
return Stream{LikeStream, ""}, nil
|
||||
default:
|
||||
return Stream{NoStream, ""}, fmt.Errorf("googlereader: unknown stream with id: %s", id)
|
||||
}
|
||||
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
|
||||
case streamID == "":
|
||||
return Stream{NoStream, ""}, nil
|
||||
default:
|
||||
return Stream{NoStream, ""}, fmt.Errorf("googlereader: unknown stream type: %s", streamID)
|
||||
}
|
||||
}
|
||||
|
||||
func getStreams(streamIDs []string, userID int64) ([]Stream, error) {
|
||||
streams := make([]Stream, 0)
|
||||
for _, streamID := range streamIDs {
|
||||
stream, err := getStream(streamID, userID)
|
||||
if err != nil {
|
||||
return []Stream{}, err
|
||||
}
|
||||
streams = append(streams, stream)
|
||||
}
|
||||
return streams, nil
|
||||
}
|
||||
|
||||
func checkAndSimplifyTags(addTags []Stream, removeTags []Stream) (map[StreamType]bool, error) {
|
||||
tags := make(map[StreamType]bool)
|
||||
for _, s := range addTags {
|
||||
switch s.Type {
|
||||
case ReadStream:
|
||||
if _, ok := tags[KeptUnreadStream]; ok {
|
||||
return nil, fmt.Errorf("googlereader: %s ad %s should not be supplied simultaneously", KeptUnread, Read)
|
||||
return nil, fmt.Errorf("googlereader: %s and %s should not be supplied simultaneously", keptUnreadStreamSuffix, readStreamSuffix)
|
||||
}
|
||||
tags[ReadStream] = true
|
||||
case KeptUnreadStream:
|
||||
if _, ok := tags[ReadStream]; ok {
|
||||
return nil, fmt.Errorf("googlereader: %s ad %s should not be supplied simultaneously", KeptUnread, Read)
|
||||
return nil, fmt.Errorf("googlereader: %s and %s should not be supplied simultaneously", keptUnreadStreamSuffix, readStreamSuffix)
|
||||
}
|
||||
tags[ReadStream] = false
|
||||
case StarredStream:
|
||||
@@ -337,17 +91,17 @@ func checkAndSimplifyTags(addTags []Stream, removeTags []Stream) (map[StreamType
|
||||
switch s.Type {
|
||||
case ReadStream:
|
||||
if _, ok := tags[ReadStream]; ok {
|
||||
return nil, fmt.Errorf("googlereader: %s ad %s should not be supplied simultaneously", KeptUnread, Read)
|
||||
return nil, fmt.Errorf("googlereader: %s and %s should not be supplied simultaneously", keptUnreadStreamSuffix, readStreamSuffix)
|
||||
}
|
||||
tags[ReadStream] = false
|
||||
case KeptUnreadStream:
|
||||
if _, ok := tags[ReadStream]; ok {
|
||||
return nil, fmt.Errorf("googlereader: %s ad %s should not be supplied simultaneously", KeptUnread, Read)
|
||||
return nil, fmt.Errorf("googlereader: %s and %s should not be supplied simultaneously", keptUnreadStreamSuffix, readStreamSuffix)
|
||||
}
|
||||
tags[ReadStream] = true
|
||||
case StarredStream:
|
||||
if _, ok := tags[StarredStream]; ok {
|
||||
return nil, fmt.Errorf("googlereader: %s should not be supplied for add and remove simultaneously", Starred)
|
||||
return nil, fmt.Errorf("googlereader: %s should not be supplied for add and remove simultaneously", starredStreamSuffix)
|
||||
}
|
||||
tags[StarredStream] = false
|
||||
case BroadcastStream, LikeStream:
|
||||
@@ -360,28 +114,6 @@ func checkAndSimplifyTags(addTags []Stream, removeTags []Stream) (map[StreamType
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func getItemIDs(r *http.Request) ([]int64, error) {
|
||||
items := r.Form[ParamItemIDs]
|
||||
if len(items) == 0 {
|
||||
return nil, fmt.Errorf("googlereader: no items requested")
|
||||
}
|
||||
|
||||
itemIDs := make([]int64, len(items))
|
||||
|
||||
for i, item := range items {
|
||||
var itemID int64
|
||||
_, err := fmt.Sscanf(item, EntryIDLong, &itemID)
|
||||
if err != nil {
|
||||
itemID, err = strconv.ParseInt(item, 16, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("googlereader: could not parse item: %v", item)
|
||||
}
|
||||
}
|
||||
itemIDs[i] = itemID
|
||||
}
|
||||
return itemIDs, nil
|
||||
}
|
||||
|
||||
func checkOutputFormat(r *http.Request) error {
|
||||
var output string
|
||||
if r.Method == http.MethodPost {
|
||||
@@ -468,7 +200,7 @@ func (h *handler) clientLoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
slog.String("username", username),
|
||||
)
|
||||
|
||||
result := login{SID: token, LSID: token, Auth: token}
|
||||
result := loginResponse{SID: token, LSID: token, Auth: token}
|
||||
if output == "json" {
|
||||
json.OK(w, r, result)
|
||||
return
|
||||
@@ -537,12 +269,12 @@ func (h *handler) editTagHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
addTags, err := getStreams(r.PostForm[ParamTagsAdd], userID)
|
||||
addTags, err := getStreams(r.PostForm[paramTagsAdd], userID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
removeTags, err := getStreams(r.PostForm[ParamTagsRemove], userID)
|
||||
removeTags, err := getStreams(r.PostForm[paramTagsRemove], userID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
@@ -558,9 +290,9 @@ func (h *handler) editTagHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
itemIDs, err := getItemIDs(r)
|
||||
itemIDs, err := parseItemIDsFromRequest(r)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -655,7 +387,7 @@ func (h *handler) editTagHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
OK(w, r)
|
||||
sendOkayResponse(w)
|
||||
}
|
||||
|
||||
func (h *handler) quickAddHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -675,7 +407,7 @@ func (h *handler) quickAddHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
feedURL := r.Form.Get(ParamQuickAdd)
|
||||
feedURL := r.Form.Get(paramQuickAdd)
|
||||
if !validator.IsValidURL(feedURL) {
|
||||
json.BadRequest(w, r, fmt.Errorf("googlereader: invalid URL: %s", feedURL))
|
||||
return
|
||||
@@ -683,14 +415,16 @@ func (h *handler) quickAddHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
requestBuilder := fetcher.NewRequestBuilder()
|
||||
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
|
||||
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
|
||||
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
|
||||
|
||||
var rssBridgeURL string
|
||||
var rssBridgeToken string
|
||||
if intg, err := h.store.Integration(userID); err == nil && intg != nil && intg.RSSBridgeEnabled {
|
||||
rssBridgeURL = intg.RSSBridgeURL
|
||||
rssBridgeToken = intg.RSSBridgeToken
|
||||
}
|
||||
|
||||
subscriptions, localizedError := mfs.NewSubscriptionFinder(requestBuilder).FindSubscriptions(feedURL, rssBridgeURL)
|
||||
subscriptions, localizedError := mfs.NewSubscriptionFinder(requestBuilder).FindSubscriptions(feedURL, rssBridgeURL, rssBridgeToken)
|
||||
if localizedError != nil {
|
||||
json.ServerError(w, r, localizedError.Error())
|
||||
return
|
||||
@@ -722,7 +456,7 @@ func (h *handler) quickAddHandler(w http.ResponseWriter, r *http.Request) {
|
||||
json.OK(w, r, quickAddResponse{
|
||||
NumResults: 1,
|
||||
Query: newFeed.FeedURL,
|
||||
StreamID: fmt.Sprintf(FeedPrefix+"%d", newFeed.ID),
|
||||
StreamID: fmt.Sprintf(feedPrefix+"%d", newFeed.ID),
|
||||
StreamName: newFeed.Title,
|
||||
})
|
||||
}
|
||||
@@ -735,17 +469,16 @@ func getFeed(stream Stream, store *storage.Storage, userID int64) (*model.Feed,
|
||||
return store.FeedByID(userID, feedID)
|
||||
}
|
||||
|
||||
func getOrCreateCategory(category Stream, store *storage.Storage, userID int64) (*model.Category, error) {
|
||||
func getOrCreateCategory(streamCategory Stream, store *storage.Storage, userID int64) (*model.Category, error) {
|
||||
switch {
|
||||
case category.ID == "":
|
||||
case streamCategory.ID == "":
|
||||
return store.FirstCategory(userID)
|
||||
case store.CategoryTitleExists(userID, category.ID):
|
||||
return store.CategoryByTitle(userID, category.ID)
|
||||
case store.CategoryTitleExists(userID, streamCategory.ID):
|
||||
return store.CategoryByTitle(userID, streamCategory.ID)
|
||||
default:
|
||||
catRequest := model.CategoryRequest{
|
||||
Title: category.ID,
|
||||
}
|
||||
return store.CreateCategory(userID, &catRequest)
|
||||
return store.CreateCategory(userID, &model.CategoryCreationRequest{
|
||||
Title: streamCategory.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,14 +529,25 @@ func unsubscribe(streams []Stream, store *storage.Storage, userID int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func rename(stream Stream, title string, store *storage.Storage, userID int64) error {
|
||||
func rename(feedStream Stream, title string, store *storage.Storage, userID int64) error {
|
||||
slog.Debug("[GoogleReader] Renaming feed",
|
||||
slog.Int64("user_id", userID),
|
||||
slog.Any("feed_stream", feedStream),
|
||||
slog.String("new_title", title),
|
||||
)
|
||||
|
||||
if title == "" {
|
||||
return errors.New("empty title")
|
||||
return errEmptyFeedTitle
|
||||
}
|
||||
feed, err := getFeed(stream, store, userID)
|
||||
|
||||
feed, err := getFeed(feedStream, store, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if feed == nil {
|
||||
return errFeedNotFound
|
||||
}
|
||||
|
||||
feedModification := model.FeedModificationRequest{
|
||||
Title: &title,
|
||||
}
|
||||
@@ -811,15 +555,29 @@ func rename(stream Stream, title string, store *storage.Storage, userID int64) e
|
||||
return store.UpdateFeed(feed)
|
||||
}
|
||||
|
||||
func move(stream Stream, destination Stream, store *storage.Storage, userID int64) error {
|
||||
feed, err := getFeed(stream, store, userID)
|
||||
func move(feedStream Stream, labelStream Stream, store *storage.Storage, userID int64) error {
|
||||
slog.Debug("[GoogleReader] Moving feed",
|
||||
slog.Int64("user_id", userID),
|
||||
slog.Any("feed_stream", feedStream),
|
||||
slog.Any("label_stream", labelStream),
|
||||
)
|
||||
|
||||
feed, err := getFeed(feedStream, store, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
category, err := getOrCreateCategory(destination, store, userID)
|
||||
if feed == nil {
|
||||
return errFeedNotFound
|
||||
}
|
||||
|
||||
category, err := getOrCreateCategory(labelStream, store, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if category == nil {
|
||||
return errCategoryNotFound
|
||||
}
|
||||
|
||||
feedModification := model.FeedModificationRequest{
|
||||
CategoryID: &category.ID,
|
||||
}
|
||||
@@ -851,20 +609,20 @@ func (h *handler) editSubscriptionHandler(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
streamIds, err := getStreams(r.Form[ParamStreamID], userID)
|
||||
streamIds, err := getStreams(r.Form[paramStreamID], userID)
|
||||
if err != nil || len(streamIds) == 0 {
|
||||
json.BadRequest(w, r, errors.New("googlereader: no valid stream IDs provided"))
|
||||
return
|
||||
}
|
||||
|
||||
newLabel, err := getStream(r.Form.Get(ParamTagsAdd), userID)
|
||||
newLabel, err := getStream(r.Form.Get(paramTagsAdd), userID)
|
||||
if err != nil {
|
||||
json.BadRequest(w, r, fmt.Errorf("googlereader: invalid data in %s", ParamTagsAdd))
|
||||
json.BadRequest(w, r, fmt.Errorf("googlereader: invalid data in %s", paramTagsAdd))
|
||||
return
|
||||
}
|
||||
|
||||
title := r.Form.Get(ParamTitle)
|
||||
action := r.Form.Get(ParamSubscribeAction)
|
||||
title := r.Form.Get(paramTitle)
|
||||
action := r.Form.Get(paramSubscribeAction)
|
||||
|
||||
switch action {
|
||||
case "subscribe":
|
||||
@@ -882,32 +640,41 @@ func (h *handler) editSubscriptionHandler(w http.ResponseWriter, r *http.Request
|
||||
case "edit":
|
||||
if title != "" {
|
||||
if err := rename(streamIds[0], title, h.store, userID); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
if errors.Is(err, errFeedNotFound) || errors.Is(err, errEmptyFeedTitle) {
|
||||
json.BadRequest(w, r, err)
|
||||
} else {
|
||||
json.ServerError(w, r, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if r.Form.Has(ParamTagsAdd) {
|
||||
if r.Form.Has(paramTagsAdd) {
|
||||
if newLabel.Type != LabelStream {
|
||||
json.BadRequest(w, r, errors.New("destination must be a label"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := move(streamIds[0], newLabel, h.store, userID); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
if errors.Is(err, errFeedNotFound) || errors.Is(err, errCategoryNotFound) {
|
||||
json.BadRequest(w, r, err)
|
||||
} else {
|
||||
json.ServerError(w, r, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
default:
|
||||
json.ServerError(w, r, fmt.Errorf("googlereader: unrecognized action %s", action))
|
||||
json.BadRequest(w, r, fmt.Errorf("googlereader: unrecognized action %s", action))
|
||||
return
|
||||
}
|
||||
|
||||
OK(w, r)
|
||||
sendOkayResponse(w)
|
||||
}
|
||||
|
||||
func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
userName := request.UserName(r)
|
||||
clientIP := request.ClientIP(r)
|
||||
|
||||
slog.Debug("[GoogleReader] Handle /stream/items/contents",
|
||||
@@ -927,25 +694,20 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
var user *model.User
|
||||
if user, err = h.store.UserByID(userID); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
requestModifiers, err := getStreamFilterModifiers(r)
|
||||
requestModifiers, err := parseStreamFilterFromRequest(r)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
userReadingList := fmt.Sprintf(UserStreamPrefix, userID) + ReadingList
|
||||
userRead := fmt.Sprintf(UserStreamPrefix, userID) + Read
|
||||
userStarred := fmt.Sprintf(UserStreamPrefix, userID) + Starred
|
||||
userReadingList := fmt.Sprintf(userStreamPrefix, userID) + readingListStreamSuffix
|
||||
userRead := fmt.Sprintf(userStreamPrefix, userID) + readStreamSuffix
|
||||
userStarred := fmt.Sprintf(userStreamPrefix, userID) + starredStreamSuffix
|
||||
|
||||
itemIDs, err := getItemIDs(r)
|
||||
itemIDs, err := parseItemIDsFromRequest(r)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -969,28 +731,17 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
json.BadRequest(w, r, fmt.Errorf("googlereader: no items returned from the database for item IDs: %v", itemIDs))
|
||||
return
|
||||
}
|
||||
|
||||
result := streamContentItems{
|
||||
result := streamContentItemsResponse{
|
||||
Direction: "ltr",
|
||||
ID: fmt.Sprintf("feed/%d", entries[0].FeedID),
|
||||
Title: entries[0].Feed.Title,
|
||||
Alternate: []contentHREFType{
|
||||
{
|
||||
HREF: entries[0].Feed.SiteURL,
|
||||
Type: "text/html",
|
||||
},
|
||||
},
|
||||
Updated: time.Now().Unix(),
|
||||
ID: "user/-/state/com.google/reading-list",
|
||||
Title: "Reading List",
|
||||
Updated: time.Now().Unix(),
|
||||
Self: []contentHREF{
|
||||
{
|
||||
HREF: config.Opts.RootURL() + route.Path(h.router, "StreamItemsContents"),
|
||||
},
|
||||
},
|
||||
Author: user.Username,
|
||||
Author: userName,
|
||||
}
|
||||
contentItems := make([]contentItem, len(entries))
|
||||
for i, entry := range entries {
|
||||
@@ -1001,7 +752,7 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque
|
||||
categories := make([]string, 0)
|
||||
categories = append(categories, userReadingList)
|
||||
if entry.Feed.Category.Title != "" {
|
||||
categories = append(categories, fmt.Sprintf(UserLabelPrefix, userID)+entry.Feed.Category.Title)
|
||||
categories = append(categories, fmt.Sprintf(userLabelPrefix, userID)+entry.Feed.Category.Title)
|
||||
}
|
||||
if entry.Status == model.EntryStatusRead {
|
||||
categories = append(categories, userRead)
|
||||
@@ -1012,11 +763,10 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content)
|
||||
|
||||
entry.Enclosures.ProxifyEnclosureURL(h.router)
|
||||
entry.Enclosures.ProxifyEnclosureURL(h.router, config.Opts.MediaProxyMode(), config.Opts.MediaProxyResourceTypes())
|
||||
|
||||
contentItems[i] = contentItem{
|
||||
ID: fmt.Sprintf(EntryIDLong, entry.ID),
|
||||
ID: convertEntryIDToLongFormItemID(entry.ID),
|
||||
Title: entry.Title,
|
||||
Author: entry.Author,
|
||||
TimestampUsec: fmt.Sprintf("%d", entry.Date.UnixMicro()),
|
||||
@@ -1072,9 +822,9 @@ func (h *handler) disableTagHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
streams, err := getStreams(r.Form[ParamStreamID], userID)
|
||||
streams, err := getStreams(r.Form[paramStreamID], userID)
|
||||
if err != nil {
|
||||
json.BadRequest(w, r, fmt.Errorf("googlereader: invalid data in %s", ParamStreamID))
|
||||
json.BadRequest(w, r, fmt.Errorf("googlereader: invalid data in %s", paramStreamID))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1093,7 +843,7 @@ func (h *handler) disableTagHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
OK(w, r)
|
||||
sendOkayResponse(w)
|
||||
}
|
||||
|
||||
func (h *handler) renameTagHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1112,15 +862,15 @@ func (h *handler) renameTagHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
source, err := getStream(r.Form.Get(ParamStreamID), userID)
|
||||
source, err := getStream(r.Form.Get(paramStreamID), userID)
|
||||
if err != nil {
|
||||
json.BadRequest(w, r, fmt.Errorf("googlereader: invalid data in %s", ParamStreamID))
|
||||
json.BadRequest(w, r, fmt.Errorf("googlereader: invalid data in %s", paramStreamID))
|
||||
return
|
||||
}
|
||||
|
||||
destination, err := getStream(r.Form.Get(ParamDestination), userID)
|
||||
destination, err := getStream(r.Form.Get(paramDestination), userID)
|
||||
if err != nil {
|
||||
json.BadRequest(w, r, fmt.Errorf("googlereader: invalid data in %s", ParamDestination))
|
||||
json.BadRequest(w, r, fmt.Errorf("googlereader: invalid data in %s", paramDestination))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1143,21 +893,24 @@ func (h *handler) renameTagHandler(w http.ResponseWriter, r *http.Request) {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
categoryRequest := model.CategoryRequest{
|
||||
Title: destination.ID,
|
||||
|
||||
categoryModificationRequest := model.CategoryModificationRequest{
|
||||
Title: model.SetOptionalField(destination.ID),
|
||||
}
|
||||
verr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryRequest)
|
||||
if verr != nil {
|
||||
json.BadRequest(w, r, verr.Error())
|
||||
|
||||
if validationError := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryModificationRequest); validationError != nil {
|
||||
json.BadRequest(w, r, validationError.Error())
|
||||
return
|
||||
}
|
||||
categoryRequest.Patch(category)
|
||||
err = h.store.UpdateCategory(category)
|
||||
if err != nil {
|
||||
|
||||
categoryModificationRequest.Patch(category)
|
||||
|
||||
if err := h.store.UpdateCategory(category); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
OK(w, r)
|
||||
|
||||
sendOkayResponse(w)
|
||||
}
|
||||
|
||||
func (h *handler) tagListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1181,13 +934,13 @@ func (h *handler) tagListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
result.Tags = make([]subscriptionCategory, 0)
|
||||
result.Tags = append(result.Tags, subscriptionCategory{
|
||||
ID: fmt.Sprintf(UserStreamPrefix, userID) + Starred,
|
||||
result.Tags = make([]subscriptionCategoryResponse, 0)
|
||||
result.Tags = append(result.Tags, subscriptionCategoryResponse{
|
||||
ID: fmt.Sprintf(userStreamPrefix, userID) + starredStreamSuffix,
|
||||
})
|
||||
for _, category := range categories {
|
||||
result.Tags = append(result.Tags, subscriptionCategory{
|
||||
ID: fmt.Sprintf(UserLabelPrefix, userID) + category.Title,
|
||||
result.Tags = append(result.Tags, subscriptionCategoryResponse{
|
||||
ID: fmt.Sprintf(userLabelPrefix, userID) + category.Title,
|
||||
Label: category.Title,
|
||||
Type: "folder",
|
||||
})
|
||||
@@ -1217,13 +970,13 @@ func (h *handler) subscriptionListHandler(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
result.Subscriptions = make([]subscription, 0)
|
||||
result.Subscriptions = make([]subscriptionResponse, 0)
|
||||
for _, feed := range feeds {
|
||||
result.Subscriptions = append(result.Subscriptions, subscription{
|
||||
ID: fmt.Sprintf(FeedPrefix+"%d", feed.ID),
|
||||
result.Subscriptions = append(result.Subscriptions, subscriptionResponse{
|
||||
ID: fmt.Sprintf(feedPrefix+"%d", feed.ID),
|
||||
Title: feed.Title,
|
||||
URL: feed.FeedURL,
|
||||
Categories: []subscriptionCategory{{fmt.Sprintf(UserLabelPrefix, userID) + feed.Category.Title, feed.Category.Title, "folder"}},
|
||||
Categories: []subscriptionCategoryResponse{{fmt.Sprintf(userLabelPrefix, userID) + feed.Category.Title, feed.Category.Title, "folder"}},
|
||||
HTMLURL: feed.SiteURL,
|
||||
IconURL: h.feedIconURL(feed),
|
||||
})
|
||||
@@ -1262,7 +1015,7 @@ func (h *handler) userInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
userInfo := userInfo{UserID: fmt.Sprint(user.ID), UserName: user.Username, UserProfileID: fmt.Sprint(user.ID), UserEmail: user.Username}
|
||||
userInfo := userInfoResponse{UserID: fmt.Sprint(user.ID), UserName: user.Username, UserProfileID: fmt.Sprint(user.ID), UserEmail: user.Username}
|
||||
json.OK(w, r, userInfo)
|
||||
}
|
||||
|
||||
@@ -1282,7 +1035,7 @@ func (h *handler) streamItemIDsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
rm, err := getStreamFilterModifiers(r)
|
||||
rm, err := parseStreamFilterFromRequest(r)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
@@ -1338,7 +1091,7 @@ func (h *handler) handleReadingListStreamHandler(w http.ResponseWriter, r *http.
|
||||
slog.String("handler", "handleReadingListStreamHandler"),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.Any("filter_type", s.Type),
|
||||
slog.Int("filter_type", int(s.Type)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1509,3 +1262,82 @@ func (h *handler) handleFeedStreamHandler(w http.ResponseWriter, r *http.Request
|
||||
|
||||
json.OK(w, r, streamIDResponse{itemRefs, continuation})
|
||||
}
|
||||
|
||||
func (h *handler) markAllAsReadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := request.UserID(r)
|
||||
clientIP := request.ClientIP(r)
|
||||
|
||||
slog.Debug("[GoogleReader] Handle /mark-all-as-read",
|
||||
slog.String("handler", "markAllAsReadHandler"),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
stream, err := getStream(r.Form.Get(paramStreamID), userID)
|
||||
if err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var before time.Time
|
||||
if timestampParamValue := r.Form.Get(paramTimestamp); timestampParamValue != "" {
|
||||
timestampParsedValue, err := strconv.ParseInt(timestampParamValue, 10, 64)
|
||||
if err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if timestampParsedValue > 0 {
|
||||
// It's unclear if the timestamp is in seconds or microseconds, so we try both using a naive approach.
|
||||
if len(timestampParamValue) >= 16 {
|
||||
before = time.UnixMicro(timestampParsedValue)
|
||||
} else {
|
||||
before = time.Unix(timestampParsedValue, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if before.IsZero() {
|
||||
before = time.Now()
|
||||
}
|
||||
|
||||
switch stream.Type {
|
||||
case FeedStream:
|
||||
feedID, err := strconv.ParseInt(stream.ID, 10, 64)
|
||||
if err != nil {
|
||||
json.BadRequest(w, r, err)
|
||||
return
|
||||
}
|
||||
err = h.store.MarkFeedAsRead(userID, feedID, before)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
case LabelStream:
|
||||
category, err := h.store.CategoryByTitle(userID, stream.ID)
|
||||
if err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
if category == nil {
|
||||
json.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if err := h.store.MarkCategoryAsRead(userID, category.ID, before); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
case ReadingListStream:
|
||||
if err = h.store.MarkAllAsReadBeforeDate(userID, before); err != nil {
|
||||
json.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sendOkayResponse(w)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package googlereader // import "miniflux.app/v2/internal/googlereader"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
ItemIDPrefix = "tag:google.com,2005:reader/item/"
|
||||
ItemIDFormat = "tag:google.com,2005:reader/item/%016x"
|
||||
)
|
||||
|
||||
func convertEntryIDToLongFormItemID(entryID int64) string {
|
||||
// The entry ID is a 64-bit integer, so we need to format it as a 16-character hexadecimal string.
|
||||
return fmt.Sprintf(ItemIDFormat, entryID)
|
||||
}
|
||||
|
||||
// Expected format: "tag:google.com,2005:reader/item/00000000148b9369" (hexadecimal string with prefix and padding)
|
||||
// NetNewsWire uses this format: "tag:google.com,2005:reader/item/2f2" (hexadecimal string with prefix and no padding)
|
||||
// Reeder uses this format: "000000000000048c" (hexadecimal string without prefix and padding)
|
||||
// Liferea uses this format: "12345" (decimal string)
|
||||
// It returns the parsed ID as a int64 and an error if parsing fails.
|
||||
func parseItemID(itemIDValue string) (int64, error) {
|
||||
var itemID int64
|
||||
if strings.HasPrefix(itemIDValue, ItemIDPrefix) {
|
||||
n, err := fmt.Sscanf(itemIDValue, ItemIDFormat, &itemID)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse hexadecimal item ID %s: %w", itemIDValue, err)
|
||||
}
|
||||
if n != 1 {
|
||||
return 0, fmt.Errorf("failed to parse hexadecimal item ID %s: expected 1 value, got %d", itemIDValue, n)
|
||||
}
|
||||
if itemID == 0 {
|
||||
return 0, fmt.Errorf("failed to parse hexadecimal item ID %s: item ID is zero", itemIDValue)
|
||||
}
|
||||
return itemID, nil
|
||||
}
|
||||
|
||||
if len(itemIDValue) == 16 {
|
||||
if n, err := fmt.Sscanf(itemIDValue, "%016x", &itemID); err == nil && n == 1 {
|
||||
return itemID, nil
|
||||
}
|
||||
}
|
||||
|
||||
itemID, err := strconv.ParseInt(itemIDValue, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse decimal item ID %s: %w", itemIDValue, err)
|
||||
}
|
||||
|
||||
return itemID, nil
|
||||
}
|
||||
|
||||
func parseItemIDsFromRequest(r *http.Request) ([]int64, error) {
|
||||
items := r.Form[paramItemIDs]
|
||||
if len(items) == 0 {
|
||||
return nil, fmt.Errorf("googlereader: no items requested")
|
||||
}
|
||||
|
||||
itemIDs := make([]int64, len(items))
|
||||
for i, item := range items {
|
||||
itemID, err := parseItemID(item)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("googlereader: failed to parse item ID %s: %w", item, err)
|
||||
}
|
||||
itemIDs[i] = itemID
|
||||
}
|
||||
|
||||
return itemIDs, nil
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package googlereader // import "miniflux.app/v2/internal/googlereader"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConvertEntryIDToLongFormItemID(t *testing.T) {
|
||||
entryID := int64(344691561)
|
||||
expected := "tag:google.com,2005:reader/item/00000000148b9369"
|
||||
result := convertEntryIDToLongFormItemID(entryID)
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("expected %s, got %s", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseItemIDsFromRequest(t *testing.T) {
|
||||
formValues := url.Values{}
|
||||
formValues.Add("i", "12345")
|
||||
formValues.Add("i", "tag:google.com,2005:reader/item/00000000148b9369")
|
||||
formValues.Add("i", "tag:google.com,2005:reader/item/2f2")
|
||||
formValues.Add("i", "000000000000046f")
|
||||
formValues.Add("i", "tag:google.com,2005:reader/item/272")
|
||||
|
||||
request := &http.Request{
|
||||
Form: formValues,
|
||||
}
|
||||
|
||||
result, err := parseItemIDsFromRequest(request)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var expected = []int64{12345, 344691561, 754, 1135, 626}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Errorf("expected %v, got %v", expected, result)
|
||||
}
|
||||
|
||||
// Test with no item IDs
|
||||
formValues = url.Values{}
|
||||
request = &http.Request{
|
||||
Form: formValues,
|
||||
}
|
||||
_, err = parseItemIDsFromRequest(request)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseItemID(t *testing.T) {
|
||||
// Test with long form ID and hex ID
|
||||
result, err := parseItemID("tag:google.com,2005:reader/item/0000000000000001")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
expected := int64(1)
|
||||
if result != expected {
|
||||
t.Errorf("expected %d, got %d", expected, result)
|
||||
}
|
||||
|
||||
// Test with hexadecimal long form ID
|
||||
result, err = parseItemID("0000000000000468")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
expected = int64(1128)
|
||||
if result != expected {
|
||||
t.Errorf("expected %d, got %d", expected, result)
|
||||
}
|
||||
|
||||
// Test with short form ID
|
||||
result, err = parseItemID("12345")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
expected = int64(12345)
|
||||
if result != expected {
|
||||
t.Errorf("expected %d, got %d", expected, result)
|
||||
}
|
||||
|
||||
// Test with invalid long form ID
|
||||
_, err = parseItemID("tag:google.com,2005:reader/item/000000000000000g")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
|
||||
// Test with invalid short form ID
|
||||
_, err = parseItemID("invalid_id")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
|
||||
// Test with empty ID
|
||||
_, err = parseItemID("")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
Unauthorized(w, r)
|
||||
sendUnauthorizedResponse(w)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
Unauthorized(w, r)
|
||||
sendUnauthorizedResponse(w)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@@ -74,7 +74,7 @@ func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
Unauthorized(w, r)
|
||||
sendUnauthorizedResponse(w)
|
||||
return
|
||||
}
|
||||
fields := strings.Fields(authorization)
|
||||
@@ -84,7 +84,7 @@ func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
Unauthorized(w, r)
|
||||
sendUnauthorizedResponse(w)
|
||||
return
|
||||
}
|
||||
if fields[0] != "GoogleLogin" {
|
||||
@@ -93,7 +93,7 @@ func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
Unauthorized(w, r)
|
||||
sendUnauthorizedResponse(w)
|
||||
return
|
||||
}
|
||||
auths := strings.Split(fields[1], "=")
|
||||
@@ -103,7 +103,7 @@ func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
Unauthorized(w, r)
|
||||
sendUnauthorizedResponse(w)
|
||||
return
|
||||
}
|
||||
if auths[0] != "auth" {
|
||||
@@ -112,7 +112,7 @@ func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
Unauthorized(w, r)
|
||||
sendUnauthorizedResponse(w)
|
||||
return
|
||||
}
|
||||
token = auths[1]
|
||||
@@ -126,7 +126,7 @@ func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.String("token", token),
|
||||
)
|
||||
Unauthorized(w, r)
|
||||
sendUnauthorizedResponse(w)
|
||||
return
|
||||
}
|
||||
var integration *model.Integration
|
||||
@@ -139,7 +139,7 @@ func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
Unauthorized(w, r)
|
||||
sendUnauthorizedResponse(w)
|
||||
return
|
||||
}
|
||||
expectedToken := getAuthToken(integration.GoogleReaderUsername, integration.GoogleReaderPassword)
|
||||
@@ -149,7 +149,7 @@ func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
Unauthorized(w, r)
|
||||
sendUnauthorizedResponse(w)
|
||||
return
|
||||
}
|
||||
if user, err = m.store.UserByID(integration.UserID); err != nil {
|
||||
@@ -159,7 +159,7 @@ func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
Unauthorized(w, r)
|
||||
sendUnauthorizedResponse(w)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -169,22 +169,15 @@ func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
)
|
||||
Unauthorized(w, r)
|
||||
sendUnauthorizedResponse(w)
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("[GoogleReader] User authenticated successfully",
|
||||
slog.Bool("authentication_successful", true),
|
||||
slog.String("client_ip", clientIP),
|
||||
slog.String("user_agent", r.UserAgent()),
|
||||
slog.Int64("user_id", user.ID),
|
||||
slog.String("username", user.Username),
|
||||
)
|
||||
|
||||
m.store.SetLastLogin(integration.UserID)
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, request.UserIDContextKey, user.ID)
|
||||
ctx = context.WithValue(ctx, request.UserNameContextKey, user.Username)
|
||||
ctx = context.WithValue(ctx, request.UserTimezoneContextKey, user.Timezone)
|
||||
ctx = context.WithValue(ctx, request.IsAdminUserContextKey, user.IsAdmin)
|
||||
ctx = context.WithValue(ctx, request.IsAuthenticatedContextKey, true)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package googlereader // import "miniflux.app/v2/internal/googlereader"
|
||||
|
||||
const (
|
||||
// paramItemIDs - name of the parameter with the item ids
|
||||
paramItemIDs = "i"
|
||||
// paramStreamID - name of the parameter containing the stream to be included
|
||||
paramStreamID = "s"
|
||||
// paramStreamExcludes - name of the parameter containing streams to be excluded
|
||||
paramStreamExcludes = "xt"
|
||||
// paramStreamFilters - name of the parameter containing streams to be included
|
||||
paramStreamFilters = "it"
|
||||
// paramStreamMaxItems - name of the parameter containing number of items per page/max items returned
|
||||
paramStreamMaxItems = "n"
|
||||
// paramStreamOrder - name of the parameter containing the sort criteria
|
||||
paramStreamOrder = "r"
|
||||
// paramStreamStartTime - name of the parameter containing epoch timestamp, filtering items older than
|
||||
paramStreamStartTime = "ot"
|
||||
// paramStreamStopTime - name of the parameter containing epoch timestamp, filtering items newer than
|
||||
paramStreamStopTime = "nt"
|
||||
// paramTagsRemove - name of the parameter containing tags (streams) to be removed
|
||||
paramTagsRemove = "r"
|
||||
// paramTagsAdd - name of the parameter containing tags (streams) to be added
|
||||
paramTagsAdd = "a"
|
||||
// paramSubscribeAction - name of the parameter indicating the action to take for subscription/edit
|
||||
paramSubscribeAction = "ac"
|
||||
// paramTitle - name of the parameter for the title of the subscription
|
||||
paramTitle = "t"
|
||||
// paramQuickAdd - name of the parameter for a URL being quick subscribed to
|
||||
paramQuickAdd = "quickadd"
|
||||
// paramDestination - name of the parameter for the new name of a tag
|
||||
paramDestination = "dest"
|
||||
// paramContinuation - name of the parameter for callers to pass to receive the next page of results
|
||||
paramContinuation = "c"
|
||||
// paramTimestamp - name of the parameter for unix timestamp
|
||||
paramTimestamp = "ts"
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package googlereader // import "miniflux.app/v2/internal/googlereader"
|
||||
|
||||
const (
|
||||
// streamPrefix is the prefix for streams (read/starred/reading list and so on)
|
||||
streamPrefix = "user/-/state/com.google/"
|
||||
// userStreamPrefix is the user specific prefix for streams (read/starred/reading list and so on)
|
||||
userStreamPrefix = "user/%d/state/com.google/"
|
||||
// labelPrefix is the prefix for a label stream
|
||||
labelPrefix = "user/-/label/"
|
||||
// userLabelPrefix is the user specific prefix prefix for a label stream
|
||||
userLabelPrefix = "user/%d/label/"
|
||||
// feedPrefix is the prefix for a feed stream
|
||||
feedPrefix = "feed/"
|
||||
// readStreamSuffix is the suffix for read stream
|
||||
readStreamSuffix = "read"
|
||||
// starredStreamSuffix is the suffix for starred stream
|
||||
starredStreamSuffix = "starred"
|
||||
// readingListStreamSuffix is the suffix for reading list stream
|
||||
readingListStreamSuffix = "reading-list"
|
||||
// keptUnreadStreamSuffix is the suffix for kept unread stream
|
||||
keptUnreadStreamSuffix = "kept-unread"
|
||||
// broadcastStreamSuffix is the suffix for broadcast stream
|
||||
broadcastStreamSuffix = "broadcast"
|
||||
// broadcastFriendsStreamSuffix is the suffix for broadcast friends stream
|
||||
broadcastFriendsStreamSuffix = "broadcast-friends"
|
||||
// likeStreamSuffix is the suffix for like stream
|
||||
likeStreamSuffix = "like"
|
||||
)
|
||||
@@ -0,0 +1,91 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package googlereader // import "miniflux.app/v2/internal/googlereader"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"miniflux.app/v2/internal/http/request"
|
||||
)
|
||||
|
||||
type RequestModifiers struct {
|
||||
ExcludeTargets []Stream
|
||||
FilterTargets []Stream
|
||||
Streams []Stream
|
||||
Count int
|
||||
Offset int
|
||||
SortDirection string
|
||||
StartTime int64
|
||||
StopTime int64
|
||||
ContinuationToken string
|
||||
UserID int64
|
||||
}
|
||||
|
||||
func (r RequestModifiers) String() string {
|
||||
var results []string
|
||||
|
||||
results = append(results, fmt.Sprintf("UserID: %d", r.UserID))
|
||||
|
||||
var streamStr []string
|
||||
for _, s := range r.Streams {
|
||||
streamStr = append(streamStr, s.String())
|
||||
}
|
||||
results = append(results, fmt.Sprintf("Streams: [%s]", strings.Join(streamStr, ", ")))
|
||||
|
||||
var exclusions []string
|
||||
for _, s := range r.ExcludeTargets {
|
||||
exclusions = append(exclusions, s.String())
|
||||
}
|
||||
results = append(results, fmt.Sprintf("Exclusions: [%s]", strings.Join(exclusions, ", ")))
|
||||
|
||||
var filters []string
|
||||
for _, s := range r.FilterTargets {
|
||||
filters = append(filters, s.String())
|
||||
}
|
||||
results = append(results, fmt.Sprintf("Filters: [%s]", strings.Join(filters, ", ")))
|
||||
|
||||
results = append(results, fmt.Sprintf("Count: %d", r.Count))
|
||||
results = append(results, fmt.Sprintf("Offset: %d", r.Offset))
|
||||
results = append(results, fmt.Sprintf("Sort Direction: %s", r.SortDirection))
|
||||
results = append(results, fmt.Sprintf("Continuation Token: %s", r.ContinuationToken))
|
||||
results = append(results, fmt.Sprintf("Start Time: %d", r.StartTime))
|
||||
results = append(results, fmt.Sprintf("Stop Time: %d", r.StopTime))
|
||||
|
||||
return strings.Join(results, "; ")
|
||||
}
|
||||
|
||||
func parseStreamFilterFromRequest(r *http.Request) (RequestModifiers, error) {
|
||||
userID := request.UserID(r)
|
||||
result := RequestModifiers{
|
||||
SortDirection: "desc",
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
streamOrder := request.QueryStringParam(r, paramStreamOrder, "d")
|
||||
if streamOrder == "o" {
|
||||
result.SortDirection = "asc"
|
||||
}
|
||||
var err error
|
||||
result.Streams, err = getStreams(request.QueryStringParamList(r, paramStreamID), userID)
|
||||
if err != nil {
|
||||
return RequestModifiers{}, err
|
||||
}
|
||||
result.ExcludeTargets, err = getStreams(request.QueryStringParamList(r, paramStreamExcludes), userID)
|
||||
if err != nil {
|
||||
return RequestModifiers{}, err
|
||||
}
|
||||
|
||||
result.FilterTargets, err = getStreams(request.QueryStringParamList(r, paramStreamFilters), userID)
|
||||
if err != nil {
|
||||
return RequestModifiers{}, err
|
||||
}
|
||||
|
||||
result.Count = request.QueryIntParam(r, paramStreamMaxItems, 0)
|
||||
result.Offset = request.QueryIntParam(r, paramContinuation, 0)
|
||||
result.StartTime = request.QueryInt64Param(r, paramStreamStartTime, int64(0))
|
||||
result.StopTime = request.QueryInt64Param(r, paramStreamStopTime, int64(0))
|
||||
return result, nil
|
||||
}
|
||||
@@ -6,34 +6,36 @@ package googlereader // import "miniflux.app/v2/internal/googlereader"
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/v2/internal/http/response"
|
||||
)
|
||||
|
||||
type login struct {
|
||||
type loginResponse struct {
|
||||
SID string `json:"SID,omitempty"`
|
||||
LSID string `json:"LSID,omitempty"`
|
||||
Auth string `json:"Auth,omitempty"`
|
||||
}
|
||||
|
||||
func (l login) String() string {
|
||||
func (l loginResponse) String() string {
|
||||
return fmt.Sprintf("SID=%s\nLSID=%s\nAuth=%s\n", l.SID, l.LSID, l.Auth)
|
||||
}
|
||||
|
||||
type userInfo struct {
|
||||
type userInfoResponse struct {
|
||||
UserID string `json:"userId"`
|
||||
UserName string `json:"userName"`
|
||||
UserProfileID string `json:"userProfileId"`
|
||||
UserEmail string `json:"userEmail"`
|
||||
}
|
||||
|
||||
type subscription struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Categories []subscriptionCategory `json:"categories"`
|
||||
URL string `json:"url"`
|
||||
HTMLURL string `json:"htmlUrl"`
|
||||
IconURL string `json:"iconUrl"`
|
||||
type subscriptionResponse struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Categories []subscriptionCategoryResponse `json:"categories"`
|
||||
URL string `json:"url"`
|
||||
HTMLURL string `json:"htmlUrl"`
|
||||
IconURL string `json:"iconUrl"`
|
||||
}
|
||||
|
||||
type subscriptionsResponse struct {
|
||||
Subscriptions []subscriptionResponse `json:"subscriptions"`
|
||||
}
|
||||
|
||||
type quickAddResponse struct {
|
||||
@@ -43,14 +45,11 @@ type quickAddResponse struct {
|
||||
StreamName string `json:"streamName,omitempty"`
|
||||
}
|
||||
|
||||
type subscriptionCategory struct {
|
||||
type subscriptionCategoryResponse struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
type subscriptionsResponse struct {
|
||||
Subscriptions []subscription `json:"subscriptions"`
|
||||
}
|
||||
|
||||
type itemRef struct {
|
||||
ID string `json:"id"`
|
||||
@@ -64,18 +63,17 @@ type streamIDResponse struct {
|
||||
}
|
||||
|
||||
type tagsResponse struct {
|
||||
Tags []subscriptionCategory `json:"tags"`
|
||||
Tags []subscriptionCategoryResponse `json:"tags"`
|
||||
}
|
||||
|
||||
type streamContentItems struct {
|
||||
Direction string `json:"direction"`
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Self []contentHREF `json:"self"`
|
||||
Alternate []contentHREFType `json:"alternate"`
|
||||
Updated int64 `json:"updated"`
|
||||
Items []contentItem `json:"items"`
|
||||
Author string `json:"author"`
|
||||
type streamContentItemsResponse struct {
|
||||
Direction string `json:"direction"`
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Self []contentHREF `json:"self"`
|
||||
Updated int64 `json:"updated"`
|
||||
Items []contentItem `json:"items"`
|
||||
Author string `json:"author"`
|
||||
}
|
||||
|
||||
type contentItem struct {
|
||||
@@ -119,21 +117,15 @@ type contentItemOrigin struct {
|
||||
HTMLUrl string `json:"htmlUrl"`
|
||||
}
|
||||
|
||||
// Unauthorized sends a not authorized error to the client.
|
||||
func Unauthorized(w http.ResponseWriter, r *http.Request) {
|
||||
builder := response.New(w, r)
|
||||
builder.WithStatus(http.StatusUnauthorized)
|
||||
builder.WithHeader("Content-Type", "text/plain")
|
||||
builder.WithHeader("X-Reader-Google-Bad-Token", "true")
|
||||
builder.WithBody("Unauthorized")
|
||||
builder.Write()
|
||||
func sendUnauthorizedResponse(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Header().Set("X-Reader-Google-Bad-Token", "true")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
}
|
||||
|
||||
// OK sends a ok response to the client.
|
||||
func OK(w http.ResponseWriter, r *http.Request) {
|
||||
builder := response.New(w, r)
|
||||
builder.WithStatus(http.StatusOK)
|
||||
builder.WithHeader("Content-Type", "text/plain")
|
||||
builder.WithBody("OK")
|
||||
builder.Write()
|
||||
func sendOkayResponse(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package googlereader // import "miniflux.app/v2/internal/googlereader"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type StreamType int
|
||||
|
||||
const (
|
||||
// NoStream - no stream type
|
||||
NoStream StreamType = iota
|
||||
// ReadStream - read stream type
|
||||
ReadStream
|
||||
// StarredStream - starred stream type
|
||||
StarredStream
|
||||
// ReadingListStream - reading list stream type
|
||||
ReadingListStream
|
||||
// KeptUnreadStream - kept unread stream type
|
||||
KeptUnreadStream
|
||||
// BroadcastStream - broadcast stream type
|
||||
BroadcastStream
|
||||
// BroadcastFriendsStream - broadcast friends stream type
|
||||
BroadcastFriendsStream
|
||||
// LabelStream - label stream type
|
||||
LabelStream
|
||||
// FeedStream - feed stream type
|
||||
FeedStream
|
||||
// LikeStream - like stream type
|
||||
LikeStream
|
||||
)
|
||||
|
||||
// Stream defines a stream type and its ID.
|
||||
type Stream struct {
|
||||
Type StreamType
|
||||
ID string
|
||||
}
|
||||
|
||||
func (s Stream) String() string {
|
||||
return fmt.Sprintf("%v - '%s'", s.Type, s.ID)
|
||||
}
|
||||
|
||||
func (st StreamType) String() string {
|
||||
switch st {
|
||||
case NoStream:
|
||||
return "NoStream"
|
||||
case ReadStream:
|
||||
return "ReadStream"
|
||||
case StarredStream:
|
||||
return "StarredStream"
|
||||
case ReadingListStream:
|
||||
return "ReadingListStream"
|
||||
case KeptUnreadStream:
|
||||
return "KeptUnreadStream"
|
||||
case BroadcastStream:
|
||||
return "BroadcastStream"
|
||||
case BroadcastFriendsStream:
|
||||
return "BroadcastFriendsStream"
|
||||
case LabelStream:
|
||||
return "LabelStream"
|
||||
case FeedStream:
|
||||
return "FeedStream"
|
||||
case LikeStream:
|
||||
return "LikeStream"
|
||||
default:
|
||||
return st.String()
|
||||
}
|
||||
}
|
||||
|
||||
func getStream(streamID string, userID int64) (Stream, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(streamID, feedPrefix):
|
||||
return Stream{Type: FeedStream, ID: strings.TrimPrefix(streamID, feedPrefix)}, nil
|
||||
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 {
|
||||
case readStreamSuffix:
|
||||
return Stream{ReadStream, ""}, nil
|
||||
case starredStreamSuffix:
|
||||
return Stream{StarredStream, ""}, nil
|
||||
case readingListStreamSuffix:
|
||||
return Stream{ReadingListStream, ""}, nil
|
||||
case keptUnreadStreamSuffix:
|
||||
return Stream{KeptUnreadStream, ""}, nil
|
||||
case broadcastStreamSuffix:
|
||||
return Stream{BroadcastStream, ""}, nil
|
||||
case broadcastFriendsStreamSuffix:
|
||||
return Stream{BroadcastFriendsStream, ""}, nil
|
||||
case likeStreamSuffix:
|
||||
return Stream{LikeStream, ""}, nil
|
||||
default:
|
||||
return Stream{NoStream, ""}, fmt.Errorf("googlereader: unknown stream with id: %s", id)
|
||||
}
|
||||
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
|
||||
case streamID == "":
|
||||
return Stream{NoStream, ""}, nil
|
||||
default:
|
||||
return Stream{NoStream, ""}, fmt.Errorf("googlereader: unknown stream type: %s", streamID)
|
||||
}
|
||||
}
|
||||
|
||||
func getStreams(streamIDs []string, userID int64) ([]Stream, error) {
|
||||
streams := make([]Stream, 0, len(streamIDs))
|
||||
for _, streamID := range streamIDs {
|
||||
stream, err := getStream(streamID, userID)
|
||||
if err != nil {
|
||||
return []Stream{}, err
|
||||
}
|
||||
streams = append(streams, stream)
|
||||
}
|
||||
return streams, nil
|
||||
}
|
||||
@@ -18,20 +18,26 @@ const (
|
||||
|
||||
// New creates a new cookie.
|
||||
func New(name, value string, isHTTPS bool, path string) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
cookie := &http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
Path: basePath(path),
|
||||
Secure: isHTTPS,
|
||||
HttpOnly: true,
|
||||
Expires: time.Now().Add(time.Duration(config.Opts.CleanupRemoveSessionsDays()) * 24 * time.Hour),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
|
||||
// OAuth doesn't work when cookies are in strict mode.
|
||||
if config.Opts.OAuth2Provider() != "" {
|
||||
cookie.SameSite = http.SameSiteLaxMode
|
||||
}
|
||||
return cookie
|
||||
}
|
||||
|
||||
// Expired returns an expired cookie.
|
||||
func Expired(name string, isHTTPS bool, path string) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
cookie := &http.Cookie{
|
||||
Name: name,
|
||||
Value: "",
|
||||
Path: basePath(path),
|
||||
@@ -39,8 +45,14 @@ func Expired(name string, isHTTPS bool, path string) *http.Cookie {
|
||||
HttpOnly: true,
|
||||
MaxAge: -1,
|
||||
Expires: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
|
||||
// OAuth doesn't work when cookies are in strict mode.
|
||||
if config.Opts.OAuth2Provider() != "" {
|
||||
cookie.SameSite = http.SameSiteLaxMode
|
||||
}
|
||||
return cookie
|
||||
}
|
||||
|
||||
func basePath(path string) string {
|
||||
|
||||
@@ -16,6 +16,7 @@ type ContextKey int
|
||||
// List of context keys.
|
||||
const (
|
||||
UserIDContextKey ContextKey = iota
|
||||
UserNameContextKey
|
||||
UserTimezoneContextKey
|
||||
IsAdminUserContextKey
|
||||
IsAuthenticatedContextKey
|
||||
@@ -28,7 +29,6 @@ const (
|
||||
OAuth2CodeVerifierContextKey
|
||||
FlashMessageContextKey
|
||||
FlashErrorMessageContextKey
|
||||
PocketRequestTokenContextKey
|
||||
LastForceRefreshContextKey
|
||||
ClientIPContextKey
|
||||
GoogleReaderToken
|
||||
@@ -64,6 +64,15 @@ func UserID(r *http.Request) int64 {
|
||||
return getContextInt64Value(r, UserIDContextKey)
|
||||
}
|
||||
|
||||
// UserName returns the username of the logged user.
|
||||
func UserName(r *http.Request) string {
|
||||
value := getContextStringValue(r, UserNameContextKey)
|
||||
if value == "" {
|
||||
value = "unknown"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// UserTimezone returns the timezone used by the logged user.
|
||||
func UserTimezone(r *http.Request) string {
|
||||
value := getContextStringValue(r, UserTimezoneContextKey)
|
||||
@@ -125,11 +134,6 @@ func FlashErrorMessage(r *http.Request) string {
|
||||
return getContextStringValue(r, FlashErrorMessageContextKey)
|
||||
}
|
||||
|
||||
// PocketRequestToken returns the Pocket Request Token if any.
|
||||
func PocketRequestToken(r *http.Request) string {
|
||||
return getContextStringValue(r, PocketRequestTokenContextKey)
|
||||
}
|
||||
|
||||
// LastForceRefresh returns the last force refresh timestamp.
|
||||
func LastForceRefresh(r *http.Request) int64 {
|
||||
jsonStringValue := getContextStringValue(r, LastForceRefreshContextKey)
|
||||
|
||||
@@ -390,28 +390,6 @@ func TestFlashErrorMessage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPocketRequestToken(t *testing.T) {
|
||||
r, _ := http.NewRequest("GET", "http://example.org", nil)
|
||||
|
||||
result := PocketRequestToken(r)
|
||||
expected := ""
|
||||
|
||||
if result != expected {
|
||||
t.Errorf(`Unexpected context value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, PocketRequestTokenContextKey, "request token")
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
result = PocketRequestToken(r)
|
||||
expected = "request token"
|
||||
|
||||
if result != expected {
|
||||
t.Errorf(`Unexpected context value, got %q instead of %q`, result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientIP(t *testing.T) {
|
||||
r, _ := http.NewRequest("GET", "http://example.org", nil)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package html // import "miniflux.app/v2/internal/http/response/html"
|
||||
|
||||
import (
|
||||
"html"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
@@ -38,9 +39,9 @@ func ServerError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
builder := response.New(w, r)
|
||||
builder.WithStatus(http.StatusInternalServerError)
|
||||
builder.WithHeader("Content-Security-Policy", response.ContentSecurityPolicyForUntrustedContent)
|
||||
builder.WithHeader("Content-Type", "text/html; charset=utf-8")
|
||||
builder.WithHeader("Content-Type", "text/plain; charset=utf-8")
|
||||
builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store")
|
||||
builder.WithBody(err)
|
||||
builder.WithBody(html.EscapeString(err.Error()))
|
||||
builder.Write()
|
||||
}
|
||||
|
||||
@@ -62,9 +63,9 @@ func BadRequest(w http.ResponseWriter, r *http.Request, err error) {
|
||||
builder := response.New(w, r)
|
||||
builder.WithStatus(http.StatusBadRequest)
|
||||
builder.WithHeader("Content-Security-Policy", response.ContentSecurityPolicyForUntrustedContent)
|
||||
builder.WithHeader("Content-Type", "text/html; charset=utf-8")
|
||||
builder.WithHeader("Content-Type", "text/plain; charset=utf-8")
|
||||
builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store")
|
||||
builder.WithBody(err)
|
||||
builder.WithBody(html.EscapeString(err.Error()))
|
||||
builder.Write()
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ func TestServerErrorResponse(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ServerError(w, r, errors.New("Some error"))
|
||||
ServerError(w, r, errors.New("Some error with injected HTML <script>alert('XSS')</script>"))
|
||||
})
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
@@ -69,13 +69,13 @@ func TestServerErrorResponse(t *testing.T) {
|
||||
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
|
||||
}
|
||||
|
||||
expectedBody := `Some error`
|
||||
expectedBody := `Some error with injected HTML <script>alert('XSS')</script>`
|
||||
actualBody := w.Body.String()
|
||||
if actualBody != expectedBody {
|
||||
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
|
||||
}
|
||||
|
||||
expectedContentType := "text/html; charset=utf-8"
|
||||
expectedContentType := "text/plain; charset=utf-8"
|
||||
actualContentType := resp.Header.Get("Content-Type")
|
||||
if actualContentType != expectedContentType {
|
||||
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
|
||||
@@ -91,7 +91,7 @@ func TestBadRequestResponse(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
BadRequest(w, r, errors.New("Some error"))
|
||||
BadRequest(w, r, errors.New("Some error with injected HTML <script>alert('XSS')</script>"))
|
||||
})
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
@@ -102,13 +102,13 @@ func TestBadRequestResponse(t *testing.T) {
|
||||
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
|
||||
}
|
||||
|
||||
expectedBody := `Some error`
|
||||
expectedBody := `Some error with injected HTML <script>alert('XSS')</script>`
|
||||
actualBody := w.Body.String()
|
||||
if actualBody != expectedBody {
|
||||
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
|
||||
}
|
||||
|
||||
expectedContentType := "text/html; charset=utf-8"
|
||||
expectedContentType := "text/plain; charset=utf-8"
|
||||
actualContentType := resp.Header.Get("Content-Type")
|
||||
if actualContentType != expectedContentType {
|
||||
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
|
||||
|
||||
+144
-98
@@ -1,7 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package httpd // import "miniflux.app/v2/internal/http/server"
|
||||
package server // import "miniflux.app/v2/internal/http/server"
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
@@ -30,36 +30,84 @@ import (
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
func StartWebServer(store *storage.Storage, pool *worker.Pool) *http.Server {
|
||||
func StartWebServer(store *storage.Storage, pool *worker.Pool) []*http.Server {
|
||||
listenAddresses := config.Opts.ListenAddr()
|
||||
var httpServers []*http.Server
|
||||
|
||||
certFile := config.Opts.CertFile()
|
||||
keyFile := config.Opts.CertKeyFile()
|
||||
certDomain := config.Opts.CertDomain()
|
||||
listenAddr := config.Opts.ListenAddr()
|
||||
server := &http.Server{
|
||||
ReadTimeout: time.Duration(config.Opts.HTTPServerTimeout()) * time.Second,
|
||||
WriteTimeout: time.Duration(config.Opts.HTTPServerTimeout()) * time.Second,
|
||||
IdleTimeout: time.Duration(config.Opts.HTTPServerTimeout()) * time.Second,
|
||||
Handler: setupHandler(store, pool),
|
||||
var sharedAutocertTLSConfig *tls.Config
|
||||
|
||||
if certDomain != "" {
|
||||
slog.Debug("Configuring autocert manager and shared TLS config", slog.String("domain", certDomain))
|
||||
certManager := autocert.Manager{
|
||||
Cache: storage.NewCertificateCache(store),
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: autocert.HostWhitelist(certDomain),
|
||||
}
|
||||
|
||||
sharedAutocertTLSConfig = &tls.Config{}
|
||||
sharedAutocertTLSConfig.GetCertificate = certManager.GetCertificate
|
||||
sharedAutocertTLSConfig.NextProtos = []string{"h2", "http/1.1", acme.ALPNProto}
|
||||
|
||||
challengeServer := &http.Server{
|
||||
Handler: certManager.HTTPHandler(nil),
|
||||
Addr: ":http",
|
||||
}
|
||||
slog.Info("Starting ACME HTTP challenge server for autocert", slog.String("address", challengeServer.Addr))
|
||||
go func() {
|
||||
if err := challengeServer.ListenAndServe(); err != http.ErrServerClosed {
|
||||
slog.Error("ACME HTTP challenge server failed", slog.Any("error", err))
|
||||
}
|
||||
}()
|
||||
config.Opts.HTTPS = true
|
||||
httpServers = append(httpServers, challengeServer)
|
||||
}
|
||||
|
||||
switch {
|
||||
case os.Getenv("LISTEN_PID") == strconv.Itoa(os.Getpid()):
|
||||
startSystemdSocketServer(server)
|
||||
case strings.HasPrefix(listenAddr, "/"):
|
||||
startUnixSocketServer(server, listenAddr)
|
||||
case certDomain != "":
|
||||
config.Opts.HTTPS = true
|
||||
startAutoCertTLSServer(server, certDomain, store)
|
||||
case certFile != "" && keyFile != "":
|
||||
config.Opts.HTTPS = true
|
||||
server.Addr = listenAddr
|
||||
startTLSServer(server, certFile, keyFile)
|
||||
default:
|
||||
server.Addr = listenAddr
|
||||
startHTTPServer(server)
|
||||
for i, listenAddr := range listenAddresses {
|
||||
server := &http.Server{
|
||||
ReadTimeout: time.Duration(config.Opts.HTTPServerTimeout()) * time.Second,
|
||||
WriteTimeout: time.Duration(config.Opts.HTTPServerTimeout()) * time.Second,
|
||||
IdleTimeout: time.Duration(config.Opts.HTTPServerTimeout()) * time.Second,
|
||||
Handler: setupHandler(store, pool),
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(listenAddr, "/") && os.Getenv("LISTEN_PID") != strconv.Itoa(os.Getpid()) {
|
||||
server.Addr = listenAddr
|
||||
}
|
||||
|
||||
shouldAddServer := true
|
||||
|
||||
switch {
|
||||
case os.Getenv("LISTEN_PID") == strconv.Itoa(os.Getpid()):
|
||||
if i == 0 {
|
||||
slog.Info("Starting server using systemd socket for the first listen address", slog.String("address_info", listenAddr))
|
||||
startSystemdSocketServer(server)
|
||||
} else {
|
||||
slog.Warn("Systemd socket activation: Only the first listen address is used by systemd. Other addresses ignored.", slog.String("skipped_address", listenAddr))
|
||||
shouldAddServer = false
|
||||
}
|
||||
case strings.HasPrefix(listenAddr, "/"): // Unix socket
|
||||
startUnixSocketServer(server, listenAddr)
|
||||
case certDomain != "" && (listenAddr == ":https" || (i == 0 && strings.Contains(listenAddr, ":"))):
|
||||
server.Addr = listenAddr
|
||||
startAutoCertTLSServer(server, sharedAutocertTLSConfig)
|
||||
case certFile != "" && keyFile != "":
|
||||
server.Addr = listenAddr
|
||||
startTLSServer(server, certFile, keyFile)
|
||||
config.Opts.HTTPS = true
|
||||
default:
|
||||
server.Addr = listenAddr
|
||||
startHTTPServer(server)
|
||||
}
|
||||
|
||||
if shouldAddServer {
|
||||
httpServers = append(httpServers, server)
|
||||
}
|
||||
}
|
||||
|
||||
return server
|
||||
return httpServers
|
||||
}
|
||||
|
||||
func startSystemdSocketServer(server *http.Server) {
|
||||
@@ -72,83 +120,66 @@ func startSystemdSocketServer(server *http.Server) {
|
||||
|
||||
slog.Info(`Starting server using systemd socket`)
|
||||
if err := server.Serve(listener); err != http.ErrServerClosed {
|
||||
printErrorAndExit(`Server failed to start: %v`, err)
|
||||
printErrorAndExit(`Systemd socket server failed to start: %v`, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func startUnixSocketServer(server *http.Server, socketFile string) {
|
||||
os.Remove(socketFile)
|
||||
if err := os.Remove(socketFile); err != nil && !os.IsNotExist(err) {
|
||||
printErrorAndExit("Unable to remove existing Unix socket %s: %v", socketFile, err)
|
||||
}
|
||||
listener, err := net.Listen("unix", socketFile)
|
||||
if err != nil {
|
||||
printErrorAndExit(`Server failed to listen on Unix socket %s: %v`, socketFile, err)
|
||||
}
|
||||
|
||||
go func(sock string) {
|
||||
listener, err := net.Listen("unix", sock)
|
||||
if err != nil {
|
||||
printErrorAndExit(`Server failed to start: %v`, err)
|
||||
}
|
||||
defer listener.Close()
|
||||
if err := os.Chmod(socketFile, 0666); err != nil {
|
||||
printErrorAndExit(`Unable to change socket permission for %s: %v`, socketFile, err)
|
||||
}
|
||||
|
||||
if err := os.Chmod(sock, 0666); err != nil {
|
||||
printErrorAndExit(`Unable to change socket permission: %v`, err)
|
||||
}
|
||||
go func() {
|
||||
certFile := config.Opts.CertFile()
|
||||
keyFile := config.Opts.CertKeyFile()
|
||||
|
||||
slog.Info("Starting server using a Unix socket", slog.String("socket", sock))
|
||||
if err := server.Serve(listener); err != http.ErrServerClosed {
|
||||
printErrorAndExit(`Server failed to start: %v`, err)
|
||||
if certFile != "" && keyFile != "" {
|
||||
slog.Info("Starting TLS server using a Unix socket",
|
||||
slog.String("socket", socketFile),
|
||||
slog.String("cert_file", certFile),
|
||||
slog.String("key_file", keyFile),
|
||||
)
|
||||
// Ensure HTTPS is marked as true if any listener uses TLS
|
||||
config.Opts.HTTPS = true
|
||||
if err := server.ServeTLS(listener, certFile, keyFile); err != http.ErrServerClosed {
|
||||
printErrorAndExit("TLS Unix socket server failed to start on %s: %v", socketFile, err)
|
||||
}
|
||||
} else {
|
||||
slog.Info("Starting server using a Unix socket", slog.String("socket", socketFile))
|
||||
if err := server.Serve(listener); err != http.ErrServerClosed {
|
||||
printErrorAndExit("Unix socket server failed to start on %s: %v", socketFile, err)
|
||||
}
|
||||
}
|
||||
}(socketFile)
|
||||
}()
|
||||
}
|
||||
|
||||
func tlsConfig() *tls.Config {
|
||||
// See https://blog.cloudflare.com/exposing-go-on-the-internet/
|
||||
// And https://wiki.mozilla.org/Security/Server_Side_TLS
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CurvePreferences: []tls.CurveID{
|
||||
tls.CurveP256,
|
||||
tls.X25519,
|
||||
},
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
},
|
||||
func startAutoCertTLSServer(server *http.Server, autoTLSConfig *tls.Config) {
|
||||
if server.TLSConfig == nil {
|
||||
server.TLSConfig = &tls.Config{}
|
||||
}
|
||||
}
|
||||
|
||||
func startAutoCertTLSServer(server *http.Server, certDomain string, store *storage.Storage) {
|
||||
server.Addr = ":https"
|
||||
certManager := autocert.Manager{
|
||||
Cache: storage.NewCertificateCache(store),
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: autocert.HostWhitelist(certDomain),
|
||||
}
|
||||
server.TLSConfig = tlsConfig()
|
||||
server.TLSConfig.GetCertificate = certManager.GetCertificate
|
||||
server.TLSConfig.NextProtos = []string{"h2", "http/1.1", acme.ALPNProto}
|
||||
|
||||
// Handle http-01 challenge.
|
||||
s := &http.Server{
|
||||
Handler: certManager.HTTPHandler(nil),
|
||||
Addr: ":http",
|
||||
}
|
||||
go s.ListenAndServe()
|
||||
server.TLSConfig.GetCertificate = autoTLSConfig.GetCertificate
|
||||
server.TLSConfig.NextProtos = autoTLSConfig.NextProtos
|
||||
|
||||
go func() {
|
||||
slog.Info("Starting TLS server using automatic certificate management",
|
||||
slog.String("listen_address", server.Addr),
|
||||
slog.String("domain", certDomain),
|
||||
)
|
||||
if err := server.ListenAndServeTLS("", ""); err != http.ErrServerClosed {
|
||||
printErrorAndExit(`Server failed to start: %v`, err)
|
||||
printErrorAndExit("Autocert server failed to start on %s: %v", server.Addr, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func startTLSServer(server *http.Server, certFile, keyFile string) {
|
||||
server.TLSConfig = tlsConfig()
|
||||
go func() {
|
||||
slog.Info("Starting TLS server using a certificate",
|
||||
slog.String("listen_address", server.Addr),
|
||||
@@ -156,7 +187,7 @@ func startTLSServer(server *http.Server, certFile, keyFile string) {
|
||||
slog.String("key_file", keyFile),
|
||||
)
|
||||
if err := server.ListenAndServeTLS(certFile, keyFile); err != http.ErrServerClosed {
|
||||
printErrorAndExit(`Server failed to start: %v`, err)
|
||||
printErrorAndExit("TLS server failed to start on %s: %v", server.Addr, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -167,49 +198,64 @@ func startHTTPServer(server *http.Server) {
|
||||
slog.String("listen_address", server.Addr),
|
||||
)
|
||||
if err := server.ListenAndServe(); err != http.ErrServerClosed {
|
||||
printErrorAndExit(`Server failed to start: %v`, err)
|
||||
printErrorAndExit("HTTP server failed to start on %s: %v", server.Addr, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func setupHandler(store *storage.Storage, pool *worker.Pool) *mux.Router {
|
||||
livenessProbe := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
}
|
||||
readinessProbe := func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := store.Ping(); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Database Connection Error: %q", err), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
}
|
||||
|
||||
router := mux.NewRouter()
|
||||
|
||||
// These routes do not take the base path into consideration and are always available at the root of the server.
|
||||
router.HandleFunc("/liveness", livenessProbe).Name("liveness")
|
||||
router.HandleFunc("/healthz", livenessProbe).Name("healthz")
|
||||
router.HandleFunc("/readiness", readinessProbe).Name("readiness")
|
||||
router.HandleFunc("/readyz", readinessProbe).Name("readyz")
|
||||
|
||||
var subrouter *mux.Router
|
||||
if config.Opts.BasePath() != "" {
|
||||
router = router.PathPrefix(config.Opts.BasePath()).Subrouter()
|
||||
subrouter = router.PathPrefix(config.Opts.BasePath()).Subrouter()
|
||||
} else {
|
||||
subrouter = router.NewRoute().Subrouter()
|
||||
}
|
||||
|
||||
if config.Opts.HasMaintenanceMode() {
|
||||
router.Use(func(next http.Handler) http.Handler {
|
||||
subrouter.Use(func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(config.Opts.MaintenanceMessage()))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
router.Use(middleware)
|
||||
subrouter.Use(middleware)
|
||||
|
||||
fever.Serve(router, store)
|
||||
googlereader.Serve(router, store)
|
||||
api.Serve(router, store, pool)
|
||||
ui.Serve(router, store, pool)
|
||||
fever.Serve(subrouter, store)
|
||||
googlereader.Serve(subrouter, store)
|
||||
api.Serve(subrouter, store, pool)
|
||||
ui.Serve(subrouter, store, pool)
|
||||
|
||||
router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := store.Ping(); err != nil {
|
||||
http.Error(w, "Database Connection Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
subrouter.HandleFunc("/healthcheck", readinessProbe).Name("healthcheck")
|
||||
|
||||
w.Write([]byte("OK"))
|
||||
}).Name("healthcheck")
|
||||
|
||||
router.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
|
||||
subrouter.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(version.Version))
|
||||
}).Name("version")
|
||||
|
||||
if config.Opts.HasMetricsCollector() {
|
||||
router.Handle("/metrics", promhttp.Handler()).Name("metrics")
|
||||
router.Use(func(next http.Handler) http.Handler {
|
||||
subrouter.Handle("/metrics", promhttp.Handler()).Name("metrics")
|
||||
subrouter.Use(func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
route := mux.CurrentRoute(r)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package httpd // import "miniflux.app/v2/internal/http/server"
|
||||
package server // import "miniflux.app/v2/internal/http/server"
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
@@ -6,13 +6,13 @@ package integration // import "miniflux.app/v2/internal/integration"
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"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/discord"
|
||||
"miniflux.app/v2/internal/integration/espial"
|
||||
"miniflux.app/v2/internal/integration/instapaper"
|
||||
"miniflux.app/v2/internal/integration/karakeep"
|
||||
"miniflux.app/v2/internal/integration/linkace"
|
||||
"miniflux.app/v2/internal/integration/linkding"
|
||||
"miniflux.app/v2/internal/integration/linkwarden"
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"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/pushover"
|
||||
"miniflux.app/v2/internal/integration/raindrop"
|
||||
"miniflux.app/v2/internal/integration/readeck"
|
||||
@@ -196,24 +195,6 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
|
||||
}
|
||||
}
|
||||
|
||||
if userIntegrations.PocketEnabled {
|
||||
slog.Debug("Sending entry to Pocket",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int64("entry_id", entry.ID),
|
||||
slog.String("entry_url", entry.URL),
|
||||
)
|
||||
|
||||
client := pocket.NewClient(config.Opts.PocketConsumerKey(userIntegrations.PocketConsumerKey), userIntegrations.PocketAccessToken)
|
||||
if err := client.AddURL(entry.URL, entry.Title); err != nil {
|
||||
slog.Error("Unable to send entry to Pocket",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int64("entry_id", entry.ID),
|
||||
slog.String("entry_url", entry.URL),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if userIntegrations.LinkAceEnabled {
|
||||
slog.Debug("Sending entry to LinkAce",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
@@ -428,6 +409,24 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
|
||||
}
|
||||
}
|
||||
|
||||
if userIntegrations.KarakeepEnabled {
|
||||
slog.Debug("Sending entry to Karakeep",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int64("entry_id", entry.ID),
|
||||
slog.String("entry_url", entry.URL),
|
||||
)
|
||||
|
||||
client := karakeep.NewClient(userIntegrations.KarakeepAPIKey, userIntegrations.KarakeepURL)
|
||||
if err := client.SaveURL(entry.URL); err != nil {
|
||||
slog.Error("Unable to send entry to Karakeep",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
slog.Int64("entry_id", entry.ID),
|
||||
slog.String("entry_url", entry.URL),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if userIntegrations.RaindropEnabled {
|
||||
slog.Debug("Sending entry to Raindrop",
|
||||
slog.Int64("user_id", userIntegrations.UserID),
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package karakeep // import "miniflux.app/v2/internal/integration/karakeep"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type errorResponse struct {
|
||||
Code string `json:"code"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type saveURLPayload struct {
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
wrapped *http.Client
|
||||
apiEndpoint string
|
||||
apiToken string
|
||||
}
|
||||
|
||||
func NewClient(apiToken string, apiEndpoint string) *Client {
|
||||
return &Client{wrapped: &http.Client{Timeout: defaultClientTimeout}, apiEndpoint: apiEndpoint, apiToken: apiToken}
|
||||
}
|
||||
|
||||
func (c *Client) SaveURL(entryURL string) error {
|
||||
requestBody, err := json.Marshal(&saveURLPayload{
|
||||
Type: "link",
|
||||
URL: entryURL,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("karakeep: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, c.apiEndpoint, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("karakeep: unable to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiToken))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
|
||||
resp, err := c.wrapped.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("karakeep: unable to send request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("karakeep: failed to parse response: %s", err)
|
||||
}
|
||||
|
||||
if resp.Header.Get("Content-Type") != "application/json" {
|
||||
return fmt.Errorf("karakeep: unexpected content type response: %s", resp.Header.Get("Content-Type"))
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
var errResponse errorResponse
|
||||
if err := json.Unmarshal(responseBody, &errResponse); err != nil {
|
||||
return fmt.Errorf("karakeep: unable to parse error response: status=%d body=%s", resp.StatusCode, string(responseBody))
|
||||
}
|
||||
return fmt.Errorf("karakeep: failed to save URL: status=%d errorcode=%s %s", resp.StatusCode, errResponse.Code, errResponse.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -35,12 +35,9 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
return fmt.Errorf(`linkwarden: invalid API endpoint: %v`, err)
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(&linkwardenBookmark{
|
||||
Url: entryURL,
|
||||
Name: "",
|
||||
Description: "",
|
||||
Tags: []string{},
|
||||
Collection: map[string]interface{}{},
|
||||
requestBody, err := json.Marshal(map[string]string{
|
||||
"url": entryURL,
|
||||
"name": entryTitle,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -54,8 +51,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.AddCookie(&http.Cookie{Name: "__Secure-next-auth.session-token", Value: c.apiKey})
|
||||
request.AddCookie(&http.Cookie{Name: "next-auth.session-token", Value: c.apiKey})
|
||||
request.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
|
||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
||||
response, err := httpClient.Do(request)
|
||||
@@ -70,11 +66,3 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type linkwardenBookmark struct {
|
||||
Url string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags"`
|
||||
Collection map[string]interface{} `json:"collection"`
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pocket // import "miniflux.app/v2/internal/integration/pocket"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
// Connector manages the authorization flow with Pocket to get a personal access token.
|
||||
type Connector struct {
|
||||
consumerKey string
|
||||
}
|
||||
|
||||
// NewConnector returns a new Pocket Connector.
|
||||
func NewConnector(consumerKey string) *Connector {
|
||||
return &Connector{consumerKey}
|
||||
}
|
||||
|
||||
// RequestToken fetches a new request token from Pocket API.
|
||||
func (c *Connector) RequestToken(redirectURL string) (string, error) {
|
||||
apiEndpoint := "https://getpocket.com/v3/oauth/request"
|
||||
requestBody, err := json.Marshal(&createTokenRequest{ConsumerKey: c.consumerKey, RedirectURI: redirectURL})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pocket: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pocket: unable to create request: %v", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("X-Accept", "application/json")
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
|
||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pocket: unable to send request: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
return "", fmt.Errorf("pocket: unable get request token: url=%s status=%d", apiEndpoint, response.StatusCode)
|
||||
}
|
||||
|
||||
var result createTokenResponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return "", fmt.Errorf("pocket: unable to decode response: %v", err)
|
||||
}
|
||||
|
||||
if result.Code == "" {
|
||||
return "", errors.New("pocket: request token is empty")
|
||||
}
|
||||
|
||||
return result.Code, nil
|
||||
}
|
||||
|
||||
// AccessToken fetches a new access token once the end-user authorized the application.
|
||||
func (c *Connector) AccessToken(requestToken string) (string, error) {
|
||||
apiEndpoint := "https://getpocket.com/v3/oauth/authorize"
|
||||
requestBody, err := json.Marshal(&authorizeRequest{ConsumerKey: c.consumerKey, Code: requestToken})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pocket: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pocket: unable to create request: %v", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("X-Accept", "application/json")
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
|
||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pocket: unable to send request: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
return "", fmt.Errorf("pocket: unable get access token: url=%s status=%d", apiEndpoint, response.StatusCode)
|
||||
}
|
||||
|
||||
var result authorizeReponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return "", fmt.Errorf("pocket: unable to decode response: %v", err)
|
||||
}
|
||||
|
||||
if result.AccessToken == "" {
|
||||
return "", errors.New("pocket: access token is empty")
|
||||
}
|
||||
|
||||
return result.AccessToken, nil
|
||||
}
|
||||
|
||||
// AuthorizationURL returns the authorization URL for the end-user.
|
||||
func (c *Connector) AuthorizationURL(requestToken, redirectURL string) string {
|
||||
return fmt.Sprintf(
|
||||
"https://getpocket.com/auth/authorize?request_token=%s&redirect_uri=%s",
|
||||
requestToken,
|
||||
redirectURL,
|
||||
)
|
||||
}
|
||||
|
||||
type createTokenRequest struct {
|
||||
ConsumerKey string `json:"consumer_key"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
type createTokenResponse struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
type authorizeRequest struct {
|
||||
ConsumerKey string `json:"consumer_key"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
type authorizeReponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package pocket // import "miniflux.app/v2/internal/integration/pocket"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/version"
|
||||
)
|
||||
|
||||
const defaultClientTimeout = 10 * time.Second
|
||||
|
||||
type Client struct {
|
||||
consumerKey string
|
||||
accessToken string
|
||||
}
|
||||
|
||||
func NewClient(consumerKey, accessToken string) *Client {
|
||||
return &Client{consumerKey, accessToken}
|
||||
}
|
||||
|
||||
func (c *Client) AddURL(entryURL, entryTitle string) error {
|
||||
if c.consumerKey == "" || c.accessToken == "" {
|
||||
return fmt.Errorf("pocket: missing consumer key or access token")
|
||||
}
|
||||
|
||||
apiEndpoint := "https://getpocket.com/v3/add"
|
||||
requestBody, err := json.Marshal(&createItemRequest{
|
||||
AccessToken: c.accessToken,
|
||||
ConsumerKey: c.consumerKey,
|
||||
Title: entryTitle,
|
||||
URL: entryURL,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("pocket: unable to encode request body: %v", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("pocket: unable to create request: %v", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
||||
|
||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pocket: unable to send request: %v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
return fmt.Errorf("pocket: unable to create item: url=%s status=%d", apiEndpoint, response.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type createItemRequest struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ConsumerKey string `json:"consumer_key"`
|
||||
Title string `json:"title,omitempty"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
@@ -24,13 +24,16 @@ type BridgeMeta struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func DetectBridges(rssBridgeURL, websiteURL string) ([]*Bridge, error) {
|
||||
func DetectBridges(rssBridgeURL, rssBridgeToken, websiteURL string) ([]*Bridge, error) {
|
||||
endpointURL, err := url.Parse(rssBridgeURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("RSS-Bridge: unable to parse bridge URL: %w", err)
|
||||
}
|
||||
|
||||
values := endpointURL.Query()
|
||||
if rssBridgeToken != "" {
|
||||
values.Add("token", rssBridgeToken)
|
||||
}
|
||||
values.Add("action", "findfeed")
|
||||
values.Add("format", "atom")
|
||||
values.Add("url", websiteURL)
|
||||
@@ -78,6 +81,15 @@ func DetectBridges(rssBridgeURL, websiteURL string) ([]*Bridge, error) {
|
||||
slog.String("url", bridge.URL),
|
||||
)
|
||||
}
|
||||
|
||||
if rssBridgeToken != "" {
|
||||
bridge.URL = bridge.URL + "&token=" + rssBridgeToken
|
||||
|
||||
slog.Debug("Appended token to RSS bridge URL",
|
||||
slog.String("name", bridge.BridgeMeta.Name),
|
||||
slog.String("url", bridge.URL),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return bridgeResponse, nil
|
||||
|
||||
@@ -5,8 +5,12 @@ package telegrambot // import "miniflux.app/v2/internal/integration/telegrambot"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/model"
|
||||
"miniflux.app/v2/internal/urllib"
|
||||
)
|
||||
|
||||
func PushEntry(feed *model.Feed, entry *model.Entry, botToken, chatID string, topicID *int64, disableWebPagePreview, disableNotification bool, disableButtons bool) error {
|
||||
@@ -32,8 +36,16 @@ func PushEntry(feed *model.Feed, entry *model.Entry, botToken, chatID string, to
|
||||
if !disableButtons {
|
||||
var markupRow []*InlineKeyboardButton
|
||||
|
||||
websiteURLButton := InlineKeyboardButton{Text: "Go to website", URL: feed.SiteURL}
|
||||
markupRow = append(markupRow, &websiteURLButton)
|
||||
baseURL := config.Opts.BaseURL()
|
||||
entryPath := "/unread/entry/" + strconv.FormatInt(entry.ID, 10)
|
||||
|
||||
minifluxEntryURL, err := urllib.JoinBaseURLAndPath(baseURL, entryPath)
|
||||
if err != nil {
|
||||
slog.Error("Unable to create Miniflux entry URL", slog.Any("error", err))
|
||||
} else {
|
||||
minifluxEntryURLButton := InlineKeyboardButton{Text: "Go to Miniflux", URL: minifluxEntryURL}
|
||||
markupRow = append(markupRow, &minifluxEntryURLButton)
|
||||
}
|
||||
|
||||
articleURLButton := InlineKeyboardButton{Text: "Go to article", URL: entry.URL}
|
||||
markupRow = append(markupRow, &articleURLButton)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type translationDict map[string]interface{}
|
||||
type translationDict map[string]any
|
||||
type catalog map[string]translationDict
|
||||
|
||||
var defaultCatalog = make(catalog, len(AvailableLanguages))
|
||||
@@ -17,7 +17,7 @@ var defaultCatalog = make(catalog, len(AvailableLanguages))
|
||||
//go:embed translations/*.json
|
||||
var translationFiles embed.FS
|
||||
|
||||
func GetTranslationDict(language string) (translationDict, error) {
|
||||
func getTranslationDict(language string) (translationDict, error) {
|
||||
if _, ok := defaultCatalog[language]; !ok {
|
||||
var err error
|
||||
if defaultCatalog[language], err = loadTranslationFile(language); err != nil {
|
||||
@@ -27,21 +27,8 @@ func GetTranslationDict(language string) (translationDict, error) {
|
||||
return defaultCatalog[language], nil
|
||||
}
|
||||
|
||||
// LoadCatalogMessages loads and parses all translations encoded in JSON.
|
||||
func LoadCatalogMessages() error {
|
||||
var err error
|
||||
|
||||
for language := range AvailableLanguages {
|
||||
defaultCatalog[language], err = loadTranslationFile(language)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadTranslationFile(language string) (translationDict, error) {
|
||||
translationFileData, err := translationFiles.ReadFile(fmt.Sprintf("translations/%s.json", language))
|
||||
translationFileData, err := translationFiles.ReadFile("translations/" + language + ".json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -33,8 +33,11 @@ func TestParser(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadCatalog(t *testing.T) {
|
||||
if err := LoadCatalogMessages(); err != nil {
|
||||
t.Fatal(err)
|
||||
for language := range AvailableLanguages {
|
||||
_, err := loadTranslationFile(language)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package locale // import "miniflux.app/v2/internal/locale"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewLocalizedErrorWrapper(t *testing.T) {
|
||||
originalErr := errors.New("original error message")
|
||||
translationKey := "error.test_key"
|
||||
args := []any{"arg1", 42}
|
||||
|
||||
wrapper := NewLocalizedErrorWrapper(originalErr, translationKey, args...)
|
||||
|
||||
if wrapper.originalErr != originalErr {
|
||||
t.Errorf("Expected original error to be %v, got %v", originalErr, wrapper.originalErr)
|
||||
}
|
||||
|
||||
if wrapper.translationKey != translationKey {
|
||||
t.Errorf("Expected translation key to be %q, got %q", translationKey, wrapper.translationKey)
|
||||
}
|
||||
|
||||
if len(wrapper.translationArgs) != 2 {
|
||||
t.Errorf("Expected 2 translation args, got %d", len(wrapper.translationArgs))
|
||||
}
|
||||
|
||||
if wrapper.translationArgs[0] != "arg1" || wrapper.translationArgs[1] != 42 {
|
||||
t.Errorf("Expected translation args [arg1, 42], got %v", wrapper.translationArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalizedErrorWrapper_Error(t *testing.T) {
|
||||
originalErr := errors.New("original error message")
|
||||
wrapper := NewLocalizedErrorWrapper(originalErr, "error.test_key")
|
||||
|
||||
result := wrapper.Error()
|
||||
if result != originalErr {
|
||||
t.Errorf("Expected Error() to return original error %v, got %v", originalErr, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalizedErrorWrapper_Translate(t *testing.T) {
|
||||
// Set up test catalog
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"error.test_key": "Error: %s (code: %d)",
|
||||
},
|
||||
"fr_FR": translationDict{
|
||||
"error.test_key": "Erreur : %s (code : %d)",
|
||||
},
|
||||
}
|
||||
|
||||
originalErr := errors.New("original error")
|
||||
wrapper := NewLocalizedErrorWrapper(originalErr, "error.test_key", "test message", 404)
|
||||
|
||||
// Test English translation
|
||||
result := wrapper.Translate("en_US")
|
||||
expected := "Error: test message (code: 404)"
|
||||
if result != expected {
|
||||
t.Errorf("Expected English translation %q, got %q", expected, result)
|
||||
}
|
||||
|
||||
// Test French translation
|
||||
result = wrapper.Translate("fr_FR")
|
||||
expected = "Erreur : test message (code : 404)"
|
||||
if result != expected {
|
||||
t.Errorf("Expected French translation %q, got %q", expected, result)
|
||||
}
|
||||
|
||||
// Test with missing language (should use key as fallback with args applied)
|
||||
result = wrapper.Translate("invalid_lang")
|
||||
expected = "error.test_key%!(EXTRA string=test message, int=404)"
|
||||
if result != expected {
|
||||
t.Errorf("Expected fallback translation %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalizedErrorWrapper_TranslateWithEmptyKey(t *testing.T) {
|
||||
originalErr := errors.New("original error message")
|
||||
wrapper := NewLocalizedErrorWrapper(originalErr, "")
|
||||
|
||||
result := wrapper.Translate("en_US")
|
||||
expected := "original error message"
|
||||
if result != expected {
|
||||
t.Errorf("Expected original error message %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalizedErrorWrapper_TranslateWithNoArgs(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"error.simple": "Simple error message",
|
||||
},
|
||||
}
|
||||
|
||||
originalErr := errors.New("original error")
|
||||
wrapper := NewLocalizedErrorWrapper(originalErr, "error.simple")
|
||||
|
||||
result := wrapper.Translate("en_US")
|
||||
expected := "Simple error message"
|
||||
if result != expected {
|
||||
t.Errorf("Expected translation %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLocalizedError(t *testing.T) {
|
||||
translationKey := "error.validation"
|
||||
args := []any{"field1", "invalid"}
|
||||
|
||||
localizedErr := NewLocalizedError(translationKey, args...)
|
||||
|
||||
if localizedErr.translationKey != translationKey {
|
||||
t.Errorf("Expected translation key to be %q, got %q", translationKey, localizedErr.translationKey)
|
||||
}
|
||||
|
||||
if len(localizedErr.translationArgs) != 2 {
|
||||
t.Errorf("Expected 2 translation args, got %d", len(localizedErr.translationArgs))
|
||||
}
|
||||
|
||||
if localizedErr.translationArgs[0] != "field1" || localizedErr.translationArgs[1] != "invalid" {
|
||||
t.Errorf("Expected translation args [field1, invalid], got %v", localizedErr.translationArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalizedError_String(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"error.validation": "Validation failed for %s: %s",
|
||||
},
|
||||
}
|
||||
|
||||
localizedErr := NewLocalizedError("error.validation", "username", "too short")
|
||||
|
||||
result := localizedErr.String()
|
||||
expected := "Validation failed for username: too short"
|
||||
if result != expected {
|
||||
t.Errorf("Expected String() result %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalizedError_StringWithMissingTranslation(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{},
|
||||
}
|
||||
|
||||
localizedErr := NewLocalizedError("error.missing", "arg1")
|
||||
|
||||
result := localizedErr.String()
|
||||
expected := "error.missing%!(EXTRA string=arg1)"
|
||||
if result != expected {
|
||||
t.Errorf("Expected String() result %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalizedError_Error(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"error.database": "Database connection failed: %s",
|
||||
},
|
||||
}
|
||||
|
||||
localizedErr := NewLocalizedError("error.database", "timeout")
|
||||
|
||||
result := localizedErr.Error()
|
||||
if result == nil {
|
||||
t.Error("Expected Error() to return a non-nil error")
|
||||
}
|
||||
|
||||
expected := "Database connection failed: timeout"
|
||||
if result.Error() != expected {
|
||||
t.Errorf("Expected Error() message %q, got %q", expected, result.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalizedError_Translate(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"error.permission": "Permission denied for %s",
|
||||
},
|
||||
"es_ES": translationDict{
|
||||
"error.permission": "Permiso denegado para %s",
|
||||
},
|
||||
}
|
||||
|
||||
localizedErr := NewLocalizedError("error.permission", "admin panel")
|
||||
|
||||
// Test English translation
|
||||
result := localizedErr.Translate("en_US")
|
||||
expected := "Permission denied for admin panel"
|
||||
if result != expected {
|
||||
t.Errorf("Expected English translation %q, got %q", expected, result)
|
||||
}
|
||||
|
||||
// Test Spanish translation
|
||||
result = localizedErr.Translate("es_ES")
|
||||
expected = "Permiso denegado para admin panel"
|
||||
if result != expected {
|
||||
t.Errorf("Expected Spanish translation %q, got %q", expected, result)
|
||||
}
|
||||
|
||||
// Test with missing language
|
||||
result = localizedErr.Translate("invalid_lang")
|
||||
expected = "error.permission%!(EXTRA string=admin panel)"
|
||||
if result != expected {
|
||||
t.Errorf("Expected fallback translation %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalizedError_TranslateWithNoArgs(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"error.generic": "An error occurred",
|
||||
},
|
||||
"de_DE": translationDict{
|
||||
"error.generic": "Ein Fehler ist aufgetreten",
|
||||
},
|
||||
}
|
||||
|
||||
localizedErr := NewLocalizedError("error.generic")
|
||||
|
||||
// Test English
|
||||
result := localizedErr.Translate("en_US")
|
||||
expected := "An error occurred"
|
||||
if result != expected {
|
||||
t.Errorf("Expected English translation %q, got %q", expected, result)
|
||||
}
|
||||
|
||||
// Test German
|
||||
result = localizedErr.Translate("de_DE")
|
||||
expected = "Ein Fehler ist aufgetreten"
|
||||
if result != expected {
|
||||
t.Errorf("Expected German translation %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalizedError_TranslateWithComplexArgs(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"error.complex": "Error %d: %s occurred at %s with severity %s",
|
||||
},
|
||||
}
|
||||
|
||||
localizedErr := NewLocalizedError("error.complex", 500, "Internal Server Error", "2024-01-01", "high")
|
||||
|
||||
result := localizedErr.Translate("en_US")
|
||||
expected := "Error 500: Internal Server Error occurred at 2024-01-01 with severity high"
|
||||
if result != expected {
|
||||
t.Errorf("Expected complex translation %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalizedErrorWrapper_WithNilError(t *testing.T) {
|
||||
// This tests edge case behavior - what happens with nil error
|
||||
wrapper := NewLocalizedErrorWrapper(nil, "error.test")
|
||||
|
||||
// Error() should return nil
|
||||
result := wrapper.Error()
|
||||
if result != nil {
|
||||
t.Errorf("Expected Error() to return nil, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalizedError_EmptyKey(t *testing.T) {
|
||||
localizedErr := NewLocalizedError("")
|
||||
|
||||
result := localizedErr.String()
|
||||
expected := ""
|
||||
if result != expected {
|
||||
t.Errorf("Expected empty string for empty key, got %q", result)
|
||||
}
|
||||
|
||||
result = localizedErr.Translate("en_US")
|
||||
if result != expected {
|
||||
t.Errorf("Expected empty string for empty key translation, got %q", result)
|
||||
}
|
||||
}
|
||||
+27
-71
@@ -5,16 +5,9 @@ package locale // import "miniflux.app/v2/internal/locale"
|
||||
|
||||
// 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]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 {
|
||||
func getPluralForm(lang string, n int) int {
|
||||
switch lang {
|
||||
case "ar_AR":
|
||||
switch {
|
||||
case n == 0:
|
||||
return 0
|
||||
@@ -26,90 +19,53 @@ var pluralForms = map[string]func(n int) int{
|
||||
return 3
|
||||
case n%100 >= 11:
|
||||
return 4
|
||||
default:
|
||||
return 5
|
||||
}
|
||||
return 5
|
||||
},
|
||||
// nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;
|
||||
"cs_CZ": func(n int) int {
|
||||
case "cs_CZ":
|
||||
switch {
|
||||
case n == 1:
|
||||
return 0
|
||||
case n >= 2 && n <= 4:
|
||||
return 1
|
||||
default:
|
||||
return 2
|
||||
}
|
||||
return 2
|
||||
},
|
||||
// nplurals=2; plural=(n > 1);
|
||||
"fr_FR": func(n int) int {
|
||||
if n > 1 {
|
||||
return 1
|
||||
}
|
||||
case "id_ID", "ja_JP":
|
||||
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 {
|
||||
case "pl_PL":
|
||||
switch {
|
||||
case n == 1:
|
||||
return 0
|
||||
case n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20):
|
||||
return 1
|
||||
default:
|
||||
return 2
|
||||
}
|
||||
return 2
|
||||
},
|
||||
// nplurals=2; plural=(n > 1);
|
||||
"pt_BR": func(n int) int {
|
||||
if n > 1 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
},
|
||||
// nplurals=3; plural=(n==1 ? 0 : n==0 || (n%100 > 0 && n%100 < 20) ? 1 : 2);
|
||||
"ro_RO": func(n int) int {
|
||||
case "ro_RO":
|
||||
switch {
|
||||
case n == 1:
|
||||
return 0
|
||||
case n == 0 || (n%100 > 0 && n%100 < 20):
|
||||
return 1
|
||||
default:
|
||||
return 2
|
||||
}
|
||||
return 2
|
||||
},
|
||||
"ru_RU": pluralFormRuSrUa,
|
||||
// nplurals=2; plural=(n > 1);
|
||||
"tr_TR": func(n int) int {
|
||||
case "ru_RU", "uk_UA", "sr_RS":
|
||||
switch {
|
||||
case n%10 == 1 && n%100 != 11:
|
||||
return 0
|
||||
case n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20):
|
||||
return 1
|
||||
default:
|
||||
return 2
|
||||
}
|
||||
case "zh_CN", "zh_TW", "nan_Latn_pehoeji":
|
||||
return 0
|
||||
default: // includes fr_FR, pr_BR, tr_TR
|
||||
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
|
||||
},
|
||||
"nan_Latn_pehoeji": 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 {
|
||||
switch {
|
||||
case n%10 == 1 && n%100 != 11:
|
||||
return 0
|
||||
case n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20):
|
||||
return 1
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
+156
-48
@@ -7,90 +7,198 @@ import "testing"
|
||||
|
||||
func TestPluralRules(t *testing.T) {
|
||||
scenarios := map[string]map[int]int{
|
||||
// Default rule (covers fr_FR, pt_BR, tr_TR, and other unlisted languages)
|
||||
"default": {
|
||||
1: 0,
|
||||
2: 1,
|
||||
5: 1,
|
||||
0: 0, // n <= 1
|
||||
1: 0, // n <= 1
|
||||
2: 1, // n > 1
|
||||
5: 1, // n > 1
|
||||
},
|
||||
// Arabic (ar_AR) - 6 forms
|
||||
"ar_AR": {
|
||||
0: 0,
|
||||
1: 1,
|
||||
2: 2,
|
||||
5: 3,
|
||||
11: 4,
|
||||
200: 5,
|
||||
0: 0, // n == 0
|
||||
1: 1, // n == 1
|
||||
2: 2, // n == 2
|
||||
3: 3, // n%100 >= 3 && n%100 <= 10
|
||||
5: 3, // n%100 >= 3 && n%100 <= 10
|
||||
10: 3, // n%100 >= 3 && n%100 <= 10
|
||||
11: 4, // n%100 >= 11
|
||||
15: 4, // n%100 >= 11
|
||||
99: 4, // n%100 >= 11
|
||||
100: 5, // default case (n%100 == 0, doesn't match any condition)
|
||||
101: 5, // default case (n%100 == 1, but n != 1)
|
||||
200: 5, // default case
|
||||
},
|
||||
// Czech (cs_CZ) - 3 forms
|
||||
"cs_CZ": {
|
||||
1: 0,
|
||||
2: 1,
|
||||
5: 2,
|
||||
1: 0, // n == 1
|
||||
2: 1, // n >= 2 && n <= 4
|
||||
3: 1, // n >= 2 && n <= 4
|
||||
4: 1, // n >= 2 && n <= 4
|
||||
5: 2, // default case
|
||||
},
|
||||
// French (fr_FR) - uses default rule
|
||||
"fr_FR": {
|
||||
1: 0,
|
||||
2: 1,
|
||||
5: 1,
|
||||
0: 0, // n <= 1
|
||||
1: 0, // n <= 1
|
||||
2: 1, // n > 1
|
||||
5: 1, // n > 1
|
||||
},
|
||||
// Indonesian (id_ID) - always form 0
|
||||
"id_ID": {
|
||||
1: 0,
|
||||
5: 0,
|
||||
0: 0,
|
||||
1: 0,
|
||||
5: 0,
|
||||
100: 0,
|
||||
},
|
||||
// Japanese (ja_JP) - always form 0
|
||||
"ja_JP": {
|
||||
1: 0,
|
||||
2: 0,
|
||||
5: 0,
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
5: 0,
|
||||
100: 0,
|
||||
},
|
||||
// Polish (pl_PL) - 3 forms
|
||||
"pl_PL": {
|
||||
1: 0,
|
||||
2: 1,
|
||||
5: 2,
|
||||
1: 0, // n == 1
|
||||
2: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20)
|
||||
3: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20)
|
||||
4: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20)
|
||||
5: 2, // default case
|
||||
10: 2, // default case (n%100 < 10, but n%10 not in 2-4)
|
||||
11: 2, // default case (n%100 >= 10 and < 20)
|
||||
12: 2, // default case (n%100 >= 10 and < 20)
|
||||
22: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 >= 20)
|
||||
24: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 >= 20)
|
||||
},
|
||||
// Portuguese Brazilian (pt_BR) - uses default rule
|
||||
"pt_BR": {
|
||||
1: 0,
|
||||
2: 1,
|
||||
5: 1,
|
||||
0: 0, // n <= 1
|
||||
1: 0, // n <= 1
|
||||
2: 1, // n > 1
|
||||
5: 1, // n > 1
|
||||
},
|
||||
// Romanian (ro_RO) - 3 forms
|
||||
"ro_RO": {
|
||||
1: 0,
|
||||
2: 1,
|
||||
5: 1,
|
||||
0: 1, // n == 0 || (n%100 > 0 && n%100 < 20)
|
||||
1: 0, // n == 1
|
||||
2: 1, // n == 0 || (n%100 > 0 && n%100 < 20)
|
||||
5: 1, // n == 0 || (n%100 > 0 && n%100 < 20)
|
||||
19: 1, // n == 0 || (n%100 > 0 && n%100 < 20)
|
||||
20: 2, // default case
|
||||
21: 2, // default case
|
||||
100: 2, // default case (n%100 == 0, so condition fails)
|
||||
101: 1, // n%100 == 1, so n%100 > 0 && n%100 < 20
|
||||
},
|
||||
// Russian (ru_RU) - 3 forms
|
||||
"ru_RU": {
|
||||
1: 0,
|
||||
2: 1,
|
||||
5: 2,
|
||||
1: 0, // n%10 == 1 && n%100 != 11
|
||||
2: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20)
|
||||
3: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20)
|
||||
4: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20)
|
||||
5: 2, // default case
|
||||
11: 2, // n%10 == 1 but n%100 == 11, so default case
|
||||
12: 2, // default case
|
||||
21: 0, // n%10 == 1 && n%100 != 11
|
||||
22: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 >= 20)
|
||||
},
|
||||
// Serbian (sr_RS) - same as Russian
|
||||
"sr_RS": {
|
||||
1: 0,
|
||||
2: 1,
|
||||
5: 2,
|
||||
1: 0, // n%10 == 1 && n%100 != 11
|
||||
2: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20)
|
||||
5: 2, // default case
|
||||
11: 2, // n%10 == 1 but n%100 == 11, so default case
|
||||
21: 0, // n%10 == 1 && n%100 != 11
|
||||
},
|
||||
// Turkish (tr_TR) - uses default rule
|
||||
"tr_TR": {
|
||||
1: 0,
|
||||
2: 1,
|
||||
5: 1,
|
||||
0: 0, // n <= 1
|
||||
1: 0, // n <= 1
|
||||
2: 1, // n > 1
|
||||
5: 1, // n > 1
|
||||
},
|
||||
// Ukrainian (uk_UA) - same as Russian
|
||||
"uk_UA": {
|
||||
1: 0,
|
||||
2: 1,
|
||||
5: 2,
|
||||
1: 0, // n%10 == 1 && n%100 != 11
|
||||
2: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20)
|
||||
5: 2, // default case
|
||||
11: 2, // n%10 == 1 but n%100 == 11, so default case
|
||||
21: 0, // n%10 == 1 && n%100 != 11
|
||||
},
|
||||
// Chinese Simplified (zh_CN) - always form 0
|
||||
"zh_CN": {
|
||||
1: 0,
|
||||
5: 0,
|
||||
0: 0,
|
||||
1: 0,
|
||||
5: 0,
|
||||
100: 0,
|
||||
},
|
||||
// Chinese Traditional (zh_TW) - always form 0
|
||||
"zh_TW": {
|
||||
1: 0,
|
||||
5: 0,
|
||||
0: 0,
|
||||
1: 0,
|
||||
5: 0,
|
||||
100: 0,
|
||||
},
|
||||
// Min Nan (nan_Latn_pehoeji) - always form 0
|
||||
"nan_Latn_pehoeji": {
|
||||
1: 0,
|
||||
5: 0,
|
||||
0: 0,
|
||||
1: 0,
|
||||
5: 0,
|
||||
100: 0,
|
||||
},
|
||||
// Additional languages from AvailableLanguages that use default rule
|
||||
"de_DE": {
|
||||
0: 0, // n <= 1
|
||||
1: 0, // n <= 1
|
||||
2: 1, // n > 1
|
||||
},
|
||||
"el_EL": {
|
||||
0: 0, // n <= 1
|
||||
1: 0, // n <= 1
|
||||
2: 1, // n > 1
|
||||
},
|
||||
"en_US": {
|
||||
0: 0, // n <= 1
|
||||
1: 0, // n <= 1
|
||||
2: 1, // n > 1
|
||||
},
|
||||
"es_ES": {
|
||||
0: 0, // n <= 1
|
||||
1: 0, // n <= 1
|
||||
2: 1, // n > 1
|
||||
},
|
||||
"fi_FI": {
|
||||
0: 0, // n <= 1
|
||||
1: 0, // n <= 1
|
||||
2: 1, // n > 1
|
||||
},
|
||||
"hi_IN": {
|
||||
0: 0, // n <= 1
|
||||
1: 0, // n <= 1
|
||||
2: 1, // n > 1
|
||||
},
|
||||
"it_IT": {
|
||||
0: 0, // n <= 1
|
||||
1: 0, // n <= 1
|
||||
2: 1, // n > 1
|
||||
},
|
||||
"nl_NL": {
|
||||
0: 0, // n <= 1
|
||||
1: 0, // n <= 1
|
||||
2: 1, // n > 1
|
||||
},
|
||||
// Test a language not in the switch (should use default rule)
|
||||
"unknown_language": {
|
||||
0: 0, // n <= 1
|
||||
1: 0, // n <= 1
|
||||
2: 1, // n > 1
|
||||
},
|
||||
}
|
||||
|
||||
for rule, values := range scenarios {
|
||||
for input, expected := range values {
|
||||
result := pluralForms[rule](input)
|
||||
result := getPluralForm(rule, input)
|
||||
if result != expected {
|
||||
t.Errorf(`Unexpected result for %q rule, got %d instead of %d for %d as input`, rule, result, expected, input)
|
||||
}
|
||||
|
||||
+15
-23
@@ -10,8 +10,13 @@ type Printer struct {
|
||||
language string
|
||||
}
|
||||
|
||||
// NewPrinter creates a new Printer instance for the given language.
|
||||
func NewPrinter(language string) *Printer {
|
||||
return &Printer{language}
|
||||
}
|
||||
|
||||
func (p *Printer) Print(key string) string {
|
||||
if dict, err := GetTranslationDict(p.language); err == nil {
|
||||
if dict, err := getTranslationDict(p.language); err == nil {
|
||||
if str, ok := dict[key]; ok {
|
||||
if translation, ok := str.(string); ok {
|
||||
return translation
|
||||
@@ -22,15 +27,12 @@ func (p *Printer) Print(key string) string {
|
||||
}
|
||||
|
||||
// Printf is like fmt.Printf, but using language-specific formatting.
|
||||
func (p *Printer) Printf(key string, args ...interface{}) string {
|
||||
func (p *Printer) Printf(key string, args ...any) string {
|
||||
translation := key
|
||||
|
||||
if dict, err := GetTranslationDict(p.language); err == nil {
|
||||
str, found := dict[key]
|
||||
if found {
|
||||
var valid bool
|
||||
translation, valid = str.(string)
|
||||
if !valid {
|
||||
if dict, err := getTranslationDict(p.language); err == nil {
|
||||
if str, ok := dict[key]; ok {
|
||||
if translation, ok = str.(string); !ok {
|
||||
translation = key
|
||||
}
|
||||
}
|
||||
@@ -41,7 +43,7 @@ func (p *Printer) Printf(key string, args ...interface{}) string {
|
||||
|
||||
// Plural returns the translation of the given key by using the language plural form.
|
||||
func (p *Printer) Plural(key string, n int, args ...interface{}) string {
|
||||
dict, err := GetTranslationDict(p.language)
|
||||
dict, err := getTranslationDict(p.language)
|
||||
if err != nil {
|
||||
return key
|
||||
}
|
||||
@@ -50,22 +52,17 @@ func (p *Printer) Plural(key string, n int, args ...interface{}) string {
|
||||
var plurals []string
|
||||
|
||||
switch v := choices.(type) {
|
||||
case []interface{}:
|
||||
case []string:
|
||||
plurals = v
|
||||
case []any:
|
||||
for _, v := range v {
|
||||
plurals = append(plurals, fmt.Sprint(v))
|
||||
}
|
||||
case []string:
|
||||
plurals = v
|
||||
default:
|
||||
return key
|
||||
}
|
||||
|
||||
pluralForm, found := pluralForms[p.language]
|
||||
if !found {
|
||||
pluralForm = pluralForms["default"]
|
||||
}
|
||||
|
||||
index := pluralForm(n)
|
||||
index := getPluralForm(p.language, n)
|
||||
if len(plurals) > index {
|
||||
return fmt.Sprintf(plurals[index], args...)
|
||||
}
|
||||
@@ -73,8 +70,3 @@ func (p *Printer) Plural(key string, n int, args ...interface{}) string {
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
// NewPrinter creates a new Printer.
|
||||
func NewPrinter(language string) *Printer {
|
||||
return &Printer{language}
|
||||
}
|
||||
|
||||
+260
-10
@@ -5,7 +5,7 @@ package locale // import "miniflux.app/v2/internal/locale"
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTranslateWithMissingLanguage(t *testing.T) {
|
||||
func TestPrintfWithMissingLanguage(t *testing.T) {
|
||||
defaultCatalog = catalog{}
|
||||
translation := NewPrinter("invalid").Printf("missing.key")
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestTranslateWithMissingLanguage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateWithMissingKey(t *testing.T) {
|
||||
func TestPrintfWithMissingKey(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"k": "v",
|
||||
@@ -27,7 +27,7 @@ func TestTranslateWithMissingKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateWithExistingKey(t *testing.T) {
|
||||
func TestPrintfWithExistingKey(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"auth.username": "Login",
|
||||
@@ -40,7 +40,7 @@ func TestTranslateWithExistingKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateWithExistingKeyAndPlaceholder(t *testing.T) {
|
||||
func TestPrintfWithExistingKeyAndPlaceholder(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"key": "Test: %s",
|
||||
@@ -56,7 +56,7 @@ func TestTranslateWithExistingKeyAndPlaceholder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateWithMissingKeyAndPlaceholder(t *testing.T) {
|
||||
func TestPrintfWithMissingKeyAndPlaceholder(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"auth.username": "Login",
|
||||
@@ -72,7 +72,7 @@ func TestTranslateWithMissingKeyAndPlaceholder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslateWithInvalidValue(t *testing.T) {
|
||||
func TestPrintfWithInvalidValue(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"auth.username": "Login",
|
||||
@@ -88,7 +88,134 @@ func TestTranslateWithInvalidValue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslatePluralWithDefaultRule(t *testing.T) {
|
||||
func TestPrintWithMissingLanguage(t *testing.T) {
|
||||
defaultCatalog = catalog{}
|
||||
translation := NewPrinter("invalid").Print("missing.key")
|
||||
|
||||
if translation != "missing.key" {
|
||||
t.Errorf(`Wrong translation, got %q`, translation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintWithMissingKey(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"existing.key": "value",
|
||||
},
|
||||
}
|
||||
|
||||
translation := NewPrinter("en_US").Print("missing.key")
|
||||
if translation != "missing.key" {
|
||||
t.Errorf(`Wrong translation, got %q`, translation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintWithExistingKey(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"auth.username": "Login",
|
||||
},
|
||||
}
|
||||
|
||||
translation := NewPrinter("en_US").Print("auth.username")
|
||||
if translation != "Login" {
|
||||
t.Errorf(`Wrong translation, got %q`, translation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintWithDifferentLanguages(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"greeting": "Hello",
|
||||
},
|
||||
"fr_FR": translationDict{
|
||||
"greeting": "Bonjour",
|
||||
},
|
||||
"es_ES": translationDict{
|
||||
"greeting": "Hola",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
language string
|
||||
expected string
|
||||
}{
|
||||
{"en_US", "Hello"},
|
||||
{"fr_FR", "Bonjour"},
|
||||
{"es_ES", "Hola"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
translation := NewPrinter(test.language).Print("greeting")
|
||||
if translation != test.expected {
|
||||
t.Errorf(`Wrong translation for %s, got %q instead of %q`, test.language, translation, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintWithInvalidTranslationType(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"valid.key": "valid string",
|
||||
"invalid.key": 12345, // not a string
|
||||
},
|
||||
}
|
||||
|
||||
printer := NewPrinter("en_US")
|
||||
|
||||
// Valid string should work
|
||||
translation := printer.Print("valid.key")
|
||||
if translation != "valid string" {
|
||||
t.Errorf(`Wrong translation for valid key, got %q`, translation)
|
||||
}
|
||||
|
||||
// Invalid type should return the key itself
|
||||
translation = printer.Print("invalid.key")
|
||||
if translation != "invalid.key" {
|
||||
t.Errorf(`Wrong translation for invalid key, got %q`, translation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintWithNilTranslation(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"nil.key": nil,
|
||||
},
|
||||
}
|
||||
|
||||
translation := NewPrinter("en_US").Print("nil.key")
|
||||
if translation != "nil.key" {
|
||||
t.Errorf(`Wrong translation for nil value, got %q`, translation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintWithEmptyKey(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"": "empty key translation",
|
||||
},
|
||||
}
|
||||
|
||||
translation := NewPrinter("en_US").Print("")
|
||||
if translation != "empty key translation" {
|
||||
t.Errorf(`Wrong translation for empty key, got %q`, translation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintWithEmptyTranslation(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"empty.value": "",
|
||||
},
|
||||
}
|
||||
|
||||
translation := NewPrinter("en_US").Print("empty.value")
|
||||
if translation != "" {
|
||||
t.Errorf(`Wrong translation for empty value, got %q`, translation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluralWithDefaultRule(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"number_of_users": []string{"%d user (%s)", "%d users (%s)"},
|
||||
@@ -112,7 +239,7 @@ func TestTranslatePluralWithDefaultRule(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslatePluralWithRussianRule(t *testing.T) {
|
||||
func TestPluralWithRussianRule(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"time_elapsed.years": []string{"%d year", "%d years"},
|
||||
@@ -143,7 +270,7 @@ func TestTranslatePluralWithRussianRule(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslatePluralWithMissingTranslation(t *testing.T) {
|
||||
func TestPluralWithMissingTranslation(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"number_of_users": []string{"%d user (%s)", "%d users (%s)"},
|
||||
@@ -157,7 +284,7 @@ func TestTranslatePluralWithMissingTranslation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslatePluralWithInvalidValues(t *testing.T) {
|
||||
func TestPluralWithInvalidValues(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"number_of_users": []string{"%d user (%s)", "%d users (%s)"},
|
||||
@@ -172,3 +299,126 @@ func TestTranslatePluralWithInvalidValues(t *testing.T) {
|
||||
t.Errorf(`Wrong translation, got %q instead of %q`, translation, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluralWithMissingLanguage(t *testing.T) {
|
||||
defaultCatalog = catalog{}
|
||||
translation := NewPrinter("invalid_language").Plural("test.key", 2)
|
||||
expected := "test.key"
|
||||
if translation != expected {
|
||||
t.Errorf(`Wrong translation, got %q instead of %q`, translation, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluralWithAnySliceType(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"test.key": []any{"%d item", "%d items"},
|
||||
},
|
||||
}
|
||||
|
||||
printer := NewPrinter("en_US")
|
||||
|
||||
translation := printer.Plural("test.key", 1, 1)
|
||||
expected := "1 item"
|
||||
if translation != expected {
|
||||
t.Errorf(`Wrong translation for singular, got %q instead of %q`, translation, expected)
|
||||
}
|
||||
|
||||
translation = printer.Plural("test.key", 2, 2)
|
||||
expected = "2 items"
|
||||
if translation != expected {
|
||||
t.Errorf(`Wrong translation for plural, got %q instead of %q`, translation, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluralWithMixedAnySliceTypes(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"en_US": translationDict{
|
||||
"mixed.key": []any{"single: %s", "multiple: %s", "many: %s"},
|
||||
},
|
||||
}
|
||||
|
||||
printer := NewPrinter("en_US")
|
||||
|
||||
// Test first element (should convert first any element to string)
|
||||
translation := printer.Plural("mixed.key", 0, "test") // n=0 uses index 0
|
||||
expected := "single: test"
|
||||
if translation != expected {
|
||||
t.Errorf(`Wrong translation for index 0, got %q instead of %q`, translation, expected)
|
||||
}
|
||||
|
||||
// Test second element (should use plural form)
|
||||
translation = printer.Plural("mixed.key", 2, "items") // plural form for default language
|
||||
expected = "multiple: items"
|
||||
if translation != expected {
|
||||
t.Errorf(`Wrong translation for index 1, got %q instead of %q`, translation, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluralWithIndexOutOfBounds(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"test_lang": translationDict{
|
||||
"limited.key": []string{"only one form"},
|
||||
},
|
||||
}
|
||||
|
||||
// Force a scenario where getPluralForm might return an index >= len(plurals)
|
||||
// We'll create a scenario with Czech language rules
|
||||
defaultCatalog["cs_CZ"] = translationDict{
|
||||
"limited.key": []string{"one form only"}, // Only one form, but Czech has 3 plural forms
|
||||
}
|
||||
|
||||
printer := NewPrinter("cs_CZ")
|
||||
// n=5 should return index 2 for Czech, but we only have 1 form (index 0)
|
||||
translation := printer.Plural("limited.key", 5)
|
||||
expected := "limited.key"
|
||||
if translation != expected {
|
||||
t.Errorf(`Wrong translation for out of bounds index, got %q instead of %q`, translation, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluralWithVariousLanguageRules(t *testing.T) {
|
||||
defaultCatalog = catalog{
|
||||
"ar_AR": translationDict{
|
||||
"items": []string{"no items", "one item", "two items", "few items", "many items", "other items"},
|
||||
},
|
||||
"pl_PL": translationDict{
|
||||
"files": []string{"one file", "few files", "many files"},
|
||||
},
|
||||
"ja_JP": translationDict{
|
||||
"photos": []string{"photos"},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
language string
|
||||
key string
|
||||
n int
|
||||
expected string
|
||||
}{
|
||||
// Arabic tests
|
||||
{"ar_AR", "items", 0, "no items"},
|
||||
{"ar_AR", "items", 1, "one item"},
|
||||
{"ar_AR", "items", 2, "two items"},
|
||||
{"ar_AR", "items", 5, "few items"}, // n%100 >= 3 && n%100 <= 10
|
||||
{"ar_AR", "items", 15, "many items"}, // n%100 >= 11
|
||||
|
||||
// Polish tests
|
||||
{"pl_PL", "files", 1, "one file"},
|
||||
{"pl_PL", "files", 3, "few files"}, // n%10 >= 2 && n%10 <= 4
|
||||
{"pl_PL", "files", 5, "many files"}, // default case
|
||||
|
||||
// Japanese tests (always uses same form)
|
||||
{"ja_JP", "photos", 1, "photos"},
|
||||
{"ja_JP", "photos", 10, "photos"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
printer := NewPrinter(test.language)
|
||||
translation := printer.Plural(test.key, test.n)
|
||||
if translation != test.expected {
|
||||
t.Errorf(`Wrong translation for %s with n=%d, got %q instead of %q`,
|
||||
test.language, test.n, translation, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"alert.no_tag_entry": "Es gibt keine Artikel, die diesem Tag entsprechen.",
|
||||
"alert.no_unread_entry": "Es existiert kein ungelesener Artikel.",
|
||||
"alert.no_user": "Sie sind der einzige Benutzer.",
|
||||
"alert.pocket_linked": "Ihr Pocket-Konto ist jetzt verknüpft!",
|
||||
"alert.prefs_saved": "Einstellungen gespeichert!",
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"Sie haben zu viele Aktualisierungen ausgelöst. Bitte warten Sie %d Minute, bevor Sie es erneut versuchen.",
|
||||
@@ -72,11 +71,15 @@
|
||||
"entry.state.loading": "Lade...",
|
||||
"entry.state.saving": "Speichern...",
|
||||
"entry.status.mark_as_read": "Als gelesen markieren",
|
||||
"entry.status.mark_as_unread": "Als ungelesen markieren",
|
||||
"entry.status.title": "Status des Artikels ändern",
|
||||
"entry.status.toast.read": "Als gelesen markiert",
|
||||
"entry.status.toast.unread": "Als ungelesen markiert",
|
||||
"entry.status.mark_as_unread": "Als ungelesen markieren",
|
||||
"entry.tags.label": "Stichworte:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Zeige %d weiteres Schlagwort",
|
||||
"Zeige %d weitere Schlagwörter"
|
||||
],
|
||||
"entry.unshare.label": "Nicht teilen",
|
||||
"error.api_key_already_exists": "Dieser API-Schlüssel ist bereits vorhanden.",
|
||||
"error.bad_credentials": "Benutzername oder Passwort ungültig.",
|
||||
@@ -114,9 +117,12 @@
|
||||
"error.http_service_unavailable": "Die Webseite ist aufgrund eines Internal-Server-Fehlers derzeit nicht verfügbar. Das Problem liegt nicht bei Miniflux. Bitte versuchen Sie es später erneut.",
|
||||
"error.http_too_many_requests": "Miniflux hat zu viele Anfragen an diese Webseite gestellt. Bitte versuchen Sie es später erneut oder ändern Sie die Konfiguration der Anwendung.",
|
||||
"error.http_unexpected_status_code": "Die Webseite ist aufgrund eines eines unerwarteten HTTP-Fehlers derzeit nicht verfügbar: %d. Das Problem liegt nicht bei Miniflux. Bitte versuchen Sie es später erneut.",
|
||||
"error.invalid_categories_sorting_order": "Ungültige Kategorie-Sortierreihenfolge.",
|
||||
"error.invalid_default_home_page": "Ungültige Standard-Startseite!",
|
||||
"error.invalid_display_mode": "Progressive-Web-App- (PWA-)Anzeigemodus",
|
||||
"error.invalid_entry_direction": "Ungültige Sortierreihenfolge.",
|
||||
"error.invalid_entry_order": "Ungültige Sortierreihenfolge.",
|
||||
"error.invalid_feed_proxy_url": "Ungültige Proxy-URL.",
|
||||
"error.invalid_feed_url": "Ungültiger Feed-URL.",
|
||||
"error.invalid_gesture_nav": "Ungültige Gestennavigation.",
|
||||
"error.invalid_language": "Ungültige Sprache.",
|
||||
@@ -126,8 +132,7 @@
|
||||
"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.password_min_length": "Wenigstens 6 Zeichen müssen genutzt werden.",
|
||||
"error.pocket_access_token": "Zugriffstoken konnte nicht von Pocket abgerufen werden!",
|
||||
"error.pocket_request_token": "Anfrage-Token konnte nicht von Pocket abgerufen werden!",
|
||||
"error.proxy_url_not_empty": "Die Proxy-URL darf nicht leer sein.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Ungültige Blockierregel: Regel #%d hat keinen gültigen Feldnamen (Optionen: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Ungültige Blockierregel: Das Muster für Regel #%d ist kein zulässiger regulärer Ausdruck",
|
||||
"error.settings_block_rule_regex_required": "Ungültige Blockierregel: Regel #%d hat kein Muster",
|
||||
@@ -164,7 +169,8 @@
|
||||
"form.feed.fieldset.rules": "Regeln",
|
||||
"form.feed.label.allow_self_signed_certificates": "Erlaube selbstsignierte oder ungültige Zertifikate",
|
||||
"form.feed.label.apprise_service_urls": "Kommaseparierte Liste der Apprise-Service-URLs",
|
||||
"form.feed.label.blocklist_rules": "Blockierregeln",
|
||||
"form.feed.label.block_filter_entry_rules": "Eintrags-Sperrregeln",
|
||||
"form.feed.label.blocklist_rules": "Regex-basierte Sperrfilter",
|
||||
"form.feed.label.category": "Kategorie",
|
||||
"form.feed.label.cookie": "Cookies setzen",
|
||||
"form.feed.label.crawler": "Originalinhalt herunterladen",
|
||||
@@ -174,10 +180,11 @@
|
||||
"form.feed.label.feed_password": "Passwort des Abonnements",
|
||||
"form.feed.label.feed_url": "URL des Abonnements",
|
||||
"form.feed.label.feed_username": "Benutzername des Abonnements",
|
||||
"form.feed.label.fetch_via_proxy": "Über Proxy abrufen",
|
||||
"form.feed.label.fetch_via_proxy": "Den auf Anwendungsebene konfigurierten Proxy verwenden",
|
||||
"form.feed.label.hide_globally": "Einträge in der globalen Ungelesen-Liste ausblenden",
|
||||
"form.feed.label.ignore_http_cache": "Ignoriere HTTP-Cache",
|
||||
"form.feed.label.keeplist_rules": "Erlaubnisregeln",
|
||||
"form.feed.label.keep_filter_entry_rules": "Eintrags-Erlaubnisregeln",
|
||||
"form.feed.label.keeplist_rules": "Regex-basierte Behalte-Filter",
|
||||
"form.feed.label.no_media_player": "Kein Media-Player (Audio/Video)",
|
||||
"form.feed.label.ntfy_activate": "Einträge zu ntfy pushen",
|
||||
"form.feed.label.ntfy_default_priority": "Normale Ntfy-Priorität",
|
||||
@@ -187,14 +194,15 @@
|
||||
"form.feed.label.ntfy_min_priority": "Niedrigste Ntfy-Priorität",
|
||||
"form.feed.label.ntfy_priority": "Ntfy-Priorität",
|
||||
"form.feed.label.ntfy_topic": "Ntfy-Thema (optional)",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "Umschreiberegeln",
|
||||
"form.feed.label.proxy_url": "Proxy-URL",
|
||||
"form.feed.label.pushover_activate": "Einträge an pushover.net senden",
|
||||
"form.feed.label.pushover_default_priority": "Pushover-Standardpriorität",
|
||||
"form.feed.label.pushover_high_priority": "Hohe Pushoverpriorität",
|
||||
"form.feed.label.pushover_low_priority": "Niedrige Pushoverpriorität",
|
||||
"form.feed.label.pushover_max_priority": "Höchste Pushoverpriorität",
|
||||
"form.feed.label.pushover_min_priority": "Niedrigste Pushoverpriorität",
|
||||
"form.feed.label.pushover_priority": "Pushover-Nachrichtenpriorität",
|
||||
"form.feed.label.rewrite_rules": "Inhalts-Umschreibregeln",
|
||||
"form.feed.label.scraper_rules": "Extraktionsregeln",
|
||||
"form.feed.label.site_url": "URL der Webseite",
|
||||
"form.feed.label.title": "Titel",
|
||||
@@ -228,6 +236,9 @@
|
||||
"form.integration.instapaper_activate": "Einträge in Instapaper speichern",
|
||||
"form.integration.instapaper_password": "Instapaper-Passwort",
|
||||
"form.integration.instapaper_username": "Instapaper-Benutzername",
|
||||
"form.integration.karakeep_activate": "Einträge in Karakeep speichern",
|
||||
"form.integration.karakeep_api_key": "Karakeep-API-Schlüssel",
|
||||
"form.integration.karakeep_url": "Karakeep-API-Endpunkt",
|
||||
"form.integration.linkace_activate": "Einträge in LinkAce speichern",
|
||||
"form.integration.linkace_api_key": "LinkAce-API-Schlüssel",
|
||||
"form.integration.linkace_check_disabled": "Linkprüfung deaktivieren",
|
||||
@@ -241,7 +252,7 @@
|
||||
"form.integration.linkding_tags": "Linkding-Tags",
|
||||
"form.integration.linkwarden_activate": "Artikel in Linkwarden speichern",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden-API-Schlüssel",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden-API-Endpunkt",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden-Base-URL",
|
||||
"form.integration.matrix_bot_activate": "Neue Artikel in Matrix übertragen",
|
||||
"form.integration.matrix_bot_chat_id": "ID des Matrix-Raums",
|
||||
"form.integration.matrix_bot_password": "Passwort für Matrix-Benutzer",
|
||||
@@ -268,15 +279,11 @@
|
||||
"form.integration.pinboard_bookmark": "Lesezeichen als ungelesen markieren",
|
||||
"form.integration.pinboard_tags": "Pinboard-Tags",
|
||||
"form.integration.pinboard_token": "Pinboard-API-Token",
|
||||
"form.integration.pocket_access_token": "Pocket-Zugangstoken",
|
||||
"form.integration.pocket_activate": "Einträge in Pocket speichern",
|
||||
"form.integration.pocket_connect_link": "Verbinden Sie Ihr Pocket-Konto",
|
||||
"form.integration.pocket_consumer_key": "Pocket-Verbraucher-Schlüssel",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
"form.integration.pushover_token": "Pushover application API token",
|
||||
"form.integration.pushover_user": "Pushover user key",
|
||||
"form.integration.pushover_activate": "Einträge an Pushover senden",
|
||||
"form.integration.pushover_device": "Pushovergerät (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover-URL-Präfix (optional)",
|
||||
"form.integration.pushover_token": "Pushover-Anwendungs-API-Token",
|
||||
"form.integration.pushover_user": "Pushover-Benutzerschlüssel",
|
||||
"form.integration.raindrop_activate": "Einträge in Raindrop speichern",
|
||||
"form.integration.raindrop_collection_id": "Sammlungs-ID",
|
||||
"form.integration.raindrop_tags": "Tags (kommagetrennt)",
|
||||
@@ -290,6 +297,7 @@
|
||||
"form.integration.readwise_api_key": "Readwise-Reader-Zugangstoken",
|
||||
"form.integration.readwise_api_key_link": "Erhalten Sie Ihren Readwise-Zugangstoken",
|
||||
"form.integration.rssbridge_activate": "Beim Hinzufügen von Abonnements RSS-Bridge prüfen.",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge-Authentifizierungs-Token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge-Server-URL",
|
||||
"form.integration.shaarli_activate": "Artikel in Shaarli speichern",
|
||||
"form.integration.shaarli_api_secret": "Shaarli-API-Geheimnis",
|
||||
@@ -322,6 +330,7 @@
|
||||
"form.prefs.fieldset.global_feed_settings": "Globale Feedeinstellungen",
|
||||
"form.prefs.fieldset.reader_settings": "Reader-Einstellungen",
|
||||
"form.prefs.help.external_font_hosts": "Per Leerzeichen getrennte Liste externer Schriftarten-Hosts, die erlaubt werden sollen. Beispiel: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Artikel immer mit Öffnen der Links lesen",
|
||||
"form.prefs.label.categories_sorting_order": "Kategorie-Sortierung",
|
||||
"form.prefs.label.cjk_reading_speed": "Lesegeschwindigkeit für Chinesisch, Koreanisch und Japanisch (Zeichen pro Minute)",
|
||||
"form.prefs.label.custom_css": "Benutzerdefiniertes CSS",
|
||||
@@ -342,6 +351,7 @@
|
||||
"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": "Einträge automatisch als gelesen markieren, wenn sie angezeigt werden. Audio/Video bei 90%% Wiedergabe als gelesen markieren",
|
||||
"form.prefs.label.media_playback_rate": "Wiedergabegeschwindigkeit von Audio/Video",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Externe Links in einem neuen Tab öffnen (fügt target=\"_blank\" zu Links hinzu)",
|
||||
"form.prefs.label.show_reading_time": "Geschätzte Lesezeit für Artikel anzeigen",
|
||||
"form.prefs.label.theme": "Thema",
|
||||
"form.prefs.label.timezone": "Zeitzone",
|
||||
@@ -395,13 +405,14 @@
|
||||
"menu.show_only_starred_entries": "Nur markierte Artikel anzeigen",
|
||||
"menu.show_only_unread_entries": "Nur ungelesene Artikel anzeigen",
|
||||
"menu.starred": "Lesezeichen",
|
||||
"menu.title": "Menu",
|
||||
"menu.title": "Menü",
|
||||
"menu.unread": "Ungelesen",
|
||||
"menu.users": "Benutzer",
|
||||
"page.about.author": "Autor:",
|
||||
"page.about.build_date": "Datum der Kompilierung:",
|
||||
"page.about.credits": "Urheberrechte",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.db_usage": "Datenbankgröße:",
|
||||
"page.about.git_commit": "Git-Commit:",
|
||||
"page.about.global_config_options": "Globale Konfigurationsoptionen",
|
||||
"page.about.go_version": "Go-Version:",
|
||||
"page.about.license": "Lizenz:",
|
||||
@@ -497,8 +508,8 @@
|
||||
"page.keyboard_shortcuts.title": "Tastenkürzel",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Lesezeichen hinzufügen/entfernen",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Artikelanhänge öffnen/schließen",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "Gewählten Artikel als gelesen/ungelesen markieren, fokus als nächstes",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Gewählten Artikel als gelesen/ungelesen markieren, fokus vorherige",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "Gewählten Artikel als gelesen/ungelesen markieren, nächsten auswählen",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Gewählten Artikel als gelesen/ungelesen markieren, vorherigen auswählen",
|
||||
"page.login.google_signin": "Anmeldung mit Google",
|
||||
"page.login.oidc_signin": "Anmeldung mit %s",
|
||||
"page.login.title": "Anmeldung",
|
||||
@@ -519,7 +530,7 @@
|
||||
"page.sessions.table.actions": "Aktionen",
|
||||
"page.sessions.table.current_session": "Aktuelle Sitzung",
|
||||
"page.sessions.table.date": "Datum",
|
||||
"page.sessions.table.ip": "IP-Addresse",
|
||||
"page.sessions.table.ip": "IP-Adresse",
|
||||
"page.sessions.table.user_agent": "Benutzeragent",
|
||||
"page.sessions.title": "Sitzungen",
|
||||
"page.settings.link_google_account": "Google-Konto verknüpfen",
|
||||
@@ -566,13 +577,13 @@
|
||||
"page.users.title": "Benutzer",
|
||||
"page.users.username": "Benutzername",
|
||||
"page.webauthn_rename.title": "Passkey umbenennen",
|
||||
"pagination.first": "First",
|
||||
"pagination.last": "Last",
|
||||
"pagination.first": "Erste",
|
||||
"pagination.last": "Letzte",
|
||||
"pagination.next": "Nächste",
|
||||
"pagination.previous": "Vorherige",
|
||||
"search.label": "Suche",
|
||||
"search.placeholder": "Suche...",
|
||||
"search.submit": "Search",
|
||||
"search.submit": "Suchen",
|
||||
"skip_to_content": "Zum Inhalt springen",
|
||||
"time_elapsed.days": [
|
||||
"vor %d Tag",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"action.update": "Ενημέρωση",
|
||||
"alert.account_linked": "Ο εξωτερικός σας λογαριασμός είναι πλέον συνδεδεμένος!",
|
||||
"alert.account_unlinked": "Ο εξωτερικός σας λογαριασμός είναι πλέον αποσυνδεδεμένος!",
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"alert.background_feed_refresh": "Όλες οι ροές ανανεώνονται στο παρασκήνιο. Μπορείτε να συνεχίσετε να χρησιμοποιείτε το Miniflux όσο εκτελείται αυτή η διαδικασία.",
|
||||
"alert.feed_error": "Υπάρχει πρόβλημα με αυτήν τη ροή",
|
||||
"alert.no_bookmark": "Δεν υπάρχει σελιδοδείκτης αυτή τη στιγμή.",
|
||||
"alert.no_category": "Δεν υπάρχει κατηγορία.",
|
||||
@@ -27,26 +27,25 @@
|
||||
"alert.no_tag_entry": "Δεν υπάρχουν αντικείμενα που να ταιριάζουν με αυτή την ετικέτα.",
|
||||
"alert.no_unread_entry": "Δεν υπάρχουν μη αναγνωσμένα άρθρα.",
|
||||
"alert.no_user": "Είστε ο μόνος χρήστης.",
|
||||
"alert.pocket_linked": "Ο λογαριασμός Pocket είναι τώρα συνδεδεμένος!",
|
||||
"alert.prefs_saved": "Οι προτιμήσεις αποθηκεύτηκαν!",
|
||||
"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 λεπτό πριν προσπαθήσετε ξανά.",
|
||||
"Έχετε ενεργοποιήσει πάρα πολλές ανανεώσεις ροών. Παρακαλώ περιμένετε %d λεπτά πριν προσπαθήσετε ξανά."
|
||||
],
|
||||
"confirm.loading": "Σε εξέλιξη...",
|
||||
"confirm.no": "όχι",
|
||||
"confirm.question": "Είστε σίγουροι;",
|
||||
"confirm.question.refresh": "Θέλετε να επιτελέσετε μια υποχρεωτική ανανέωση;",
|
||||
"confirm.yes": "ναι",
|
||||
"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.reset": "Reset",
|
||||
"enclosure_media_controls.speed.reset.title": "Reset speed to 1x",
|
||||
"enclosure_media_controls.speed.slower": "Slower",
|
||||
"enclosure_media_controls.speed.slower.title": "Slower by %sx",
|
||||
"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.reset": "Επαναφορά",
|
||||
"enclosure_media_controls.speed.reset.title": "Επαναφορά ταχύτητας σε 1x",
|
||||
"enclosure_media_controls.speed.slower": "Πιο αργά",
|
||||
"enclosure_media_controls.speed.slower.title": "Πιο αργά κατά %sx",
|
||||
"entry.bookmark.toast.off": "Μη αγαπημένα",
|
||||
"entry.bookmark.toast.on": "Αγαπημένα",
|
||||
"entry.bookmark.toggle.off": "Αναίρεση αγαπημένου",
|
||||
@@ -72,83 +71,89 @@
|
||||
"entry.state.loading": "Φόρτωση...",
|
||||
"entry.state.saving": "Aποθήκευση...",
|
||||
"entry.status.mark_as_read": "Επισήμανση ως αναγνωσμένο",
|
||||
"entry.status.mark_as_unread": "Επισήμανση ως μη αναγνωσμένο",
|
||||
"entry.status.title": "Αλλαγή κατάστασης καταχώρησης",
|
||||
"entry.status.toast.read": "Επισήμανση ως αναγνωσμένο",
|
||||
"entry.status.toast.unread": "Επισήμανση ως μη αναγνωσμένο",
|
||||
"entry.status.mark_as_unread": "Επισήμανση ως μη αναγνωσμένο",
|
||||
"entry.tags.label": "Ετικέτες:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Εμφάνιση %d ακόμη ετικέτας",
|
||||
"Εμφάνιση %d ακόμη ετικετών"
|
||||
],
|
||||
"entry.unshare.label": "Aναίρεση Διαμοιρασμού",
|
||||
"error.api_key_already_exists": "Αυτό το κλειδί API υπάρχει ήδη.",
|
||||
"error.bad_credentials": "Μη έγκυρο όνομα χρήστη ή κωδικό πρόσβασης.",
|
||||
"error.category_already_exists": "Αυτή η κατηγορία υπάρχει ήδη.",
|
||||
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||
"error.database_error": "Database error: %v.",
|
||||
"error.category_not_found": "Αυτή η κατηγορία δεν υπάρχει ή δεν ανήκει σε αυτόν τον χρήστη.",
|
||||
"error.database_error": "Σφάλμα βάσης δεδομένων: %v.",
|
||||
"error.different_passwords": "Οι κωδικοί πρόσβασης δεν είναι οι ίδιοι.",
|
||||
"error.duplicate_fever_username": "Υπάρχει ήδη κάποιος άλλος με το ίδιο όνομα χρήστη Fever!",
|
||||
"error.duplicate_googlereader_username": "Υπάρχει ήδη κάποιος άλλος με το ίδιο όνομα χρήστη Google Reader!",
|
||||
"error.duplicate_linked_account": "Υπάρχει ήδη κάποιος που σχετίζεται με αυτόν τον πάροχο!",
|
||||
"error.duplicated_feed": "This feed already exists.",
|
||||
"error.duplicated_feed": "Αυτή η ροή υπάρχει ήδη.",
|
||||
"error.empty_file": "Αυτό το αρχείο είναι κενό.",
|
||||
"error.entries_per_page_invalid": "Ο αριθμός των καταχωρήσεων ανά σελίδα δεν είναι έγκυρος.",
|
||||
"error.feed_already_exists": "Αυτή η ροή υπάρχει ήδη.",
|
||||
"error.feed_category_not_found": "Αυτή η κατηγορία δεν υπάρχει ή δεν ανήκει σε αυτόν τον χρήστη.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"error.feed_format_not_detected": "Δεν είναι δυνατή η ανίχνευση της μορφής ροής: %v.",
|
||||
"error.feed_invalid_blocklist_rule": "Ο κανόνας λίστας μπλοκ δεν είναι έγκυρος.",
|
||||
"error.feed_invalid_keeplist_rule": "Ο κανόνας keep list δεν είναι έγκυρος.",
|
||||
"error.feed_mandatory_fields": "Η διεύθυνση URL και η κατηγορία είναι υποχρεωτικά.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.feed_not_found": "Αυτή η ροή δεν υπάρχει ή δεν ανήκει σε αυτόν τον χρήστη.",
|
||||
"error.feed_title_not_empty": "Ο τίτλος ροής δεν μπορεί να είναι κενός.",
|
||||
"error.feed_url_not_empty": "Η διεύθυνση URL ροής δεν μπορεί να είναι κενή.",
|
||||
"error.fields_mandatory": "Όλα τα πεδία είναι υποχρεωτικά.",
|
||||
"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_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_client_error": "HTTP client error: %v.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||
"error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).",
|
||||
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_bad_gateway": "Ο ιστότοπος δεν είναι διαθέσιμος αυτήν τη στιγμή λόγω σφάλματος κακής πύλης. Το πρόβλημα δεν είναι στην πλευρά του Miniflux. Παρακαλώ δοκιμάστε ξανά αργότερα.",
|
||||
"error.http_body_read": "Δεν είναι δυνατή η ανάγνωση του σώματος HTTP: %v.",
|
||||
"error.http_client_error": "Σφάλμα πελάτη HTTP: %v.",
|
||||
"error.http_empty_response": "Η απάντηση HTTP είναι κενή. Ίσως αυτός ο ιστότοπος χρησιμοποιεί μηχανισμό προστασίας από bot;",
|
||||
"error.http_empty_response_body": "Το σώμα απάντησης HTTP είναι κενό.",
|
||||
"error.http_forbidden": "Η πρόσβαση σε αυτόν τον ιστότοπο απαγορεύεται. Ίσως αυτός ο ιστότοπος διαθέτει μηχανισμό προστασίας από bot;",
|
||||
"error.http_gateway_timeout": "Ο ιστότοπος δεν είναι διαθέσιμος αυτήν τη στιγμή λόγω σφάλματος χρονικού ορίου πύλης. Το πρόβλημα δεν είναι στην πλευρά του Miniflux. Παρακαλώ δοκιμάστε ξανά αργότερα.",
|
||||
"error.http_internal_server_error": "Ο ιστότοπος δεν είναι διαθέσιμος αυτήν τη στιγμή λόγω σφάλματος διακομιστή. Το πρόβλημα δεν είναι στην πλευρά του Miniflux. Παρακαλώ δοκιμάστε ξανά αργότερα.",
|
||||
"error.http_not_authorized": "Η πρόσβαση σε αυτόν τον ιστότοπο δεν είναι εξουσιοδοτημένη. Μπορεί να είναι λανθασμένο όνομα χρήστη ή κωδικός πρόσβασης.",
|
||||
"error.http_resource_not_found": "Ο ζητούμενος πόρος δεν βρέθηκε. Επαληθεύστε τη διεύθυνση URL.",
|
||||
"error.http_response_too_large": "Η απάντηση HTTP είναι πολύ μεγάλη. Μπορείτε να αυξήσετε το όριο μεγέθους απάντησης HTTP στις καθολικές ρυθμίσεις (απαιτεί επανεκκίνηση του διακομιστή).",
|
||||
"error.http_service_unavailable": "Ο ιστότοπος δεν είναι διαθέσιμος αυτήν τη στιγμή λόγω εσωτερικού σφάλματος διακομιστή. Το πρόβλημα δεν είναι στην πλευρά του Miniflux. Παρακαλώ δοκιμάστε ξανά αργότερα.",
|
||||
"error.http_too_many_requests": "Το Miniflux δημιούργησε πάρα πολλά αιτήματα σε αυτόν τον ιστότοπο. Παρακαλώ δοκιμάστε ξανά αργότερα ή αλλάξτε τη διαμόρφωση της εφαρμογής.",
|
||||
"error.http_unexpected_status_code": "Ο ιστότοπος δεν είναι διαθέσιμος αυτήν τη στιγμή λόγω μη αναμενόμενου κωδικού κατάστασης HTTP: %d. Το πρόβλημα δεν είναι στην πλευρά του Miniflux. Παρακαλώ δοκιμάστε ξανά αργότερα.",
|
||||
"error.invalid_categories_sorting_order": "Η κατηγορία δεν μπορεί να είναι κενή.",
|
||||
"error.invalid_default_home_page": "Μη έγκυρη προεπιλεγμένη αρχική σελίδα!",
|
||||
"error.invalid_display_mode": "Μη έγκυρη λειτουργία εμφάνισης εφαρμογών ιστού.",
|
||||
"error.invalid_entry_direction": "Μη έγκυρη κατεύθυνση ταξινόμησης άρθρων.",
|
||||
"error.invalid_entry_order": "Η σειρά των καταχωρήσεων είναι μη έγκυρη.",
|
||||
"error.invalid_feed_proxy_url": "Μη έγκυρη διεύθυνση URL διακομιστή μεσολάβησης.",
|
||||
"error.invalid_feed_url": "Μη έγκυρη διεύθυνση URL ροής.",
|
||||
"error.invalid_gesture_nav": "Μη έγκυρη πλοήγηση με χειρονομίες.",
|
||||
"error.invalid_language": "Μη έγκυρη γλώσσα.",
|
||||
"error.invalid_site_url": "Μη έγκυρη διεύθυνση URL ιστότοπου.",
|
||||
"error.invalid_theme": "Μη έγκυρο θέμα.",
|
||||
"error.invalid_timezone": "Μη έγκυρη ζώνη ώρας.",
|
||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||
"error.network_operation": "Το Miniflux δεν μπορεί να φτάσει σε αυτόν τον ιστότοπο λόγω σφάλματος δικτύου: %v.",
|
||||
"error.network_timeout": "Αυτός ο ιστότοπος είναι πολύ αργός και το αίτημα έληξε: %v",
|
||||
"error.password_min_length": "Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον 6 χαρακτήρες.",
|
||||
"error.pocket_access_token": "Δεν είναι δυνατή η λήψη του access token από το Pocket!",
|
||||
"error.pocket_request_token": "Δεν είναι δυνατή η λήψη του request token από το Pocket!",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.proxy_url_not_empty": "Η διεύθυνση URL του διακομιστή μεσολάβησης δεν μπορεί να είναι κενή.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Μη έγκυρος κανόνας αποκλεισμού: ο κανόνας #%d λείπει ένα έγκυρο όνομα πεδίου (Επιλογές: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Μη έγκυρος κανόνας αποκλεισμού: το μοτίβο του κανόνα #%d δεν είναι έγκυρη κανονική έκφραση",
|
||||
"error.settings_block_rule_regex_required": "Μη έγκυρος κανόνας αποκλεισμού: το μοτίβο του κανόνα #%d δεν παρέχεται",
|
||||
"error.settings_block_rule_separator_required": "Μη έγκυρος κανόνας αποκλεισμού: το μοτίβο του κανόνα #%d απαιτείται να διαχωρίζεται με ένα '='",
|
||||
"error.settings_invalid_domain_list": "Μη έγκυρη λίστα τομέων. Παρακαλώ δώστε μια λίστα τομέων διαχωρισμένων με κενό.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Μη έγκυρος κανόνας διατήρησης: ο κανόνας #%d λείπει ένα έγκυρο όνομα πεδίου (Επιλογές: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Μη έγκυρος κανόνας διατήρησης: το μοτίβο του κανόνα #%d δεν είναι έγκυρη κανονική έκφραση",
|
||||
"error.settings_keep_rule_regex_required": "Μη έγκυρος κανόνας διατήρησης: το μοτίβο του κανόνα #%d δεν παρέχεται",
|
||||
"error.settings_keep_rule_separator_required": "Μη έγκυρος κανόνας διατήρησης: το μοτίβο του κανόνα #%d απαιτείται να διαχωρίζεται με ένα '='",
|
||||
"error.settings_mandatory_fields": "Τα πεδία όνομα χρήστη, θέμα, Γλώσσα και ζώνη ώρας είναι υποχρεωτικά.",
|
||||
"error.settings_media_playback_rate_range": "Η ταχύτητα αναπαραγωγής είναι εκτός εύρους",
|
||||
"error.settings_reading_speed_is_positive": "Οι ταχύτητες ανάγνωσης πρέπει να είναι θετικοί ακέραιοι αριθμοί.",
|
||||
"error.site_url_not_empty": "Η διεύθυνση URL του ιστότοπου δεν μπορεί να είναι κενή.",
|
||||
"error.subscription_not_found": "Δεν είναι δυνατή η εύρεση συνδρομής.",
|
||||
"error.title_required": "Ο τίτλος είναι υποχρεωτικός.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "Σφάλμα TLS: %q. Μπορείτε να απενεργοποιήσετε την επαλήθευση TLS στις ρυθμίσεις ροής εάν το επιθυμείτε.",
|
||||
"error.unable_to_create_api_key": "Δεν είναι δυνατή η δημιουργία αυτού του κλειδιού API.",
|
||||
"error.unable_to_create_category": "Δεν είναι δυνατή η δημιουργία αυτής της κατηγορίας.",
|
||||
"error.unable_to_create_user": "Δεν είναι δυνατή η δημιουργία αυτού του χρήστη.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.unable_to_detect_rssbridge": "Δεν είναι δυνατή η ανίχνευση ροής με χρήση RSS-Bridge: %v.",
|
||||
"error.unable_to_parse_feed": "Δεν είναι δυνατή η ανάλυση αυτής της ροής: %v.",
|
||||
"error.unable_to_update_category": "Δεν είναι δυνατή η ενημέρωση αυτής της κατηγορίας.",
|
||||
"error.unable_to_update_feed": "Δεν είναι δυνατή η ενημέρωση αυτής της ροής.",
|
||||
"error.unable_to_update_user": "Δεν είναι δυνατή η ενημέρωση αυτού του χρήστη.",
|
||||
@@ -158,61 +163,64 @@
|
||||
"form.api_key.label.description": "Ετικέτα κλειδιού API",
|
||||
"form.category.hide_globally": "Απόκρυψη καταχωρήσεων σε γενική λίστα μη αναγνωσμένων",
|
||||
"form.category.label.title": "Τίτλος",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.integration": "Third-Party Services",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.general": "Γενικά",
|
||||
"form.feed.fieldset.integration": "Υπηρεσίες τρίτων",
|
||||
"form.feed.fieldset.network_settings": "Ρυθμίσεις δικτύου",
|
||||
"form.feed.fieldset.rules": "Κανόνες",
|
||||
"form.feed.label.allow_self_signed_certificates": "Να επιτρέπονται αυτο-υπογεγραμμένα ή μη έγκυρα πιστοποιητικά",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.blocklist_rules": "Κανόνες Αποκλεισμού",
|
||||
"form.feed.label.apprise_service_urls": "Λίστα διευθύνσεων URL υπηρεσιών Apprise διαχωρισμένων με κόμμα",
|
||||
"form.feed.label.block_filter_entry_rules": "Κανόνες Αποκλεισμού Καταχωρήσεων",
|
||||
"form.feed.label.blocklist_rules": "Φίλτρα Αποκλεισμού Βασισμένα σε Regex",
|
||||
"form.feed.label.category": "Κατηγορία",
|
||||
"form.feed.label.cookie": "Ορισμός Cookies",
|
||||
"form.feed.label.crawler": "Λήψη αρχικού περιεχομένου",
|
||||
"form.feed.label.description": "Περιγραφή",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.disable_http2": "Απενεργοποίηση HTTP/2 για αποφυγή δακτυλικών αποτυπωμάτων",
|
||||
"form.feed.label.disabled": "Μη ανανέωση αυτής της ροής",
|
||||
"form.feed.label.feed_password": "Κωδικός Πρόσβασης ροής",
|
||||
"form.feed.label.feed_url": "Διεύθυνση URL ροής",
|
||||
"form.feed.label.feed_username": "Όνομα Χρήστη ροής",
|
||||
"form.feed.label.fetch_via_proxy": "Λήψη μέσω διακομιστή μεσολάβησης",
|
||||
"form.feed.label.fetch_via_proxy": "Χρησιμοποιήστε τον διακομιστή μεσολάβησης που έχει ρυθμιστεί σε επίπεδο εφαρμογής",
|
||||
"form.feed.label.hide_globally": "Απόκρυψη καταχωρήσεων σε γενική λίστα μη αναγνωσμένων",
|
||||
"form.feed.label.ignore_http_cache": "Αγνοήστε την προσωρινή μνήμη HTTP",
|
||||
"form.feed.label.keeplist_rules": "Κρατήστε Κανόνες",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "Κανόνες Μετατροπής",
|
||||
"form.feed.label.keep_filter_entry_rules": "Κανόνες Επιτρεπόμενων Καταχωρήσεων",
|
||||
"form.feed.label.keeplist_rules": "Φίλτρα Διατήρησης Βασισμένα σε Regex",
|
||||
"form.feed.label.no_media_player": "Χωρίς πρόγραμμα αναπαραγωγής πολυμέσων (ήχος/βίντεο)",
|
||||
"form.feed.label.ntfy_activate": "Προώθηση καταχωρήσεων στο ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Προεπιλεγμένη προτεραιότητα Ntfy",
|
||||
"form.feed.label.ntfy_high_priority": "Υψηλή προτεραιότητα Ntfy",
|
||||
"form.feed.label.ntfy_low_priority": "Χαμηλή προτεραιότητα Ntfy",
|
||||
"form.feed.label.ntfy_max_priority": "Μέγιστη προτεραιότητα Ntfy",
|
||||
"form.feed.label.ntfy_min_priority": "Ελάχιστη προτεραιότητα Ntfy",
|
||||
"form.feed.label.ntfy_priority": "Προτεραιότητα Ntfy",
|
||||
"form.feed.label.ntfy_topic": "Θέμα Ntfy (προαιρετικό)",
|
||||
"form.feed.label.proxy_url": "Διεύθυνση URL διακομιστή μεσολάβησης",
|
||||
"form.feed.label.pushover_activate": "Προώθηση καταχωρήσεων στο pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Προεπιλεγμένη προτεραιότητα Pushover",
|
||||
"form.feed.label.pushover_high_priority": "Υψηλή προτεραιότητα Pushover",
|
||||
"form.feed.label.pushover_low_priority": "Χαμηλή προτεραιότητα Pushover",
|
||||
"form.feed.label.pushover_max_priority": "Μέγιστη προτεραιότητα Pushover",
|
||||
"form.feed.label.pushover_min_priority": "Ελάχιστη προτεραιότητα Pushover",
|
||||
"form.feed.label.pushover_priority": "Προτεραιότητα μηνύματος Pushover",
|
||||
"form.feed.label.rewrite_rules": "Κανόνες Επανασύνταξης Περιεχομένου",
|
||||
"form.feed.label.scraper_rules": "Κανόνες Scraper",
|
||||
"form.feed.label.site_url": "Διεύθυνση URL ιστότοπου",
|
||||
"form.feed.label.title": "Τίτλος",
|
||||
"form.feed.label.urlrewrite_rules": "επανεγγραφή κανόνων για τη διεύθυνση URL.",
|
||||
"form.feed.label.urlrewrite_rules": "κανόνες επανεγγραφής για τη διεύθυνση URL.",
|
||||
"form.feed.label.user_agent": "Παράκαμψη Προεπιλεγμένου User Agent Χρήστη",
|
||||
"form.feed.label.webhook_url": "Override webhook url",
|
||||
"form.feed.label.webhook_url": "Παράκαμψη διεύθυνσης URL webhook",
|
||||
"form.import.label.file": "Αρχείο OPML",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||
"form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.integration.discord_activate": "Push entries to Discord",
|
||||
"form.integration.discord_webhook_link": "Discord Webhook link",
|
||||
"form.import.label.url": "Διεύθυνση URL",
|
||||
"form.integration.apprise_activate": "Προώθηση καταχωρήσεων στο Apprise",
|
||||
"form.integration.apprise_services_url": "Λίστα διευθύνσεων URL υπηρεσιών Apprise διαχωρισμένων με κόμμα",
|
||||
"form.integration.apprise_url": "Διεύθυνση URL API Apprise",
|
||||
"form.integration.betula_activate": "Αποθήκευση καταχωρήσεων στο Betula",
|
||||
"form.integration.betula_token": "Διακριτικό Betula",
|
||||
"form.integration.betula_url": "Διεύθυνση URL διακομιστή Betula",
|
||||
"form.integration.cubox_activate": "Αποθήκευση καταχωρήσεων στο Cubox",
|
||||
"form.integration.cubox_api_link": "Σύνδεσμος API Cubox",
|
||||
"form.integration.discord_activate": "Προώθηση καταχωρήσεων στο Discord",
|
||||
"form.integration.discord_webhook_link": "Σύνδεσμος Webhook Discord",
|
||||
"form.integration.espial_activate": "Αποθήκευση άρθρων στο Espial",
|
||||
"form.integration.espial_api_key": "Κλειδί API Espial",
|
||||
"form.integration.espial_endpoint": "Τελικό σημείο Espial API",
|
||||
@@ -228,36 +236,39 @@
|
||||
"form.integration.instapaper_activate": "Αποθήκευση άρθρων στο Instapaper",
|
||||
"form.integration.instapaper_password": "Κωδικός Πρόσβασης Instapaper",
|
||||
"form.integration.instapaper_username": "Όνομα Χρήστη Instapaper",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_is_private": "Mark link as private",
|
||||
"form.integration.linkace_tags": "LinkAce Tags",
|
||||
"form.integration.karakeep_activate": "Αποθήκευση άρθρων στο Karakeep",
|
||||
"form.integration.karakeep_api_key": "Κλειδί API Karakeep",
|
||||
"form.integration.karakeep_url": "Τελικό σημείο Karakeep API",
|
||||
"form.integration.linkace_activate": "Αποθήκευση καταχωρήσεων στο LinkAce",
|
||||
"form.integration.linkace_api_key": "Κλειδί API LinkAce",
|
||||
"form.integration.linkace_check_disabled": "Απενεργοποίηση ελέγχου συνδέσμου",
|
||||
"form.integration.linkace_endpoint": "Τελικό σημείο API LinkAce",
|
||||
"form.integration.linkace_is_private": "Σήμανση συνδέσμου ως ιδιωτικού",
|
||||
"form.integration.linkace_tags": "Ετικέτες LinkAce",
|
||||
"form.integration.linkding_activate": "Αποθήκευση άρθρων στο Linkding",
|
||||
"form.integration.linkding_api_key": "Κλειδί API Linkding",
|
||||
"form.integration.linkding_bookmark": "Σημείωση του σελιδοδείκτη ως μη αναγνωσμένου",
|
||||
"form.integration.linkding_endpoint": "Τελικό σημείο Linkding API",
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkding_tags": "Ετικέτες Linkding",
|
||||
"form.integration.linkwarden_activate": "Αποθήκευση άρθρων στο Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Κλειδί API Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Τελικό σημείο Linkwarden API",
|
||||
"form.integration.linkwarden_endpoint": "URL βάσης Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Μεταφορά νέων άρθρων στο Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Αναγνωριστικό της αίθουσας Matrix",
|
||||
"form.integration.matrix_bot_password": "Κωδικός πρόσβασης για τον χρήστη Matrix",
|
||||
"form.integration.matrix_bot_url": "URL διακομιστή Matrix",
|
||||
"form.integration.matrix_bot_user": "Όνομα χρήστη για το Matrix",
|
||||
"form.integration.notion_activate": "Save entries to Notion",
|
||||
"form.integration.notion_page_id": "Notion Page ID",
|
||||
"form.integration.notion_token": "Notion Secret Token",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_topic": "Ntfy topic (default used if not set in feed)",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.notion_activate": "Αποθήκευση καταχωρήσεων στο Notion",
|
||||
"form.integration.notion_page_id": "Αναγνωριστικό σελίδας Notion",
|
||||
"form.integration.notion_token": "Μυστικό διακριτικό Notion",
|
||||
"form.integration.ntfy_activate": "Προώθηση καταχωρήσεων στο ntfy",
|
||||
"form.integration.ntfy_api_token": "Διακριτικό API Ntfy (προαιρετικό)",
|
||||
"form.integration.ntfy_icon_url": "Διεύθυνση URL εικονιδίου Ntfy (προαιρετικό)",
|
||||
"form.integration.ntfy_internal_links": "Χρήση εσωτερικών συνδέσμων με κλικ (προαιρετικό)",
|
||||
"form.integration.ntfy_password": "Κωδικός πρόσβασης Ntfy (προαιρετικό)",
|
||||
"form.integration.ntfy_topic": "Θέμα Ntfy (προεπιλογή χρησιμοποιείται εάν δεν οριστεί στη ροή)",
|
||||
"form.integration.ntfy_url": "Διεύθυνση URL Ntfy (προαιρετικό, προεπιλογή είναι ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Όνομα χρήστη Ntfy (προαιρετικό)",
|
||||
"form.integration.nunux_keeper_activate": "Αποθήκευση άρθρων στο Nunux Keeper",
|
||||
"form.integration.nunux_keeper_api_key": "Κλειδί API Nunux Keeper",
|
||||
"form.integration.nunux_keeper_endpoint": "Τελικό σημείο Nunux Keeper API",
|
||||
@@ -267,46 +278,43 @@
|
||||
"form.integration.pinboard_activate": "Αποθήκευση άρθρων στο Pinboard",
|
||||
"form.integration.pinboard_bookmark": "Σημείωση του σελιδοδείκτη ως μη αναγνωσμένου",
|
||||
"form.integration.pinboard_tags": "Ετικέτες Pinboard",
|
||||
"form.integration.pinboard_token": "Pinboard API Token",
|
||||
"form.integration.pocket_access_token": "Pocket Access Token",
|
||||
"form.integration.pocket_activate": "Αποθήκευση άρθρων στο Pocket",
|
||||
"form.integration.pocket_connect_link": "Συνδέστε τον λογαριασμό Pocket σας",
|
||||
"form.integration.pocket_consumer_key": "Pocket Consumer Key",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
"form.integration.pushover_token": "Pushover application API token",
|
||||
"form.integration.pushover_user": "Pushover user key",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.pinboard_token": "Διακριτικό API Pinboard",
|
||||
"form.integration.pushover_activate": "Προώθηση καταχωρήσεων στο Pushover",
|
||||
"form.integration.pushover_device": "Συσκευή Pushover (προαιρετικό)",
|
||||
"form.integration.pushover_prefix": "Πρόθεμα διεύθυνσης URL Pushover (προαιρετικό)",
|
||||
"form.integration.pushover_token": "Διακριτικό API εφαρμογής Pushover",
|
||||
"form.integration.pushover_user": "Κλειδί χρήστη Pushover",
|
||||
"form.integration.raindrop_activate": "Αποθήκευση καταχωρήσεων στο Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Αναγνωριστικό συλλογής",
|
||||
"form.integration.raindrop_tags": "Ετικέτες (διαχωρισμένες με κόμμα)",
|
||||
"form.integration.raindrop_token": "Διακριτικό (Δοκιμή)",
|
||||
"form.integration.readeck_activate": "Αποθήκευση άρθρων στο Readeck",
|
||||
"form.integration.readeck_api_key": "Κλειδί API Readeck",
|
||||
"form.integration.readeck_endpoint": "Τελικό σημείο Readeck API",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_labels": "Ετικέτες Readeck",
|
||||
"form.integration.readeck_only_url": "Αποστολή μόνο URL (αντί για πλήρες περιεχόμενο)",
|
||||
"form.integration.readwise_activate": "Save entries to Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.readwise_activate": "Αποθήκευση καταχωρήσεων στο Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Διακριτικό πρόσβασης Readwise Reader",
|
||||
"form.integration.readwise_api_key_link": "Λήψη του διακριτικού πρόσβασης Readwise",
|
||||
"form.integration.rssbridge_activate": "Έλεγχος RSS-Bridge κατά την προσθήκη συνδρομών",
|
||||
"form.integration.rssbridge_token": "Διακριτικό ελέγχου ταυτότητας RSS-Bridge",
|
||||
"form.integration.rssbridge_url": "Διεύθυνση URL διακομιστή RSS-Bridge",
|
||||
"form.integration.shaarli_activate": "Αποθήκευση άρθρων στο Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Μυστικό API Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Διεύθυνση URL Shaarli",
|
||||
"form.integration.shiori_activate": "Αποθήκευση άρθρων στο Shiori",
|
||||
"form.integration.shiori_endpoint": "Τελικό σημείο Shiori",
|
||||
"form.integration.shiori_password": "Κωδικός Πρόσβασης Shiori",
|
||||
"form.integration.shiori_username": "Όνομα Χρήστη Shiori",
|
||||
"form.integration.slack_activate": "Push entries to Slack",
|
||||
"form.integration.slack_webhook_link": "Slack Webhook link",
|
||||
"form.integration.slack_activate": "Προώθηση καταχωρήσεων στο Slack",
|
||||
"form.integration.slack_webhook_link": "Σύνδεσμος Webhook Slack",
|
||||
"form.integration.telegram_bot_activate": "Προωθήστε νέα άρθρα στη συνομιλία Telegram",
|
||||
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||
"form.integration.telegram_bot_disable_notification": "Disable notification",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
|
||||
"form.integration.telegram_bot_disable_buttons": "Απενεργοποίηση κουμπιών",
|
||||
"form.integration.telegram_bot_disable_notification": "Απενεργοποίηση ειδοποίησης",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Απενεργοποίηση προεπισκόπησης ιστοσελίδας",
|
||||
"form.integration.telegram_bot_token": "Διακριτικό bot",
|
||||
"form.integration.telegram_chat_id": "Αναγνωριστικό συνομιλίας",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
"form.integration.telegram_topic_id": "Αναγνωριστικό θέματος",
|
||||
"form.integration.wallabag_activate": "Αποθήκευση άρθρων στο Wallabag",
|
||||
"form.integration.wallabag_client_id": "Ταυτότητα πελάτη Wallabag",
|
||||
"form.integration.wallabag_client_secret": "Wallabag Μυστικό Πελάτη",
|
||||
@@ -314,14 +322,15 @@
|
||||
"form.integration.wallabag_only_url": "Αποστολή μόνο URL (αντί για πλήρες περιεχόμενο)",
|
||||
"form.integration.wallabag_password": "Wallabag Κωδικός Πρόσβασης",
|
||||
"form.integration.wallabag_username": "Όνομα Χρήστη Wallabag",
|
||||
"form.integration.webhook_activate": "Enable Webhooks",
|
||||
"form.integration.webhook_secret": "Webhooks Secret",
|
||||
"form.integration.webhook_url": "Default Webhook URL",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.integration.webhook_activate": "Ενεργοποίηση Webhooks",
|
||||
"form.integration.webhook_secret": "Μυστικό Webhooks",
|
||||
"form.integration.webhook_url": "Προεπιλεγμένη διεύθυνση URL Webhook",
|
||||
"form.prefs.fieldset.application_settings": "Ρυθμίσεις εφαρμογής",
|
||||
"form.prefs.fieldset.authentication_settings": "Ρυθμίσεις ελέγχου ταυτότητας",
|
||||
"form.prefs.fieldset.global_feed_settings": "Καθολικές ρυθμίσεις ροής",
|
||||
"form.prefs.fieldset.reader_settings": "Ρυθμίσεις αναγνώστη",
|
||||
"form.prefs.help.external_font_hosts": "Λίστα εξωτερικών κεντρικών υπολογιστών γραμματοσειρών διαχωρισμένων με κενό για να επιτρέπονται. Για παράδειγμα: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Ανάγνωση άρθρων ανοίγοντας εξωτερικούς συνδέσμους",
|
||||
"form.prefs.label.categories_sorting_order": "Ταξινόμηση κατηγοριών",
|
||||
"form.prefs.label.cjk_reading_speed": "Ταχύτητα ανάγνωσης για κινέζικα, κορεάτικα και ιαπωνικά (χαρακτήρες ανά λεπτό)",
|
||||
"form.prefs.label.custom_css": "Προσαρμοσμένο CSS",
|
||||
@@ -333,15 +342,16 @@
|
||||
"form.prefs.label.entry_order": "Στήλη ταξινόμησης εισόδου",
|
||||
"form.prefs.label.entry_sorting": "Ταξινόμηση",
|
||||
"form.prefs.label.entry_swipe": "Ενεργοποιήστε το σάρωση καταχώρισης στις οθόνες αφής",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.label.external_font_hosts": "Εξωτερικοί κεντρικοί υπολογιστές γραμματοσειρών",
|
||||
"form.prefs.label.gesture_nav": "Χειρονομία για πλοήγηση μεταξύ των καταχωρήσεων",
|
||||
"form.prefs.label.keyboard_shortcuts": "Ενεργοποίηση συντομεύσεων πληκτρολογίου",
|
||||
"form.prefs.label.language": "Γλώσσα",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Σήμανση καταχωρήσεων ως αναγνωσμένων με μη αυτόματο τρόπο",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Σήμανση ως αναγνωσμένου μόνο όταν η αναπαραγωγή ήχου/βίντεο φτάσει το 90%% ολοκλήρωσης",
|
||||
"form.prefs.label.mark_read_on_view": "Αυτόματη επισήμανση καταχωρήσεων ως αναγνωσμένων κατά την προβολή",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Σήμανση καταχωρήσεων ως αναγνωσμένων κατά την προβολή. Για ήχο/βίντεο, σήμανση ως αναγνωσμένου στο 90%% ολοκλήρωσης",
|
||||
"form.prefs.label.media_playback_rate": "Ταχύτητα αναπαραγωγής του ήχου/βίντεο",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Άνοιγμα εξωτερικών συνδέσμων σε νέα καρτέλα (προσθέτει target=\"_blank\" στους συνδέσμους)",
|
||||
"form.prefs.label.show_reading_time": "Εμφάνιση εκτιμώμενου χρόνου ανάγνωσης για άρθρα",
|
||||
"form.prefs.label.theme": "Θέμα",
|
||||
"form.prefs.label.timezone": "Ζώνη Ώρας",
|
||||
@@ -378,7 +388,7 @@
|
||||
"menu.feeds": "Ροές",
|
||||
"menu.flush_history": "Εκκαθάριση ιστορικού",
|
||||
"menu.history": "Ιστορικό",
|
||||
"menu.home_page": "Home page",
|
||||
"menu.home_page": "Αρχική σελίδα",
|
||||
"menu.import": "Εισαγωγή",
|
||||
"menu.integrations": "Ενσωμάτωσεις",
|
||||
"menu.logout": "Αποσύνδεση",
|
||||
@@ -395,13 +405,14 @@
|
||||
"menu.show_only_starred_entries": "Εμφάνιση μόνο αγαπημένων καταχωρήσεων",
|
||||
"menu.show_only_unread_entries": "Εμφάνιση μόνο μη αναγνωσμένων καταχωρήσεων",
|
||||
"menu.starred": "Αγαπημένα",
|
||||
"menu.title": "Menu",
|
||||
"menu.title": "Μενού",
|
||||
"menu.unread": "Μη αναγνωσμένα",
|
||||
"menu.users": "Χρήστες",
|
||||
"page.about.author": "Συγγραφέας:",
|
||||
"page.about.build_date": "Ημερομηνία Κατασκευής:",
|
||||
"page.about.credits": "Συνεισφέροντες",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.db_usage": "Μέγεθος βάσης δεδομένων:",
|
||||
"page.about.git_commit": "Υποβολή Git:",
|
||||
"page.about.global_config_options": "Γενικές ρυθμίσεις",
|
||||
"page.about.go_version": "Έκδοση Go:",
|
||||
"page.about.license": "Άδεια:",
|
||||
@@ -430,10 +441,10 @@
|
||||
"page.categories.no_feed": "Καμία ροή.",
|
||||
"page.categories.title": "Κατηγορίες",
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories"
|
||||
"%d κατηγορία",
|
||||
"%d κατηγορίες"
|
||||
],
|
||||
"page.category_label": "Category: %s",
|
||||
"page.category_label": "Κατηγορία: %s",
|
||||
"page.edit_category.title": "Επεξεργασία κατηγορίας: % s",
|
||||
"page.edit_feed.etag_header": "Κεφαλίδα ETag:",
|
||||
"page.edit_feed.last_check": "Τελευταίος έλεγχος:",
|
||||
@@ -448,7 +459,7 @@
|
||||
"%d σφάλματα"
|
||||
],
|
||||
"page.feeds.last_check": "Τελευταίος έλεγχος:",
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.next_check": "Επόμενος έλεγχος:",
|
||||
"page.feeds.read_counter": "Αριθμός αναγνωσμένων καταχωρήσεων",
|
||||
"page.feeds.title": "Ροές",
|
||||
"page.history.title": "Ιστορικό",
|
||||
@@ -496,7 +507,7 @@
|
||||
"page.keyboard_shortcuts.subtitle.sections": "Πλοήγηση Τμημάτων",
|
||||
"page.keyboard_shortcuts.title": "Συντομεύσεις Πληκτρολογίου",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Εναλλαγή σελιδοδείκτη",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Εναλλαγή άνοιγμα/κλείσιμο συνημμένων καταχώρησης",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "Εναλλαγή ανάγνωσης / μη αναγνωσμένης, εστίαση στη συνέχεια",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Εναλλαγή ανάγνωσης / μη αναγνωσμένης, εστίαση στο προηγούμενο",
|
||||
"page.login.google_signin": "Συνδεθείτε με τo Google",
|
||||
@@ -504,7 +515,7 @@
|
||||
"page.login.title": "Είσοδος",
|
||||
"page.login.webauthn_login": "Είσοδος με κωδικό πρόσβασης",
|
||||
"page.login.webauthn_login.error": "Δεν είναι δυνατή η σύνδεση με κωδικό πρόσβασης",
|
||||
"page.login.webauthn_login.help": "Please enter your username if you're using a security key. This is not required if you are using a Passkey (discoverable credentials).",
|
||||
"page.login.webauthn_login.help": "Παρακαλώ εισαγάγετε το όνομα χρήστη σας εάν χρησιμοποιείτε κλειδί ασφαλείας. Αυτό δεν απαιτείται εάν χρησιμοποιείτε Passkey (ανακαλύψιμα διαπιστευτήρια).",
|
||||
"page.new_api_key.title": "Νέο κλειδί API",
|
||||
"page.new_category.title": "Νέα Κατηγορία",
|
||||
"page.new_user.title": "Νέος Χρήστης",
|
||||
@@ -512,8 +523,8 @@
|
||||
"page.offline.refresh_page": "Προσπαθήστε να ανανεώσετε τη σελίδα",
|
||||
"page.offline.title": "Λειτουργία Εκτός Σύνδεσης",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries"
|
||||
"%d αναγνωσμένη καταχώρηση",
|
||||
"%d αναγνωσμένες καταχωρήσεις"
|
||||
],
|
||||
"page.search.title": "Αποτελέσματα Αναζήτησης",
|
||||
"page.sessions.table.actions": "Eνέργειες",
|
||||
@@ -527,35 +538,35 @@
|
||||
"page.settings.title": "Ρυθμίσεις",
|
||||
"page.settings.unlink_google_account": "Αποσύνδεση του λογαριασμού μου Google",
|
||||
"page.settings.unlink_oidc_account": "Αποσύνδεση του λογαριασμού μου %s",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.added_on": "Added On",
|
||||
"page.settings.webauthn.actions": "Ενέργειες",
|
||||
"page.settings.webauthn.added_on": "Προστέθηκε στις",
|
||||
"page.settings.webauthn.delete": [
|
||||
"Αφαιρέστε %d κωδικό πρόσβασης",
|
||||
"Καταργήστε %d κωδικούς πρόσβασης"
|
||||
"Κατάργηση %d κωδικού πρόσβασης",
|
||||
"Κατάργηση %d κωδικών πρόσβασης"
|
||||
],
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.last_seen_on": "Τελευταία χρήση",
|
||||
"page.settings.webauthn.passkey_name": "Όνομα κωδικού πρόσβασης",
|
||||
"page.settings.webauthn.passkeys": "Κωδικοί πρόσβασης",
|
||||
"page.settings.webauthn.register": "Εγγραφή κωδικού πρόσβασης",
|
||||
"page.settings.webauthn.register.error": "Δεν είναι δυνατή η εγγραφή του κωδικού πρόσβασης",
|
||||
"page.shared_entries.title": "Κοινόχρηστες Καταχωρήσεις",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries"
|
||||
"%d κοινόχρηστη καταχώρηση",
|
||||
"%d κοινόχρηστες καταχωρήσεις"
|
||||
],
|
||||
"page.starred.title": "Αγαπημένo",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries"
|
||||
"%d καταχώρηση με αστέρι",
|
||||
"%d καταχωρήσεις με αστέρι"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total"
|
||||
"%d καταχώρηση συνολικά",
|
||||
"%d καταχωρήσεις συνολικά"
|
||||
],
|
||||
"page.unread.title": "Μη αναγνωσμένα",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries"
|
||||
"%d μη αναγνωσμένη καταχώρηση",
|
||||
"%d μη αναγνωσμένες καταχωρήσεις"
|
||||
],
|
||||
"page.users.actions": "Eνέργειες",
|
||||
"page.users.admin.no": "Όχι",
|
||||
@@ -565,15 +576,15 @@
|
||||
"page.users.never_logged": "Ποτέ",
|
||||
"page.users.title": "Χρήστες",
|
||||
"page.users.username": "Χρήστης",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"pagination.first": "First",
|
||||
"pagination.last": "Last",
|
||||
"page.webauthn_rename.title": "Μετονομασία κωδικού πρόσβασης",
|
||||
"pagination.first": "Πρώτο",
|
||||
"pagination.last": "Τελευταίο",
|
||||
"pagination.next": "Επόμενη",
|
||||
"pagination.previous": "Προηγούμενη",
|
||||
"search.label": "Αναζήτηση",
|
||||
"search.placeholder": "Αναζήτηση...",
|
||||
"search.submit": "Search",
|
||||
"skip_to_content": "Skip to content",
|
||||
"search.submit": "Αναζήτηση",
|
||||
"skip_to_content": "Μετάβαση στο περιεχόμενο",
|
||||
"time_elapsed.days": [
|
||||
"πριν %d ημέρα",
|
||||
"πριν %d ημέρες"
|
||||
@@ -603,4 +614,4 @@
|
||||
"time_elapsed.yesterday": "χθες",
|
||||
"tooltip.keyboard_shortcuts": "Συντόμευση πληκτρολογίου: % s",
|
||||
"tooltip.logged_user": "Συνδεδεμένος/η ως %s"
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@
|
||||
"alert.no_tag_entry": "There are no entries matching this tag.",
|
||||
"alert.no_unread_entry": "There are no unread entries.",
|
||||
"alert.no_user": "You are the only user.",
|
||||
"alert.pocket_linked": "Your Pocket account is now linked!",
|
||||
"alert.prefs_saved": "Preferences saved!",
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again.",
|
||||
@@ -72,11 +71,15 @@
|
||||
"entry.state.loading": "Loading…",
|
||||
"entry.state.saving": "Saving…",
|
||||
"entry.status.mark_as_read": "Mark as read",
|
||||
"entry.status.mark_as_unread": "Mark as unread",
|
||||
"entry.status.title": "Change entry status",
|
||||
"entry.status.toast.read": "Marked as read",
|
||||
"entry.status.toast.unread": "Marked as unread",
|
||||
"entry.status.mark_as_unread": "Mark as unread",
|
||||
"entry.tags.label": "Tags:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Show %d more tag",
|
||||
"Show %d more tags"
|
||||
],
|
||||
"entry.unshare.label": "Unshare",
|
||||
"error.api_key_already_exists": "This API Key already exists.",
|
||||
"error.bad_credentials": "Invalid username or password.",
|
||||
@@ -114,9 +117,12 @@
|
||||
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.invalid_categories_sorting_order": "Invalid categories sorting order.",
|
||||
"error.invalid_default_home_page": "Invalid default homepage!",
|
||||
"error.invalid_display_mode": "Invalid web app display mode.",
|
||||
"error.invalid_entry_direction": "Invalid entry direction.",
|
||||
"error.invalid_entry_order": "Invalid entry order.",
|
||||
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
|
||||
"error.invalid_feed_url": "Invalid feed URL.",
|
||||
"error.invalid_gesture_nav": "Invalid gesture navigation.",
|
||||
"error.invalid_language": "Invalid language.",
|
||||
@@ -126,8 +132,7 @@
|
||||
"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.password_min_length": "The password must have at least 6 characters.",
|
||||
"error.pocket_access_token": "Unable to fetch access token from Pocket!",
|
||||
"error.pocket_request_token": "Unable to fetch request token from Pocket!",
|
||||
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
@@ -164,7 +169,8 @@
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.label.allow_self_signed_certificates": "Allow self-signed or invalid certificates",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.blocklist_rules": "Block Rules",
|
||||
"form.feed.label.block_filter_entry_rules": "Entry Blocking Rules",
|
||||
"form.feed.label.blocklist_rules": "Regex-Based Blocking Filters",
|
||||
"form.feed.label.category": "Category",
|
||||
"form.feed.label.cookie": "Set Cookies",
|
||||
"form.feed.label.crawler": "Fetch original content",
|
||||
@@ -174,10 +180,11 @@
|
||||
"form.feed.label.feed_password": "Feed Password",
|
||||
"form.feed.label.feed_url": "Feed URL",
|
||||
"form.feed.label.feed_username": "Feed Username",
|
||||
"form.feed.label.fetch_via_proxy": "Fetch via proxy",
|
||||
"form.feed.label.fetch_via_proxy": "Use the proxy configured at the application level",
|
||||
"form.feed.label.hide_globally": "Hide entries in global unread list",
|
||||
"form.feed.label.ignore_http_cache": "Ignore HTTP cache",
|
||||
"form.feed.label.keeplist_rules": "Keep Rules",
|
||||
"form.feed.label.keep_filter_entry_rules": "Entry Allow Rules",
|
||||
"form.feed.label.keeplist_rules": "Regex-Based Keep Filters",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
@@ -187,6 +194,7 @@
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.pushover_activate": "Push entries to Pushover",
|
||||
"form.feed.label.pushover_default_priority": "Default priority",
|
||||
"form.feed.label.pushover_high_priority": "High priority",
|
||||
@@ -194,7 +202,7 @@
|
||||
"form.feed.label.pushover_max_priority": "Max priority",
|
||||
"form.feed.label.pushover_min_priority": "Minimal priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "Rewrite Rules",
|
||||
"form.feed.label.rewrite_rules": "Content Rewrite Rules",
|
||||
"form.feed.label.scraper_rules": "Scraper Rules",
|
||||
"form.feed.label.site_url": "Site URL",
|
||||
"form.feed.label.title": "Title",
|
||||
@@ -228,6 +236,9 @@
|
||||
"form.integration.instapaper_activate": "Save entries to Instapaper",
|
||||
"form.integration.instapaper_password": "Instapaper Password",
|
||||
"form.integration.instapaper_username": "Instapaper Username",
|
||||
"form.integration.karakeep_activate": "Save entries to Karakeep",
|
||||
"form.integration.karakeep_api_key": "Karakeep API key",
|
||||
"form.integration.karakeep_url": "Karakeep API Endpoint",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
@@ -241,7 +252,7 @@
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkwarden_activate": "Save entries to Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API key",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden API Endpoint",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden Base URL",
|
||||
"form.integration.matrix_bot_activate": "Push new entries to Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID of Matrix Room",
|
||||
"form.integration.matrix_bot_password": "Password for Matrix user",
|
||||
@@ -268,10 +279,6 @@
|
||||
"form.integration.pinboard_bookmark": "Mark bookmark as unread",
|
||||
"form.integration.pinboard_tags": "Pinboard Tags",
|
||||
"form.integration.pinboard_token": "Pinboard API Token",
|
||||
"form.integration.pocket_access_token": "Pocket Access Token",
|
||||
"form.integration.pocket_activate": "Save entries to Pocket",
|
||||
"form.integration.pocket_connect_link": "Connect your Pocket account",
|
||||
"form.integration.pocket_consumer_key": "Pocket Consumer Key",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
@@ -290,6 +297,7 @@
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
@@ -322,6 +330,7 @@
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Read articles by opening external links",
|
||||
"form.prefs.label.categories_sorting_order": "Categories sorting",
|
||||
"form.prefs.label.cjk_reading_speed": "Reading speed for Chinese, Korean and Japanese (characters per minute)",
|
||||
"form.prefs.label.custom_css": "Custom CSS",
|
||||
@@ -342,6 +351,7 @@
|
||||
"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.media_playback_rate": "Playback speed of the audio/video",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Open external links in a new tab (adds target=\"_blank\" to links)",
|
||||
"form.prefs.label.show_reading_time": "Show estimated reading time for entries",
|
||||
"form.prefs.label.theme": "Theme",
|
||||
"form.prefs.label.timezone": "Timezone",
|
||||
@@ -402,6 +412,7 @@
|
||||
"page.about.build_date": "Build Date:",
|
||||
"page.about.credits": "Credits",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.global_config_options": "Global configuration options",
|
||||
"page.about.go_version": "Go version:",
|
||||
"page.about.license": "License:",
|
||||
@@ -421,10 +432,6 @@
|
||||
"page.api_keys.table.last_used_at": "Last Used",
|
||||
"page.api_keys.table.token": "Token",
|
||||
"page.api_keys.title": "API Keys",
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories"
|
||||
],
|
||||
"page.categories.entries": "Entries",
|
||||
"page.categories.feed_count": [
|
||||
"There is %d feed.",
|
||||
@@ -433,6 +440,10 @@
|
||||
"page.categories.feeds": "Feeds",
|
||||
"page.categories.no_feed": "No feed.",
|
||||
"page.categories.title": "Categories",
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories"
|
||||
],
|
||||
"page.category_label": "Category: %s",
|
||||
"page.edit_category.title": "Edit Category: %s",
|
||||
"page.edit_feed.etag_header": "ETag header:",
|
||||
@@ -538,25 +549,25 @@
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.register": "Register passkey",
|
||||
"page.settings.webauthn.register.error": "Unable to register passkey",
|
||||
"page.shared_entries.title": "Shared entries",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries"
|
||||
],
|
||||
"page.shared_entries.title": "Shared entries",
|
||||
"page.starred.title": "Starred",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries"
|
||||
],
|
||||
"page.starred.title": "Starred",
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total"
|
||||
],
|
||||
"page.unread.title": "Unread",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries"
|
||||
],
|
||||
"page.unread.title": "Unread",
|
||||
"page.users.actions": "Actions",
|
||||
"page.users.admin.no": "No",
|
||||
"page.users.admin.yes": "Yes",
|
||||
@@ -603,4 +614,4 @@
|
||||
"time_elapsed.yesterday": "yesterday",
|
||||
"tooltip.keyboard_shortcuts": "Keyboard Shortcut: %s",
|
||||
"tooltip.logged_user": "Logged in as %s"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"alert.no_tag_entry": "No hay artículos con esta etiqueta.",
|
||||
"alert.no_unread_entry": "No hay artículos sin leer.",
|
||||
"alert.no_user": "Eres el único usuario.",
|
||||
"alert.pocket_linked": "¡Tu cuenta de Pocket ya está vinculada!",
|
||||
"alert.prefs_saved": "¡Las preferencias se han guardado!",
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"Has activado demasiadas actualizaciones del feed. Espere %d minuto antes de volver a intentarlo.",
|
||||
@@ -72,11 +71,15 @@
|
||||
"entry.state.loading": "Cargando...",
|
||||
"entry.state.saving": "Guardando...",
|
||||
"entry.status.mark_as_read": "Marcar como leído",
|
||||
"entry.status.mark_as_unread": "Marcar como no leído",
|
||||
"entry.status.title": "Cambiar estado del artículo",
|
||||
"entry.status.toast.read": "Marcado como leído",
|
||||
"entry.status.toast.unread": "Marcado como no leído",
|
||||
"entry.status.mark_as_unread": "Marcar como no leído",
|
||||
"entry.tags.label": "Etiquetas:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Mostrar %d etiqueta más",
|
||||
"Mostrar %d etiquetas más"
|
||||
],
|
||||
"entry.unshare.label": "No compartir",
|
||||
"error.api_key_already_exists": "Esta clave API ya existe.",
|
||||
"error.bad_credentials": "Usuario o contraseña no válido.",
|
||||
@@ -114,9 +117,12 @@
|
||||
"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_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_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.invalid_categories_sorting_order": "Orden de clasificación de categorías no válido.",
|
||||
"error.invalid_default_home_page": "¡Página de inicio por defecto no válida!",
|
||||
"error.invalid_display_mode": "Modo de visualización de la aplicación web no válido.",
|
||||
"error.invalid_entry_direction": "Dirección de artículo no válida.",
|
||||
"error.invalid_entry_order": "Orden de artículo no válido.",
|
||||
"error.invalid_feed_proxy_url": "URL de proxy inválida.",
|
||||
"error.invalid_feed_url": "URL de feed no válida.",
|
||||
"error.invalid_gesture_nav": "Navegación por gestos no válida.",
|
||||
"error.invalid_language": "Idioma no válido.",
|
||||
@@ -126,13 +132,12 @@
|
||||
"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.password_min_length": "La contraseña debería tener al menos 6 caracteres.",
|
||||
"error.pocket_access_token": "Incapaz de obtener un token de acceso de Pocket!",
|
||||
"error.pocket_request_token": "Incapaz de obtener un token de solicitud de Pocket!",
|
||||
"error.proxy_url_not_empty": "La URL del proxy no puede estar vacía.",
|
||||
"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_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_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_separator_required": "Regla de bloqueo no válida: el patrón de la regla #%d debe estar separado por un '='",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"error.settings_invalid_domain_list": "Lista de dominios inválida. Por favor proporcione una lista de dominios separados por espacios.",
|
||||
"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_invalid_regex": "Regla de mantenimiento no válida: el patrón de la regla #%d no es una expresión regular válida",
|
||||
"error.settings_keep_rule_regex_required": "Regla de conservación no válida: no se ha proporcionado la regla #%d patrón",
|
||||
@@ -164,7 +169,8 @@
|
||||
"form.feed.fieldset.rules": "Reglas",
|
||||
"form.feed.label.allow_self_signed_certificates": "Permitir certificados autofirmados o no válidos",
|
||||
"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.block_filter_entry_rules": "Reglas de Bloqueo de Entradas",
|
||||
"form.feed.label.blocklist_rules": "Filtros de Bloqueo Basados en Regex",
|
||||
"form.feed.label.category": "Categoría",
|
||||
"form.feed.label.cookie": "Configurar las cookies",
|
||||
"form.feed.label.crawler": "Obtener rastreador original",
|
||||
@@ -174,10 +180,11 @@
|
||||
"form.feed.label.feed_password": "Contraseña de la fuente",
|
||||
"form.feed.label.feed_url": "URL de la fuente",
|
||||
"form.feed.label.feed_username": "Nombre de usuario de la fuente",
|
||||
"form.feed.label.fetch_via_proxy": "Buscar a través de proxy",
|
||||
"form.feed.label.fetch_via_proxy": "Usar el proxy configurado a nivel de la aplicación",
|
||||
"form.feed.label.hide_globally": "Ocultar artículos en la lista global de no leídos",
|
||||
"form.feed.label.ignore_http_cache": "Ignorar caché HTTP",
|
||||
"form.feed.label.keeplist_rules": "Reglas de Filtrado (Permitir)",
|
||||
"form.feed.label.keep_filter_entry_rules": "Reglas de Permitir Entradas",
|
||||
"form.feed.label.keeplist_rules": "Filtros de Mantener Basados en Regex",
|
||||
"form.feed.label.no_media_player": "Sin reproductor multimedia (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Enviar entradas a ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Prioridad predeterminada a Ntfy",
|
||||
@@ -187,20 +194,21 @@
|
||||
"form.feed.label.ntfy_min_priority": "Prioridad mínima a Ntfy",
|
||||
"form.feed.label.ntfy_priority": "Prioridad Ntfy",
|
||||
"form.feed.label.ntfy_topic": "Tema Ntfy (opcional)",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "Reglas de reescribir",
|
||||
"form.feed.label.proxy_url": "URL del Proxy",
|
||||
"form.feed.label.pushover_activate": "Enviar artículos a pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Prioridad predeterminada de Pushover",
|
||||
"form.feed.label.pushover_high_priority": "Prioridad alta de Pushover",
|
||||
"form.feed.label.pushover_low_priority": "Prioridad baja de Pushover",
|
||||
"form.feed.label.pushover_max_priority": "Prioridad máxima de Pushover",
|
||||
"form.feed.label.pushover_min_priority": "Prioridad mínima de Pushover",
|
||||
"form.feed.label.pushover_priority": "Prioridad del mensaje de Pushover",
|
||||
"form.feed.label.rewrite_rules": "Reglas de Reescritura de Contenido",
|
||||
"form.feed.label.scraper_rules": "Reglas de extracción de información",
|
||||
"form.feed.label.site_url": "URL del sitio",
|
||||
"form.feed.label.title": "Título",
|
||||
"form.feed.label.urlrewrite_rules": "Reglas de Filtrado (Reescritura)",
|
||||
"form.feed.label.user_agent": "Invalidar el agente de usuario predeterminado",
|
||||
"form.feed.label.webhook_url": "Override webhook url",
|
||||
"form.feed.label.webhook_url": "Invalidar la URL del webhook",
|
||||
"form.import.label.file": "Archivo OPML",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.apprise_activate": "Enviar artículos a Apprise",
|
||||
@@ -209,8 +217,8 @@
|
||||
"form.integration.betula_activate": "Guardar artículos en Betula",
|
||||
"form.integration.betula_token": "Token de Betula",
|
||||
"form.integration.betula_url": "URL del servidor Betula",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.integration.cubox_activate": "Guardar artículos en Cubox",
|
||||
"form.integration.cubox_api_link": "Enlace de la API de Cubox",
|
||||
"form.integration.discord_activate": "Enviar artículos a Discord",
|
||||
"form.integration.discord_webhook_link": "URL de la Webhook de Discord",
|
||||
"form.integration.espial_activate": "Enviar artículos a Espial",
|
||||
@@ -228,6 +236,9 @@
|
||||
"form.integration.instapaper_activate": "Enviar artículos a Instapaper",
|
||||
"form.integration.instapaper_password": "Contraseña de Instapaper",
|
||||
"form.integration.instapaper_username": "Nombre de usuario de Instapaper",
|
||||
"form.integration.karakeep_activate": "Enviar artículos a Karakeep",
|
||||
"form.integration.karakeep_api_key": "Clave de API de Karakeep",
|
||||
"form.integration.karakeep_url": "Acceso API de Karakeep",
|
||||
"form.integration.linkace_activate": "Guardar artículos en LinkAce",
|
||||
"form.integration.linkace_api_key": "Clave API de LinkAce",
|
||||
"form.integration.linkace_check_disabled": "Deshabilitar la comprobación de enlace",
|
||||
@@ -241,7 +252,7 @@
|
||||
"form.integration.linkding_tags": "Etiquetas de Linkding",
|
||||
"form.integration.linkwarden_activate": "Enviar artículos a Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Clave de API de Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Acceso API de Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "URL base de Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Transferir nuevos artículos a Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID de la sala de Matrix",
|
||||
"form.integration.matrix_bot_password": "Contraseña para el usuario de Matrix",
|
||||
@@ -268,15 +279,11 @@
|
||||
"form.integration.pinboard_bookmark": "Marcar marcador como no leído",
|
||||
"form.integration.pinboard_tags": "Etiquetas de Pinboard",
|
||||
"form.integration.pinboard_token": "Token de API de Pinboard",
|
||||
"form.integration.pocket_access_token": "Token de acceso de Pocket",
|
||||
"form.integration.pocket_activate": "Enviar artículos a Pocket",
|
||||
"form.integration.pocket_connect_link": "Conectar a la cuenta de Pocket",
|
||||
"form.integration.pocket_consumer_key": "Clave del consumidor de Pocket",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
"form.integration.pushover_token": "Pushover application API token",
|
||||
"form.integration.pushover_user": "Pushover user key",
|
||||
"form.integration.pushover_activate": "Enviar artículos a Pushover",
|
||||
"form.integration.pushover_device": "Dispositivo Pushover (opcional)",
|
||||
"form.integration.pushover_prefix": "Prefijo de URL de Pushover (opcional)",
|
||||
"form.integration.pushover_token": "Token de API de la aplicación Pushover",
|
||||
"form.integration.pushover_user": "Clave de usuario de Pushover",
|
||||
"form.integration.raindrop_activate": "Guardar artículos en Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Colección ID",
|
||||
"form.integration.raindrop_tags": "Etiquetas (separadas por comas)",
|
||||
@@ -290,6 +297,7 @@
|
||||
"form.integration.readwise_api_key": "Token de acceso a Readwise Reader",
|
||||
"form.integration.readwise_api_key_link": "Obtener tu token de acceso a Readwise",
|
||||
"form.integration.rssbridge_activate": "Vericar RSS-Bridge al agregar suscripciones",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "URL del servidro RSS-Bridge",
|
||||
"form.integration.shaarli_activate": "Guardar artículos en Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Secreto API de Shaarli",
|
||||
@@ -321,7 +329,8 @@
|
||||
"form.prefs.fieldset.authentication_settings": "Ajustes de la autentificación",
|
||||
"form.prefs.fieldset.global_feed_settings": "Ajustes globales del feed",
|
||||
"form.prefs.fieldset.reader_settings": "Ajustes del lector",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.help.external_font_hosts": "Lista separada por espacios de hosts de fuentes externas permitidos. Por ejemplo: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Leer artículos abriendo enlaces externos",
|
||||
"form.prefs.label.categories_sorting_order": "Clasificación por categorías",
|
||||
"form.prefs.label.cjk_reading_speed": "Velocidad de lectura en chino, coreano y japonés (caracteres por minuto)",
|
||||
"form.prefs.label.custom_css": "CSS personalizado",
|
||||
@@ -333,7 +342,7 @@
|
||||
"form.prefs.label.entry_order": "Columna de clasificación de artículos",
|
||||
"form.prefs.label.entry_sorting": "Clasificación de artículos",
|
||||
"form.prefs.label.entry_swipe": "Habilitar deslizamiento de entrada en pantallas táctiles",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.label.external_font_hosts": "Hosts de fuentes externas",
|
||||
"form.prefs.label.gesture_nav": "Gesto para navegar entre entradas",
|
||||
"form.prefs.label.keyboard_shortcuts": "Habilitar atajos de teclado",
|
||||
"form.prefs.label.language": "Idioma",
|
||||
@@ -342,6 +351,7 @@
|
||||
"form.prefs.label.mark_read_on_view": "Marcar automáticamente las entradas como leídas cuando se vean",
|
||||
"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.media_playback_rate": "Velocidad de reproducción del audio/vídeo",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Abrir enlaces externos en una nueva pestaña (agrega target=\"_blank\" a los enlaces)",
|
||||
"form.prefs.label.show_reading_time": "Mostrar el tiempo estimado de lectura de los artículos",
|
||||
"form.prefs.label.theme": "Tema",
|
||||
"form.prefs.label.timezone": "Zona horaria",
|
||||
@@ -401,7 +411,8 @@
|
||||
"page.about.author": "Autor:",
|
||||
"page.about.build_date": "Fecha de compilación:",
|
||||
"page.about.credits": "Créditos",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.db_usage": "Tamaño de la base de datos:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.global_config_options": "Opciones de configuración global",
|
||||
"page.about.go_version": "Go versión:",
|
||||
"page.about.license": "Licencia:",
|
||||
@@ -464,7 +475,7 @@
|
||||
"page.integration.miniflux_api_username": "Nombre de usuario",
|
||||
"page.integrations.title": "Integraciones",
|
||||
"page.keyboard_shortcuts.close_modal": "Cerrar el cuadro de diálogo modal",
|
||||
"page.keyboard_shortcuts.download_content": "Descargar el contento original",
|
||||
"page.keyboard_shortcuts.download_content": "Descargar el contenido original",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "Ir al elemento inferior",
|
||||
"page.keyboard_shortcuts.go_to_categories": "Ir a las categorías",
|
||||
"page.keyboard_shortcuts.go_to_feed": "Ir a la fuente",
|
||||
@@ -504,7 +515,7 @@
|
||||
"page.login.title": "Iniciar sesión",
|
||||
"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 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.login.webauthn_login.help": "Por favor, introduce tu nombre de usuario si usas una clave de seguridad. Esto no es necesario si usas una Passkey (credenciales detectables).",
|
||||
"page.new_api_key.title": "Nueva clave API",
|
||||
"page.new_category.title": "Nueva categoría",
|
||||
"page.new_user.title": "Nuevo usuario",
|
||||
@@ -527,7 +538,7 @@
|
||||
"page.settings.title": "Ajustes",
|
||||
"page.settings.unlink_google_account": "Desvincular mi cuenta de Google",
|
||||
"page.settings.unlink_oidc_account": "Desvincular mi cuenta de %s",
|
||||
"page.settings.webauthn.actions": "Accioness",
|
||||
"page.settings.webauthn.actions": "Acciones",
|
||||
"page.settings.webauthn.added_on": "Añadido",
|
||||
"page.settings.webauthn.delete": [
|
||||
"Eliminar %d clave de acceso",
|
||||
@@ -603,4 +614,4 @@
|
||||
"time_elapsed.yesterday": "ayer",
|
||||
"tooltip.keyboard_shortcuts": "Atajo de teclado: %s",
|
||||
"tooltip.logged_user": "Registrado como %s"
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"action.update": "Päivitä",
|
||||
"alert.account_linked": "Ulkoinen tilisi on nyt linkitetty!",
|
||||
"alert.account_unlinked": "Ulkoinen tilisi on nyt irrotettu!",
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"alert.background_feed_refresh": "Kaikki syötteet päivitetään taustalla. Voit jatkaa Minifluxin käyttöä tämän prosessin aikana.",
|
||||
"alert.feed_error": "Tässä syötteessä on ongelma",
|
||||
"alert.no_bookmark": "Tällä hetkellä ei ole kirjanmerkkiä.",
|
||||
"alert.no_category": "Ei ole kategoriaa.",
|
||||
@@ -27,26 +27,25 @@
|
||||
"alert.no_tag_entry": "Tätä tunnistetta vastaavia merkintöjä ei ole.",
|
||||
"alert.no_unread_entry": "Ei ole lukemattomia artikkeleita.",
|
||||
"alert.no_user": "Olet ainoa käyttäjä.",
|
||||
"alert.pocket_linked": "Pocket-tilisi on nyt linkitetty!",
|
||||
"alert.prefs_saved": "Asetukset tallennettu!",
|
||||
"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."
|
||||
"Olet käynnistänyt liian monta syötteen päivitystä. Odota %d minuutti ennen kuin yrität uudelleen.",
|
||||
"Olet käynnistänyt liian monta syötteen päivitystä. Odota %d minuuttia ennen kuin yrität uudelleen."
|
||||
],
|
||||
"confirm.loading": "Käynnissä...",
|
||||
"confirm.no": "ei",
|
||||
"confirm.question": "Oletko varma?",
|
||||
"confirm.question.refresh": "Haluatko pakottaa päivityksen?",
|
||||
"confirm.yes": "kyllä",
|
||||
"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.reset": "Reset",
|
||||
"enclosure_media_controls.speed.reset.title": "Reset speed to 1x",
|
||||
"enclosure_media_controls.speed.slower": "Slower",
|
||||
"enclosure_media_controls.speed.slower.title": "Slower by %sx",
|
||||
"enclosure_media_controls.seek": "Siirry:",
|
||||
"enclosure_media_controls.seek.title": "Siirry %s sekuntia",
|
||||
"enclosure_media_controls.speed": "Nopeus:",
|
||||
"enclosure_media_controls.speed.faster": "Nopeammin",
|
||||
"enclosure_media_controls.speed.faster.title": "Nopeampi %sx",
|
||||
"enclosure_media_controls.speed.reset": "Palauta",
|
||||
"enclosure_media_controls.speed.reset.title": "Palauta nopeus 1x",
|
||||
"enclosure_media_controls.speed.slower": "Hitaammin",
|
||||
"enclosure_media_controls.speed.slower.title": "Hitaampi %sx",
|
||||
"entry.bookmark.toast.off": "Tähdettömät",
|
||||
"entry.bookmark.toast.on": "Tähdellä merkityt",
|
||||
"entry.bookmark.toggle.off": "Poista suosikeista",
|
||||
@@ -72,35 +71,39 @@
|
||||
"entry.state.loading": "Ladataan...",
|
||||
"entry.state.saving": "Tallennetaan...",
|
||||
"entry.status.mark_as_read": "Merkitse luetuksi",
|
||||
"entry.status.mark_as_unread": "Merkitse lukemattomaksi",
|
||||
"entry.status.title": "Vaihda artikkelin tilaa",
|
||||
"entry.status.toast.read": "Merkitty luetuksi",
|
||||
"entry.status.toast.unread": "Merkitty lukemattomaksi",
|
||||
"entry.status.mark_as_unread": "Merkitse lukemattomaksi",
|
||||
"entry.tags.label": "Tags:",
|
||||
"entry.tags.label": "Tunnisteet:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Näytä %d lisää tunnistetta",
|
||||
"Näytä %d lisää tunnisteita"
|
||||
],
|
||||
"entry.unshare.label": "Poista jako",
|
||||
"error.api_key_already_exists": "API-avain on jo olemassa.",
|
||||
"error.bad_credentials": "Virheellinen käyttäjänimi tai salasana.",
|
||||
"error.category_already_exists": "Kategoria on jo olemassa. ",
|
||||
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||
"error.database_error": "Database error: %v.",
|
||||
"error.category_not_found": "Tämä kategoria ei ole olemassa tai se ei kuulu tälle käyttäjälle.",
|
||||
"error.database_error": "Tietokantavirhe: %v.",
|
||||
"error.different_passwords": "Salasanat eivät ole samat.",
|
||||
"error.duplicate_fever_username": "There is already someone else with the same Fever username!",
|
||||
"error.duplicate_fever_username": "Joku muu käyttää jo samaa Fever-käyttäjänimeä!",
|
||||
"error.duplicate_googlereader_username": "On jo joku muu, jolla on sama Google-syötteenlukijan käyttäjätunnus!",
|
||||
"error.duplicate_linked_account": "There is already someone associated with this provider!",
|
||||
"error.duplicated_feed": "This feed already exists.",
|
||||
"error.duplicate_linked_account": "Joku on jo yhdistetty tähän palveluntarjoajaan!",
|
||||
"error.duplicated_feed": "Tämä syöte on jo olemassa.",
|
||||
"error.empty_file": "Tiedosto on tyhjä.",
|
||||
"error.entries_per_page_invalid": "Artikkelien määrä sivulla ei kelpaa.",
|
||||
"error.feed_already_exists": "Tämä syöte on jo olemassa.",
|
||||
"error.feed_category_not_found": "Tätä kategoriaa ei ole olemassa tai se ei kuulu tälle käyttäjälle.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"error.feed_invalid_blocklist_rule": "The block list rule is invalid.",
|
||||
"error.feed_invalid_keeplist_rule": "The keep list rule is invalid.",
|
||||
"error.feed_format_not_detected": "Syötteen muotoa ei voitu tunnistaa: %v.",
|
||||
"error.feed_invalid_blocklist_rule": "Estolistan sääntö on virheellinen.",
|
||||
"error.feed_invalid_keeplist_rule": "Säilytettävien listan sääntö on virheellinen.",
|
||||
"error.feed_mandatory_fields": "URL-osoite ja kategoria ovat pakollisia.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.feed_not_found": "Tämä syöte ei ole olemassa tai se ei kuulu tälle käyttäjälle.",
|
||||
"error.feed_title_not_empty": "Syötteen otsikko ei voi olla tyhjä.",
|
||||
"error.feed_url_not_empty": "Syötteen URL-osoite ei voi olla tyhjä.",
|
||||
"error.fields_mandatory": "Kaikki kentät ovat pakollisia.",
|
||||
"error.http_bad_gateway": "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_bad_gateway": "Verkkosivusto ei ole tällä hetkellä saatavilla huonon yhdyskäytävän virheen vuoksi. Ongelma ei ole Miniflux-puolella. Yritä uudelleen myöhemmin.",
|
||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_client_error": "HTTP client error: %v.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
@@ -114,9 +117,12 @@
|
||||
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.invalid_categories_sorting_order": "Virheellinen kategorioiden lajittelujärjestys.",
|
||||
"error.invalid_default_home_page": "Väärä oletusarvoinen kotisivu!",
|
||||
"error.invalid_display_mode": "Virheellinen verkkosovelluksen näyttötila.",
|
||||
"error.invalid_entry_direction": "Invalid entry direction.",
|
||||
"error.invalid_entry_order": "Virheellinen artikkelin lajittelu.",
|
||||
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
|
||||
"error.invalid_feed_url": "Virheellinen syötteen URL-osoite.",
|
||||
"error.invalid_gesture_nav": "Virheellinen ele-navigointi.",
|
||||
"error.invalid_language": "Virheellinen kieli.",
|
||||
@@ -126,8 +132,7 @@
|
||||
"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.password_min_length": "Salasanassa on oltava vähintään 6 merkkiä.",
|
||||
"error.pocket_access_token": "Unable to fetch access token from Pocket!",
|
||||
"error.pocket_request_token": "Unable to fetch request token from Pocket!",
|
||||
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
@@ -164,7 +169,8 @@
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.label.allow_self_signed_certificates": "Salli itseallekirjoitetut tai virheelliset varmenteet",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.blocklist_rules": "Block-säännöt",
|
||||
"form.feed.label.block_filter_entry_rules": "Merkinnän estosäännöt",
|
||||
"form.feed.label.blocklist_rules": "Regex-pohjaiset estosuodattimet",
|
||||
"form.feed.label.category": "Kategoria",
|
||||
"form.feed.label.cookie": "Aseta evästeet",
|
||||
"form.feed.label.crawler": "Nouda alkuperäinen sisältö",
|
||||
@@ -174,10 +180,11 @@
|
||||
"form.feed.label.feed_password": "Syötteen salasana",
|
||||
"form.feed.label.feed_url": "Syötteen URL-osoite",
|
||||
"form.feed.label.feed_username": "Syötteen käyttäjätunnus",
|
||||
"form.feed.label.fetch_via_proxy": "Nouda välityspalvelimen kautta",
|
||||
"form.feed.label.fetch_via_proxy": "Käytä sovellustasolla määritettyä välityspalvelinta",
|
||||
"form.feed.label.hide_globally": "Piilota artikkelit lukemattomien listassa",
|
||||
"form.feed.label.ignore_http_cache": "Ohita HTTP-välimuisti",
|
||||
"form.feed.label.keeplist_rules": "Keep-säännöt",
|
||||
"form.feed.label.keep_filter_entry_rules": "Merkinnän sallimissäännöt",
|
||||
"form.feed.label.keeplist_rules": "Regex-pohjaiset säilytyssuodattimet",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
@@ -187,6 +194,7 @@
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
@@ -194,7 +202,7 @@
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "Rewrite-säännöt",
|
||||
"form.feed.label.rewrite_rules": "Sisällön uudelleenkirjoitussäännöt",
|
||||
"form.feed.label.scraper_rules": "Scraper-säännöt",
|
||||
"form.feed.label.site_url": "Sivuston URL-osoite",
|
||||
"form.feed.label.title": "Otsikko",
|
||||
@@ -228,6 +236,9 @@
|
||||
"form.integration.instapaper_activate": "Tallenna artikkelit Instapaperiin",
|
||||
"form.integration.instapaper_password": "Instapaper-salasana",
|
||||
"form.integration.instapaper_username": "Instapaper-käyttäjätunnus",
|
||||
"form.integration.karakeep_activate": "Tallenna artikkelit Karakeepiin",
|
||||
"form.integration.karakeep_api_key": "Karakeep API-avain",
|
||||
"form.integration.karakeep_url": "Karakeep API-päätepiste",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
@@ -241,7 +252,7 @@
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkwarden_activate": "Tallenna artikkelit Linkkiin",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API-avain",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden API-päätepiste",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden Base URL",
|
||||
"form.integration.matrix_bot_activate": "Siirrä uudet artikkelit Matrixiin",
|
||||
"form.integration.matrix_bot_chat_id": "Matrix-huoneen tunnus",
|
||||
"form.integration.matrix_bot_password": "Matrix-käyttäjän salasana",
|
||||
@@ -268,10 +279,6 @@
|
||||
"form.integration.pinboard_bookmark": "Merkitse kirjanmerkki lukemattomaksi",
|
||||
"form.integration.pinboard_tags": "Pinboard-tagit",
|
||||
"form.integration.pinboard_token": "Pinboard API-tunnus",
|
||||
"form.integration.pocket_access_token": "Pocket-käyttöoikeustunnus",
|
||||
"form.integration.pocket_activate": "Tallenna artikkelit Pocketiin",
|
||||
"form.integration.pocket_connect_link": "Yhdistä Pocket-tilisi",
|
||||
"form.integration.pocket_consumer_key": "Pocket Consumer Key",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
@@ -290,6 +297,7 @@
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
@@ -322,6 +330,7 @@
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Read articles by opening external links",
|
||||
"form.prefs.label.categories_sorting_order": "Kategorioiden lajittelu",
|
||||
"form.prefs.label.cjk_reading_speed": "Kiinan, Korean ja Japanin lukunopeus (merkkejä minuutissa)",
|
||||
"form.prefs.label.custom_css": "Mukautettu CSS",
|
||||
@@ -342,6 +351,7 @@
|
||||
"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.media_playback_rate": "Äänen/videon toistonopeus",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Avaa ulkoiset linkit uuteen välilehteen (lisää target=\"_blank\" linkkeihin)",
|
||||
"form.prefs.label.show_reading_time": "Näytä artikkeleiden arvioitu lukuaika",
|
||||
"form.prefs.label.theme": "Teema",
|
||||
"form.prefs.label.timezone": "Aikavyöhyke",
|
||||
@@ -378,7 +388,7 @@
|
||||
"menu.feeds": "Syötteet",
|
||||
"menu.flush_history": "Tyhjennä historia",
|
||||
"menu.history": "Historia",
|
||||
"menu.home_page": "Home page",
|
||||
"menu.home_page": "Etusivu",
|
||||
"menu.import": "Tuo",
|
||||
"menu.integrations": "Integraatiot",
|
||||
"menu.logout": "Kirjaudu ulos",
|
||||
@@ -392,7 +402,7 @@
|
||||
"menu.settings": "Asetukset",
|
||||
"menu.shared_entries": "Jaetut artikkelit",
|
||||
"menu.show_all_entries": "Näytä kaikki artikkelit",
|
||||
"menu.show_only_starred_entries": "Show only starred entries",
|
||||
"menu.show_only_starred_entries": "Näytä vain suosikit",
|
||||
"menu.show_only_unread_entries": "Näytä vain lukemattomat artikkelit",
|
||||
"menu.starred": "Suosikit",
|
||||
"menu.title": "Menu",
|
||||
@@ -402,6 +412,7 @@
|
||||
"page.about.build_date": "Valmistuspäivä:",
|
||||
"page.about.credits": "Kiitokset",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.global_config_options": "Yleiset asetukset",
|
||||
"page.about.go_version": "Go-versio:",
|
||||
"page.about.license": "Lisenssi:",
|
||||
@@ -566,14 +577,14 @@
|
||||
"page.users.title": "Käyttäjät",
|
||||
"page.users.username": "Käyttäjätunnus",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"pagination.first": "First",
|
||||
"pagination.last": "Last",
|
||||
"pagination.first": "Ensimmäinen",
|
||||
"pagination.last": "Viimeinen",
|
||||
"pagination.next": "Seuraava",
|
||||
"pagination.previous": "Edellinen",
|
||||
"search.label": "Haku",
|
||||
"search.placeholder": "Hae...",
|
||||
"search.submit": "Search",
|
||||
"skip_to_content": "Skip to content",
|
||||
"search.submit": "Hae",
|
||||
"skip_to_content": "Siirry sisältöön",
|
||||
"time_elapsed.days": [
|
||||
"%d päivä sitten",
|
||||
"%d päivää sitten"
|
||||
@@ -603,4 +614,4 @@
|
||||
"time_elapsed.yesterday": "eilen",
|
||||
"tooltip.keyboard_shortcuts": "Pikanäppäin: %s",
|
||||
"tooltip.logged_user": "Kirjautunut %s-käyttäjänä"
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@
|
||||
"alert.no_tag_entry": "Il n'y a aucun article correspondant à ce tag.",
|
||||
"alert.no_unread_entry": "Il n'y a rien de nouveau à lire.",
|
||||
"alert.no_user": "Vous êtes le seul utilisateur.",
|
||||
"alert.pocket_linked": "Votre compte Pocket est maintenant connecté !",
|
||||
"alert.prefs_saved": "Préférences sauvegardées !",
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"Vous avez déclenché trop d'actualisations de flux. Veuillez attendre %d minute avant de réessayer.",
|
||||
@@ -72,11 +71,15 @@
|
||||
"entry.state.loading": "Chargement...",
|
||||
"entry.state.saving": "Sauvegarde en cours...",
|
||||
"entry.status.mark_as_read": "Marquer comme lu",
|
||||
"entry.status.mark_as_unread": "Marquer comme non lu",
|
||||
"entry.status.title": "Changer le statut de l'entrée",
|
||||
"entry.status.toast.read": "Marqué comme lu",
|
||||
"entry.status.toast.unread": "Marqué comme non lu",
|
||||
"entry.status.mark_as_unread": "Marquer comme non lu",
|
||||
"entry.tags.label": "Libellés :",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Afficher %d libellé supplémentaire",
|
||||
"Afficher %d libellés supplémentaires"
|
||||
],
|
||||
"entry.unshare.label": "Enlever le partage",
|
||||
"error.api_key_already_exists": "Cette clé d'API existe déjà.",
|
||||
"error.bad_credentials": "Mauvais identifiant ou mot de passe.",
|
||||
@@ -114,9 +117,12 @@
|
||||
"error.http_service_unavailable": "Le site web n'est pas disponible pour le moment. Le problème ne vient pas de Miniflux. Veuillez réessayer plus tard.",
|
||||
"error.http_too_many_requests": "Miniflux a généré trop de requêtes vers ce site web. Veuillez réessayer plus tard ou changez la configuration de l'application.",
|
||||
"error.http_unexpected_status_code": "Le site web a répondu avec un code HTTP inattendu : %d. Le problème ne vient pas de Miniflux. Veuillez réessayer plus tard.",
|
||||
"error.invalid_categories_sorting_order": "L'ordre de tri des catégories n'est pas valide.",
|
||||
"error.invalid_default_home_page": "Page d'accueil par défaut invalide !",
|
||||
"error.invalid_display_mode": "Mode d'affichage de l'application web non valide.",
|
||||
"error.invalid_entry_direction": "Ordre de trie non valide.",
|
||||
"error.invalid_entry_order": "Ordre de tri non valide.",
|
||||
"error.invalid_feed_proxy_url": "L'URL du proxy n'est pas valide.",
|
||||
"error.invalid_feed_url": "URL de flux non valide.",
|
||||
"error.invalid_gesture_nav": "Navigation gestuelle non valide.",
|
||||
"error.invalid_language": "Langue non valide.",
|
||||
@@ -126,8 +132,7 @@
|
||||
"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.password_min_length": "Vous devez utiliser au moins 6 caractères pour le mot de passe.",
|
||||
"error.pocket_access_token": "Impossible de récupérer le jeton d'accès depuis Pocket !",
|
||||
"error.pocket_request_token": "Impossible de récupérer le jeton d'accès depuis Pocket !",
|
||||
"error.proxy_url_not_empty": "L'URL du proxy ne peut pas être vide.",
|
||||
"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_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_block_rule_regex_required": "Règle de blocage invalide : le motif de la règle n°%d n'est pas fourni",
|
||||
@@ -164,7 +169,8 @@
|
||||
"form.feed.fieldset.rules": "Règles",
|
||||
"form.feed.label.allow_self_signed_certificates": "Autoriser les certificats auto-signés ou non valides",
|
||||
"form.feed.label.apprise_service_urls": "Liste séparée par des virgules des URL du service Apprise",
|
||||
"form.feed.label.blocklist_rules": "Règles de blocage",
|
||||
"form.feed.label.block_filter_entry_rules": "Règles de blocage des entrées",
|
||||
"form.feed.label.blocklist_rules": "Filtres de blocage basés sur des expressions régulières",
|
||||
"form.feed.label.category": "Catégorie",
|
||||
"form.feed.label.cookie": "Définir les cookies",
|
||||
"form.feed.label.crawler": "Récupérer le contenu original",
|
||||
@@ -174,10 +180,11 @@
|
||||
"form.feed.label.feed_password": "Mot de passe du flux",
|
||||
"form.feed.label.feed_url": "URL du flux",
|
||||
"form.feed.label.feed_username": "Nom d'utilisateur du flux",
|
||||
"form.feed.label.fetch_via_proxy": "Récupérer via proxy",
|
||||
"form.feed.label.fetch_via_proxy": "Utiliser le proxy configuré au niveau de l'application",
|
||||
"form.feed.label.hide_globally": "Masquer les entrées dans la liste globale non lue",
|
||||
"form.feed.label.ignore_http_cache": "Ignorer le cache HTTP",
|
||||
"form.feed.label.keeplist_rules": "Règles d'autorisation",
|
||||
"form.feed.label.keep_filter_entry_rules": "Règles d'autorisation des entrées",
|
||||
"form.feed.label.keeplist_rules": "Filtres de conservation basés sur des expressions régulières",
|
||||
"form.feed.label.no_media_player": "Pas de lecteur multimedia (audio/vidéo)",
|
||||
"form.feed.label.ntfy_activate": "Activer les notifications",
|
||||
"form.feed.label.ntfy_default_priority": "Priorité par défaut de notification",
|
||||
@@ -187,6 +194,7 @@
|
||||
"form.feed.label.ntfy_min_priority": "Priorité minimale de notification",
|
||||
"form.feed.label.ntfy_priority": "Priorité de notification",
|
||||
"form.feed.label.ntfy_topic": "Sujet Ntfy (facultatif)",
|
||||
"form.feed.label.proxy_url": "URL du proxy",
|
||||
"form.feed.label.pushover_activate": "Activer les notifications vers Pushover",
|
||||
"form.feed.label.pushover_default_priority": "Priorité par défaut",
|
||||
"form.feed.label.pushover_high_priority": "Priorité élevée",
|
||||
@@ -194,7 +202,7 @@
|
||||
"form.feed.label.pushover_max_priority": "Priorité maximale",
|
||||
"form.feed.label.pushover_min_priority": "Priorité minimale",
|
||||
"form.feed.label.pushover_priority": "Priorité des notifications Pushover",
|
||||
"form.feed.label.rewrite_rules": "Règles de réécriture",
|
||||
"form.feed.label.rewrite_rules": "Règles de réécriture du contenu",
|
||||
"form.feed.label.scraper_rules": "Règles pour récupérer le contenu original",
|
||||
"form.feed.label.site_url": "URL du site web",
|
||||
"form.feed.label.title": "Titre",
|
||||
@@ -209,8 +217,8 @@
|
||||
"form.integration.betula_activate": "Sauvegarder les entrées vers Betula",
|
||||
"form.integration.betula_token": "Jeton de sécurité de l'API de Betula",
|
||||
"form.integration.betula_url": "URL du serveur Betula",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.integration.cubox_activate": "Sauvegarder les entrées vers Cubox",
|
||||
"form.integration.cubox_api_link": "Lien API Cubox",
|
||||
"form.integration.discord_activate": "Envoyer les articles vers Discord",
|
||||
"form.integration.discord_webhook_link": "URL du Webhook Discord",
|
||||
"form.integration.espial_activate": "Sauvegarder les articles vers Espial",
|
||||
@@ -228,6 +236,9 @@
|
||||
"form.integration.instapaper_activate": "Sauvegarder les articles vers Instapaper",
|
||||
"form.integration.instapaper_password": "Mot de passe Instapaper",
|
||||
"form.integration.instapaper_username": "Nom d'utilisateur Instapaper",
|
||||
"form.integration.karakeep_activate": "Sauvegarder les articles vers Karakeep",
|
||||
"form.integration.karakeep_api_key": "Clé d'API de Karakeep",
|
||||
"form.integration.karakeep_url": "URL de l'API de Karakeep",
|
||||
"form.integration.linkace_activate": "Enregistrer les entrées vers LinkAce",
|
||||
"form.integration.linkace_api_key": "Clé d'API LinkAce",
|
||||
"form.integration.linkace_check_disabled": "Désactiver la vérification des liens",
|
||||
@@ -241,7 +252,7 @@
|
||||
"form.integration.linkding_tags": "Libellés",
|
||||
"form.integration.linkwarden_activate": "Sauvegarder les articles vers Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Clé d'API de Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "URL de l'API de Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "URL de base de Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Envoyer les nouveaux articles vers Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Identifiant de la salle Matrix",
|
||||
"form.integration.matrix_bot_password": "Mot de passe de l'utilisateur Matrix",
|
||||
@@ -268,10 +279,6 @@
|
||||
"form.integration.pinboard_bookmark": "Marquer le lien comme non lu",
|
||||
"form.integration.pinboard_tags": "Libellés de Pinboard",
|
||||
"form.integration.pinboard_token": "Jeton de sécurité de l'API de Pinboard",
|
||||
"form.integration.pocket_access_token": "Jeton d'accès de l'API de Pocket",
|
||||
"form.integration.pocket_activate": "Sauvegarder les articles vers Pocket",
|
||||
"form.integration.pocket_connect_link": "Connectez votre compte Pocket",
|
||||
"form.integration.pocket_consumer_key": "Clé de l'API de Pocket",
|
||||
"form.integration.pushover_activate": "Envoyer les articles vers Pushover",
|
||||
"form.integration.pushover_device": "Nom de l'appareil Pushover (facultatif)",
|
||||
"form.integration.pushover_prefix": "URL de préfixe Pushover (facultatif)",
|
||||
@@ -284,13 +291,14 @@
|
||||
"form.integration.readeck_activate": "Sauvegarder les articles vers Readeck",
|
||||
"form.integration.readeck_api_key": "Clé d'API de Readeck",
|
||||
"form.integration.readeck_endpoint": "URL de l'API de Readeck",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_labels": "Libellés Readeck",
|
||||
"form.integration.readeck_only_url": "Envoyer uniquement l'URL (au lieu du contenu complet)",
|
||||
"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.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.rssbridge_activate": "Vérifier RSS-Bridge lors de l'ajout d'abonnements",
|
||||
"form.integration.rssbridge_token": "Jeton d'authentification RSS-Bridge",
|
||||
"form.integration.rssbridge_url": "URL du serveur RSS-Bridge",
|
||||
"form.integration.shaarli_activate": "Sauvegarder les articles vers Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Clé d'API de Shaarli API",
|
||||
"form.integration.shaarli_endpoint": "URL de l'API de Shaarli",
|
||||
@@ -322,6 +330,7 @@
|
||||
"form.prefs.fieldset.global_feed_settings": "Paramètres globaux des abonnements",
|
||||
"form.prefs.fieldset.reader_settings": "Paramètres du lecteur",
|
||||
"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 ».",
|
||||
"form.prefs.label.always_open_external_links": "Lire les articles en ouvrant les liens externes",
|
||||
"form.prefs.label.categories_sorting_order": "Colonne de tri des catégories",
|
||||
"form.prefs.label.cjk_reading_speed": "Vitesse de lecture pour le chinois, le coréen et le japonais (caractères par minute)",
|
||||
"form.prefs.label.custom_css": "Feuille de style personnalisée",
|
||||
@@ -342,6 +351,7 @@
|
||||
"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.media_playback_rate": "Vitesse de lecture de l'audio/vidéo",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Ouvrir les liens externes dans un nouvel onglet (ajoute target=\"_blank\" aux liens)",
|
||||
"form.prefs.label.show_reading_time": "Afficher le temps de lecture estimé des articles",
|
||||
"form.prefs.label.theme": "Thème",
|
||||
"form.prefs.label.timezone": "Fuseau horaire",
|
||||
@@ -401,7 +411,8 @@
|
||||
"page.about.author": "Auteur :",
|
||||
"page.about.build_date": "Date de la compilation :",
|
||||
"page.about.credits": "Crédits",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.db_usage": "Taille de la base de données :",
|
||||
"page.about.git_commit": "Commit Git :",
|
||||
"page.about.global_config_options": "Options de configuration globales",
|
||||
"page.about.go_version": "Version de Go :",
|
||||
"page.about.license": "Licence :",
|
||||
@@ -496,7 +507,7 @@
|
||||
"page.keyboard_shortcuts.subtitle.sections": "Navigation entre les sections",
|
||||
"page.keyboard_shortcuts.title": "Raccourcis clavier",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Ajouter/Enlever favoris",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Ouvrir/Fermer les pièces jointes de l'entrée",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "Basculer entre lu/non lu, et changer le focus sur l'élément suivant",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Basculer entre lu/non lu, et changer le focus sur l'élément précédent",
|
||||
"page.login.google_signin": "Se connecter avec Google",
|
||||
@@ -512,8 +523,8 @@
|
||||
"page.offline.refresh_page": "Essayez de rafraîchir la page",
|
||||
"page.offline.title": "Mode Hors-Ligne",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries"
|
||||
"%d entrée lue",
|
||||
"%d entrées lues"
|
||||
],
|
||||
"page.search.title": "Résultats de la recherche",
|
||||
"page.sessions.table.actions": "Actions",
|
||||
@@ -536,7 +547,7 @@
|
||||
"page.settings.webauthn.last_seen_on": "Dernière utilisation",
|
||||
"page.settings.webauthn.passkey_name": "Nom de la clé d’accès",
|
||||
"page.settings.webauthn.passkeys": "Clés d’accès",
|
||||
"page.settings.webauthn.register": "Enregister une nouvelle clé d’accès",
|
||||
"page.settings.webauthn.register": "Enregistrer une nouvelle clé d’accès",
|
||||
"page.settings.webauthn.register.error": "Impossible d'enregistrer la clé d’accès",
|
||||
"page.shared_entries.title": "Articles partagés",
|
||||
"page.shared_entries_count": [
|
||||
@@ -565,7 +576,7 @@
|
||||
"page.users.never_logged": "Jamais",
|
||||
"page.users.title": "Utilisateurs",
|
||||
"page.users.username": "Nom d'utilisateur",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"page.webauthn_rename.title": "Renommer la clé d'accès",
|
||||
"pagination.first": "Première page",
|
||||
"pagination.last": "Dernière page",
|
||||
"pagination.next": "Suivant",
|
||||
@@ -603,4 +614,4 @@
|
||||
"time_elapsed.yesterday": "hier",
|
||||
"tooltip.keyboard_shortcuts": "Raccourci clavier : %s",
|
||||
"tooltip.logged_user": "Connecté en tant que %s"
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"action.update": "नवीनीकरण करे",
|
||||
"alert.account_linked": "आपका बाहरी खाता अब लिंक हो गया है!",
|
||||
"alert.account_unlinked": "आपका बाहरी खाता अब अलग कर दिया गया है!",
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"alert.background_feed_refresh": "सभी फ़ीड्स पृष्ठभूमि में ताज़ा की जा रही हैं। जब यह प्रक्रिया चल रही हो, तो आप मिनीफ्लक्स का उपयोग जारी रख सकते हैं।",
|
||||
"alert.feed_error": "इस फ़ीड में एक समस्या है",
|
||||
"alert.no_bookmark": "इस समय कोई बुकमार्क नहीं है",
|
||||
"alert.no_category": "कोई श्रेणी नहीं है।",
|
||||
@@ -27,26 +27,25 @@
|
||||
"alert.no_tag_entry": "इस टैग से मेल खाती कोई प्रविष्टियाँ नहीं हैं।",
|
||||
"alert.no_unread_entry": "कोई अपठित वस्तुत नहीं है।",
|
||||
"alert.no_user": "आप एकमात्र उपयोगकर्ता हैं।",
|
||||
"alert.pocket_linked": "आपका पॉकेट खाता अब लिंक हो गया है!",
|
||||
"alert.prefs_saved": "प्राथमिकताएं सहेजी गईं!",
|
||||
"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 मिनट प्रतीक्षा करें।",
|
||||
"आपने बहुत अधिक फ़ीड ताज़ा करने की प्रक्रिया शुरू कर दी है। कृपया पुनः प्रयास करने से पहले %d मिनट प्रतीक्षा करें।"
|
||||
],
|
||||
"confirm.loading": " प्रगति में है ...",
|
||||
"confirm.no": " नहीं",
|
||||
"confirm.question": "मंजूर है?",
|
||||
"confirm.question.refresh": "क्या आप बल द्वारा ताज़ा करना चाहते हैं?",
|
||||
"confirm.yes": "हाँ",
|
||||
"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.reset": "Reset",
|
||||
"enclosure_media_controls.speed.reset.title": "Reset speed to 1x",
|
||||
"enclosure_media_controls.speed.slower": "Slower",
|
||||
"enclosure_media_controls.speed.slower.title": "Slower by %sx",
|
||||
"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.reset": "रीसेट करें",
|
||||
"enclosure_media_controls.speed.reset.title": "गति 1x पर रीसेट करें",
|
||||
"enclosure_media_controls.speed.slower": "धीमा",
|
||||
"enclosure_media_controls.speed.slower.title": "%sx गुना धीमा",
|
||||
"entry.bookmark.toast.off": "तारांकित न करे",
|
||||
"entry.bookmark.toast.on": "तारांकित",
|
||||
"entry.bookmark.toggle.off": "सितारा हटा दो",
|
||||
@@ -72,40 +71,44 @@
|
||||
"entry.state.loading": "लोड हो रहा है...",
|
||||
"entry.state.saving": "सहेजा जा रहा है...",
|
||||
"entry.status.mark_as_read": "पढ़े हुए का चिह्न",
|
||||
"entry.status.mark_as_unread": "अपठित के रूप में चिह्नित करें",
|
||||
"entry.status.title": "प्रविष्टि स्थिति बदलें",
|
||||
"entry.status.toast.read": "पढ़ा हुआ चिह्नित करे",
|
||||
"entry.status.toast.unread": "अपठित के रूप में चिह्नित",
|
||||
"entry.status.mark_as_unread": "अपठित के रूप में चिह्नित करें",
|
||||
"entry.tags.label": "टैग:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"%d और टैग दिखाएँ",
|
||||
"%d और टैग दिखाएँ"
|
||||
],
|
||||
"entry.unshare.label": "न साझा कारें",
|
||||
"error.api_key_already_exists": "यह एपीआई कुंजी पहले से मौजूद है।",
|
||||
"error.bad_credentials": "अमान्य उपयोगकर्ता नाम या पासवर्ड।",
|
||||
"error.category_already_exists": "यह श्रेणी पहले से मौजूद है।",
|
||||
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||
"error.database_error": "Database error: %v.",
|
||||
"error.category_not_found": "यह श्रेणी मौजूद नहीं है या इस उपयोगकर्ता से संबंधित नहीं है।",
|
||||
"error.database_error": "डेटाबेस त्रुटि: %v।",
|
||||
"error.different_passwords": "पासवर्ड एक जैसे नहीं हैं।",
|
||||
"error.duplicate_fever_username": "पहले से ही समान फीवर उपयोगकर्ता नाम वाला कोई और है!",
|
||||
"error.duplicate_googlereader_username": "समान गूगल रीडर उपयोगकर्ता नाम वाला कोई और पहले से मौजूद है!",
|
||||
"error.duplicate_linked_account": "इस प्रदाता के साथ पहले से ही कोई व्यक्ति जुड़ा हुआ है!",
|
||||
"error.duplicated_feed": "This feed already exists.",
|
||||
"error.duplicated_feed": "यह फ़ीड पहले से मौजूद है।",
|
||||
"error.empty_file": "यह फ़ाइल खाली है।",
|
||||
"error.entries_per_page_invalid": "प्रति पृष्ठ प्रविष्टियों की संख्या मान्य नहीं है।",
|
||||
"error.feed_already_exists": "यह फ़ीड पहले से मौजूद है.",
|
||||
"error.feed_category_not_found": "यह श्रेणी मौजूद नहीं है या इस उपयोगकर्ता से संबंधित नहीं है।",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"error.feed_format_not_detected": "फ़ीड प्रारूप का पता नहीं लगा सकते: %v।",
|
||||
"error.feed_invalid_blocklist_rule": "ब्लॉक सूची नियम अमान्य है।",
|
||||
"error.feed_invalid_keeplist_rule": "सूची रखें नियम अमान्य है।",
|
||||
"error.feed_mandatory_fields": "URL और श्रेणी अनिवार्य हैं।",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.feed_not_found": "यह फ़ीड मौजूद नहीं है या इस उपयोगकर्ता से संबंधित नहीं है।",
|
||||
"error.feed_title_not_empty": "फ़ीड शीर्षक खाली नहीं हो सकता.",
|
||||
"error.feed_url_not_empty": "फ़ीड यूआरएल खाली नहीं हो सकता.",
|
||||
"error.fields_mandatory": "सभी फील्ड अनिवार्य।",
|
||||
"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_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_client_error": "HTTP client error: %v.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||
"error.http_bad_gateway": "खराब गेटवे त्रुटि के कारण वेबसाइट फिलहाल उपलब्ध नहीं है। समस्या Miniflux की तरफ नहीं है। कृपया बाद में फिर से कोशिश करें।",
|
||||
"error.http_body_read": "HTTP बॉडी पढ़ने में असमर्थ: %v।",
|
||||
"error.http_client_error": "HTTP क्लाइंट त्रुटि: %v।",
|
||||
"error.http_empty_response": "HTTP प्रतिक्रिया खाली है। शायद यह वेबसाइट बॉट सुरक्षा तंत्र का उपयोग कर रही है?",
|
||||
"error.http_empty_response_body": "HTTP प्रतिक्रिया बॉडी खाली है।",
|
||||
"error.http_forbidden": "इस वेबसाइट तक पहुंच वर्जित है। शायद इस वेबसाइट में बॉट सुरक्षा तंत्र है?",
|
||||
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
@@ -114,9 +117,12 @@
|
||||
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.invalid_categories_sorting_order": "अमान्य श्रेणी क्रम।",
|
||||
"error.invalid_default_home_page": "अमान्य डिफ़ॉल्ट मुखपृष्ठ!",
|
||||
"error.invalid_display_mode": "अमान्य वेब ऐप्लिकेशन प्रदर्शन मोड.",
|
||||
"error.invalid_entry_direction": "अमान्य प्रवेश दिशा।",
|
||||
"error.invalid_entry_order": "अमान्य प्रविष्टि क्रम।",
|
||||
"error.invalid_feed_proxy_url": "अमान्य प्रॉक्सी यूआरएल।",
|
||||
"error.invalid_feed_url": "दृष्टिकोण यूआरएल.",
|
||||
"error.invalid_gesture_nav": "अमान्य इशारा नेविगेशन।",
|
||||
"error.invalid_language": "अमान्य भाषा.",
|
||||
@@ -126,8 +132,7 @@
|
||||
"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.password_min_length": "पासवर्ड में कम से कम 6 अक्षर होने चाहिए।",
|
||||
"error.pocket_access_token": "पॉकेट से एक्सेस टोकन प्राप्त करने में असमर्थ!",
|
||||
"error.pocket_request_token": "पॉकेट से अनुरोध टोकन लाने में असमर्थ!",
|
||||
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
@@ -147,8 +152,8 @@
|
||||
"error.unable_to_create_api_key": "यह एपीआई कुंजी बनाने में असमर्थ।",
|
||||
"error.unable_to_create_category": "यह श्रेणी बनाने में असमर्थ.",
|
||||
"error.unable_to_create_user": "इस उपयोगकर्ता को बनाने में असमर्थ।",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.unable_to_detect_rssbridge": "RSS-Bridge का उपयोग करके फ़ीड का पता लगाने में असमर्थ: %v.",
|
||||
"error.unable_to_parse_feed": "इस फ़ीड को पार्स करने में असमर्थ: %v.",
|
||||
"error.unable_to_update_category": "इस श्रेणी को अपडेट करने में असमर्थ।",
|
||||
"error.unable_to_update_feed": "इस फ़ीड को अपडेट करने में असमर्थ.",
|
||||
"error.unable_to_update_user": "इस उपयोगकर्ता को अपडेट करने में असमर्थ.",
|
||||
@@ -164,7 +169,8 @@
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.label.allow_self_signed_certificates": "स्व-हस्ताक्षरित या अमान्य प्रमाणपत्रों की अनुमति दें",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.blocklist_rules": "ब्लॉक नियम",
|
||||
"form.feed.label.block_filter_entry_rules": "प्रविष्टि अवरोधन नियम",
|
||||
"form.feed.label.blocklist_rules": "रेगेक्स-आधारित अवरोधन फिल्टर",
|
||||
"form.feed.label.category": "श्रेणी",
|
||||
"form.feed.label.cookie": "कुकीज़ सेट करें",
|
||||
"form.feed.label.crawler": "मूल सामग्री प्राप्त करें",
|
||||
@@ -174,10 +180,11 @@
|
||||
"form.feed.label.feed_password": "फ़ीड पासवर्ड",
|
||||
"form.feed.label.feed_url": "फ़ीड यूआरएल",
|
||||
"form.feed.label.feed_username": "फ़ीड उपयोगकर्ता नाम",
|
||||
"form.feed.label.fetch_via_proxy": "प्रॉक्सी के माध्यम से प्राप्त करें",
|
||||
"form.feed.label.fetch_via_proxy": "एप्लिकेशन स्तर पर कॉन्फ़िगर किए गए प्रॉक्सी का उपयोग करें",
|
||||
"form.feed.label.hide_globally": "वैश्विक अपठित सूची में प्रविष्टियां छिपाएं",
|
||||
"form.feed.label.ignore_http_cache": "एचटीटीपी कैश पर ध्यान न दें",
|
||||
"form.feed.label.keeplist_rules": "नियम बनाए रखें",
|
||||
"form.feed.label.keep_filter_entry_rules": "प्रविष्टि अनुमति नियम",
|
||||
"form.feed.label.keeplist_rules": "रेगेक्स-आधारित रखने वाले फिल्टर",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
@@ -187,6 +194,7 @@
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
@@ -194,7 +202,7 @@
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "नियम फिर से लिखें",
|
||||
"form.feed.label.rewrite_rules": "सामग्री पुनर्लेखन नियम",
|
||||
"form.feed.label.scraper_rules": "खुरचनी नियम",
|
||||
"form.feed.label.site_url": "साइट यूआरएल",
|
||||
"form.feed.label.title": "शीर्षक",
|
||||
@@ -228,6 +236,9 @@
|
||||
"form.integration.instapaper_activate": "विषय-वस्तु को इंस्टापेपर में सहेजें",
|
||||
"form.integration.instapaper_password": "इंस्टापेपर पासवर्ड",
|
||||
"form.integration.instapaper_username": "इंस्टापेपर यूजरनेम",
|
||||
"form.integration.karakeep_activate": "Save entries to Karakeep",
|
||||
"form.integration.karakeep_api_key": "Karakeep API key",
|
||||
"form.integration.karakeep_url": "Karakeep API Endpoint",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
@@ -241,7 +252,7 @@
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkwarden_activate": "Save entries to Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API key",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden API Endpoint",
|
||||
"form.integration.linkwarden_endpoint": "लिंकवर्डन बेस यूआरएलL",
|
||||
"form.integration.matrix_bot_activate": "नए लेखों को मैट्रिक्स में स्थानांतरित करें",
|
||||
"form.integration.matrix_bot_chat_id": "मैट्रिक्स रूम की आईडी",
|
||||
"form.integration.matrix_bot_password": "मैट्रिक्स उपयोगकर्ता के लिए पासवर्ड",
|
||||
@@ -268,10 +279,6 @@
|
||||
"form.integration.pinboard_bookmark": "बुकमार्क को अपठित के रूप में चिह्नित करें",
|
||||
"form.integration.pinboard_tags": "पिनबोर्ड टैग",
|
||||
"form.integration.pinboard_token": "पिनबोर्ड एपीआई टोकन",
|
||||
"form.integration.pocket_access_token": "पॉकेट एक्सेस टोकन",
|
||||
"form.integration.pocket_activate": "विषय-कविता को पॉकेट में सहेजें",
|
||||
"form.integration.pocket_connect_link": "अपना पॉकेट खाता कनेक्ट करें",
|
||||
"form.integration.pocket_consumer_key": "पॉकेट उपभोक्ता कुंजी",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
@@ -290,6 +297,7 @@
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
@@ -322,6 +330,7 @@
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Read articles by opening external links",
|
||||
"form.prefs.label.categories_sorting_order": "श्रेणियाँ छँटाई",
|
||||
"form.prefs.label.cjk_reading_speed": "चीनी, कोरियाई और जापानी के लिए पढ़ने की गति (प्रति मिनट वर्ण)",
|
||||
"form.prefs.label.custom_css": "कस्टम सीएसएस",
|
||||
@@ -342,6 +351,7 @@
|
||||
"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.media_playback_rate": "ऑडियो/वीडियो की प्लेबैक गति",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "बाहरी लिंक को एक नए टैब में खोलें (लिंक में target=\"_blank\" जोड़ता है)",
|
||||
"form.prefs.label.show_reading_time": "विषय के लिए अनुमानित पढ़ने का समय दिखाएं",
|
||||
"form.prefs.label.theme": "थीम",
|
||||
"form.prefs.label.timezone": "समय क्षेत्र",
|
||||
@@ -402,6 +412,7 @@
|
||||
"page.about.build_date": "बनाने की तिथि:",
|
||||
"page.about.credits": "आभार सूची",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.global_config_options": "वैश्विक विन्यास विकल्प",
|
||||
"page.about.go_version": "गो संस्करण:",
|
||||
"page.about.license": "अनुज्ञा:",
|
||||
@@ -566,14 +577,14 @@
|
||||
"page.users.title": "उपभोक्ता",
|
||||
"page.users.username": "यूसर्नेम",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"pagination.first": "First",
|
||||
"pagination.last": "Last",
|
||||
"pagination.first": "पहला",
|
||||
"pagination.last": "अंतिम",
|
||||
"pagination.next": "अगला",
|
||||
"pagination.previous": "पिछला",
|
||||
"search.label": "खोजे",
|
||||
"search.placeholder": "खोजे...",
|
||||
"search.submit": "Search",
|
||||
"skip_to_content": "Skip to content",
|
||||
"search.submit": "खोजें",
|
||||
"skip_to_content": "सामग्री पर जाएं",
|
||||
"time_elapsed.days": [
|
||||
"%d दिन पहले",
|
||||
"%d दिन पहले"
|
||||
@@ -591,7 +602,7 @@
|
||||
"%d महिनो पहले"
|
||||
],
|
||||
"time_elapsed.not_yet": "अभी तक नहीं",
|
||||
"time_elapsed.now": "बिल्कुल अभी",
|
||||
"time_elapsed.now": "अभी",
|
||||
"time_elapsed.weeks": [
|
||||
"%d सप्ताह पहले",
|
||||
"%d हफ्तों पहले"
|
||||
@@ -601,6 +612,6 @@
|
||||
"%d वर्षों पहले"
|
||||
],
|
||||
"time_elapsed.yesterday": "कल",
|
||||
"tooltip.keyboard_shortcuts": "कुंजीपटल संक्षिप्त रीति: %s",
|
||||
"tooltip.keyboard_shortcuts": "कुंजीपटल शॉर्टकट: %s",
|
||||
"tooltip.logged_user": "%s के रूप में लॉग इन किया"
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"action.update": "Perbarui",
|
||||
"alert.account_linked": "Akun eksternal Anda sudah terhubung!",
|
||||
"alert.account_unlinked": "Akun eksternal Anda sudah terputus!",
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"alert.background_feed_refresh": "Semua umpan sedang disegarkan di latar belakang. Anda bisa lanjut menggunakan Miniflux sembari proses ini berlanjut.",
|
||||
"alert.feed_error": "Ada masalah dengan umpan ini",
|
||||
"alert.no_bookmark": "Tidak ada markah.",
|
||||
"alert.no_category": "Tidak ada kategori.",
|
||||
@@ -27,25 +27,24 @@
|
||||
"alert.no_tag_entry": "Tidak ada entri yang cocok dengan tag ini.",
|
||||
"alert.no_unread_entry": "Belum ada artikel yang dibaca.",
|
||||
"alert.no_user": "Anda adalah satu-satunya pengguna.",
|
||||
"alert.pocket_linked": "Akun Pocket Anda sudah terhubung!",
|
||||
"alert.prefs_saved": "Preferensi disimpan!",
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again."
|
||||
"Anda terlalu banyak menyegarkan umpan. Mohon tunggu %d menit sebelum mencoba lagi."
|
||||
],
|
||||
"confirm.loading": "Sedang progres...",
|
||||
"confirm.no": "tidak",
|
||||
"confirm.question": "Apakah Anda yakin?",
|
||||
"confirm.question.refresh": "Apakah Anda ingin memaksa penyegaran?",
|
||||
"confirm.yes": "ya",
|
||||
"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.reset": "Reset",
|
||||
"enclosure_media_controls.speed.reset.title": "Reset speed to 1x",
|
||||
"enclosure_media_controls.speed.slower": "Slower",
|
||||
"enclosure_media_controls.speed.slower.title": "Slower by %sx",
|
||||
"enclosure_media_controls.seek": "Putar:",
|
||||
"enclosure_media_controls.seek.title": "Putar %s detik",
|
||||
"enclosure_media_controls.speed": "Kecepatan:",
|
||||
"enclosure_media_controls.speed.faster": "Lebih cepat",
|
||||
"enclosure_media_controls.speed.faster.title": "Lebih cepat %sx",
|
||||
"enclosure_media_controls.speed.reset": "Atur ulang",
|
||||
"enclosure_media_controls.speed.reset.title": "Atur ulang ke 1x",
|
||||
"enclosure_media_controls.speed.slower": "Lebih lambat",
|
||||
"enclosure_media_controls.speed.slower.title": "Lebih lambat %sx",
|
||||
"entry.bookmark.toast.off": "Batal Markahi",
|
||||
"entry.bookmark.toast.on": "Markahi",
|
||||
"entry.bookmark.toggle.off": "Batal Markahi",
|
||||
@@ -70,83 +69,88 @@
|
||||
"entry.state.loading": "Memuat...",
|
||||
"entry.state.saving": "Menyimpan...",
|
||||
"entry.status.mark_as_read": "Telah dibaca",
|
||||
"entry.status.mark_as_unread": "Belum dibaca",
|
||||
"entry.status.title": "Ubah status entri",
|
||||
"entry.status.toast.read": "Ditandai sebagai telah dibaca",
|
||||
"entry.status.toast.unread": "Ditandai sebagai belum dibaca",
|
||||
"entry.status.mark_as_unread": "Belum dibaca",
|
||||
"entry.tags.label": "Tanda:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Tampilkan %d tag lainnya"
|
||||
],
|
||||
"entry.unshare.label": "Batal bagikan",
|
||||
"error.api_key_already_exists": "Kunci API ini sudah ada.",
|
||||
"error.bad_credentials": "Nama pengguna atau kata sandi tidak valid.",
|
||||
"error.category_already_exists": "Kategori ini telah ada.",
|
||||
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||
"error.database_error": "Database error: %v.",
|
||||
"error.category_not_found": "Kategori ini tidak ada atau tidak dipunyai oleh pengguna ini.",
|
||||
"error.database_error": "Galat basis data: %v.",
|
||||
"error.different_passwords": "Kata sandi tidak sama.",
|
||||
"error.duplicate_fever_username": "Sudah ada orang lain dengan nama pengguna Fever yang sama!",
|
||||
"error.duplicate_googlereader_username": "Sudah ada orang lain dengan nama pengguna Google Reader yang sama!",
|
||||
"error.duplicate_linked_account": "Sudah ada orang lain yang terhubung dengan penyedia ini!",
|
||||
"error.duplicated_feed": "This feed already exists.",
|
||||
"error.duplicate_fever_username": "Sudah ada pengguna lain dengan nama pengguna Fever yang sama!",
|
||||
"error.duplicate_googlereader_username": "Sudah ada pengguna lain dengan nama pengguna Google Reader yang sama!",
|
||||
"error.duplicate_linked_account": "Sudah ada pengguna lain yang terhubung dengan penyedia ini!",
|
||||
"error.duplicated_feed": "Umpan ini sudah ada.",
|
||||
"error.empty_file": "Berkas ini kosong.",
|
||||
"error.entries_per_page_invalid": "Jumlah entri per halaman tidak valid.",
|
||||
"error.feed_already_exists": "Umpan ini sudah ada.",
|
||||
"error.feed_category_not_found": "Kategori ini tidak ada atau tidak dipunyai oleh pengguna ini.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"error.feed_format_not_detected": "Tidak dapat mendeteksi format umpan: %v.",
|
||||
"error.feed_invalid_blocklist_rule": "Aturan blokir tidak valid.",
|
||||
"error.feed_invalid_keeplist_rule": "Aturan simpan tidak valid.",
|
||||
"error.feed_mandatory_fields": "Harus ada URL dan kategorinya.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.feed_not_found": "Umpan ini tidak ada atau tidak dipunyai oleh pengguna ini",
|
||||
"error.feed_title_not_empty": "Judul umpan tidak boleh kosong.",
|
||||
"error.feed_url_not_empty": "URL umpan tidak boleh kosong.",
|
||||
"error.fields_mandatory": "Semua bidang diharuskan.",
|
||||
"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_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_client_error": "HTTP client error: %v.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||
"error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).",
|
||||
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_bad_gateway": "Situs ini tidak tersedia saat ini karena kesalahan akses peladen situs. Masalah ini bukan pada sisi Miniflux. Coba lagi nanti.",
|
||||
"error.http_body_read": "Tidak dapat membaca badan HTTP: %v.",
|
||||
"error.http_client_error": "Galat klien HTTP: %v.",
|
||||
"error.http_empty_response": "Balasan HTTP kosong. Mungkin, situs ini menggunakan mekanisme perlindungan dari bot?",
|
||||
"error.http_empty_response_body": "Badan balasan HTTP kosong.",
|
||||
"error.http_forbidden": "Akses ke situs ini terlarang. Mungkin, situs ini menggunakan mekanisme perlindungan dari bot?",
|
||||
"error.http_gateway_timeout": "Situs ini tidak tersedia saat ini karena kesalahan akses jaringan peladen situs. Masalah ini bukan pada sisi Miniflux. Coba lagi nanti.",
|
||||
"error.http_internal_server_error": "Situs ini tidak tersedia saat ini karena galat peladen situs. Masalah ini bukan pada sisi Miniflux. Coba lagi nanti.",
|
||||
"error.http_not_authorized": "Akses ke situs ini tidak diizinkan. Mungkin nama pengguna atau kata sandinya salah.",
|
||||
"error.http_resource_not_found": "Sumber daya yang diminta tidak ditemukan. Periksa kembali URL-nya.",
|
||||
"error.http_response_too_large": "Balasan HTTP terlalu besar. Anda bisa menaikkan batas ukuran balasan HTTP di pengaturan global (membutuhkan pemulaian ulang peladen).",
|
||||
"error.http_service_unavailable": "Situs ini tidak tersedia saat ini dikarenakan galat internal peladen situs. Masalah ini bukan pada sisi Miniflux. Coba lagi nanti.",
|
||||
"error.http_too_many_requests": "Terlalu banyak koneksi dari Miniflux yang dibuat ke situs ini. Coba lagi nanti atau ubah konfigurasi aplikasi.",
|
||||
"error.http_unexpected_status_code": "Situs ini tidak dapat dijangkau saat ini dikarenakan kode status HTTP tak diduga: %d Masalah ini bukan pada sisi Miniflux. Coba lagi nanti.",
|
||||
"error.invalid_categories_sorting_order": "Urutan penyortiran kategori tidak valid.",
|
||||
"error.invalid_default_home_page": "Beranda baku tidak valid!",
|
||||
"error.invalid_display_mode": "Mode tampilan aplikasi web tidak valid.",
|
||||
"error.invalid_entry_direction": "Urutan entri tidak valid.",
|
||||
"error.invalid_entry_order": "Urutan entri tidak valid.",
|
||||
"error.invalid_feed_proxy_url": "URL proksi tidak valid.",
|
||||
"error.invalid_feed_url": "URL umpan tidak valid.",
|
||||
"error.invalid_gesture_nav": "Navigasi gestur tidak valid.",
|
||||
"error.invalid_language": "Bahasa tidak valid.",
|
||||
"error.invalid_site_url": "URL situs tidak valid.",
|
||||
"error.invalid_theme": "Tema tidak valid.",
|
||||
"error.invalid_timezone": "Zona waktu tidak valid.",
|
||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||
"error.network_operation": "Miniflux tidak dapat menjangkau situs ini dikarenakan galat jaringan: %v.",
|
||||
"error.network_timeout": "Situs ini terlalu lambat dan permintaan ke situs terlalu lama: %v",
|
||||
"error.password_min_length": "Kata sandi harus memiliki setidaknya 6 karakter.",
|
||||
"error.pocket_access_token": "Tidak bisa mendapatkan token akses dari Pocket!",
|
||||
"error.pocket_request_token": "Tidak bisa mendapatkan token permintaan dari Pocket!",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.proxy_url_not_empty": "URL proksi tidak boleh kosong.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Aturan blokir tidak valid: aturan #%d tidak mempunyai nama bidang yang valid (Opsi: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Aturan blokir tidak valid: aturan pola #%d bukan ekspresi regular (regex) yang valid",
|
||||
"error.settings_block_rule_regex_required": "Aturan blokir tidak valid: aturan pola #%d tidak disediakan",
|
||||
"error.settings_block_rule_separator_required": "Aturan blokir tidak valid: aturan pola #%d diharuskan dipisah menggunakan '='",
|
||||
"error.settings_invalid_domain_list": "Daftar domain tidak valid. Mohon sediakan daftar domain yang dipisah spasi.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Aturan simpan tidak valid: aturan #%d tidak mempunyai nama bidang yang valid (Opsi: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Aturan simpan tidak valid: aturan pola #%d bukan ekspresi regular (regex) yang valid",
|
||||
"error.settings_keep_rule_regex_required": "Aturan simpan tidak valid: aturan pola #%d tidak disediakan",
|
||||
"error.settings_keep_rule_separator_required": "Aturan simpan tidak valid: aturan pola #%d diharuskan dipisah menggunakan '='",
|
||||
"error.settings_mandatory_fields": "Harus ada nama pengguna, tema, bahasa, dan zona waktu.",
|
||||
"error.settings_media_playback_rate_range": "Kecepatan pemutaran di luar jangkauan",
|
||||
"error.settings_reading_speed_is_positive": "Kecepatan membaca harus integer positif.",
|
||||
"error.site_url_not_empty": "URL situs tidak boleh kosong.",
|
||||
"error.subscription_not_found": "Tidak bisa mencari langganan apa pun.",
|
||||
"error.title_required": "Judul diharuskan.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.title_required": "Judul harus ada.",
|
||||
"error.tls_error": "Galat TLS: %q. Anda bisa mematikan verifikasi TLS di pengaturan umpan jika Anda mau.",
|
||||
"error.unable_to_create_api_key": "Tidak bisa membuat kunci API ini.",
|
||||
"error.unable_to_create_category": "Tidak bisa membuat kategori ini.",
|
||||
"error.unable_to_create_user": "Tidak bisa membuat pengguna tersebut.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.unable_to_detect_rssbridge": "Tidak dapat mendeteksi umpan menggunakan RSS-Bridge: %v.",
|
||||
"error.unable_to_parse_feed": "Tidak dapat membaca umpan: %v.",
|
||||
"error.unable_to_update_category": "Tidak bisa memperbarui kategori ini.",
|
||||
"error.unable_to_update_feed": "Tidak bisa memperbarui umpan ini.",
|
||||
"error.unable_to_update_user": "Tidak bisa memperbarui pengguna tersebut.",
|
||||
@@ -156,61 +160,64 @@
|
||||
"form.api_key.label.description": "Label Kunci API",
|
||||
"form.category.hide_globally": "Sembunyikan entri di daftar belum dibaca global",
|
||||
"form.category.label.title": "Judul",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.integration": "Third-Party Services",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.general": "Umum",
|
||||
"form.feed.fieldset.integration": "Pengaturan Pihak Ketiga",
|
||||
"form.feed.fieldset.network_settings": "Pengaturan Jaringan",
|
||||
"form.feed.fieldset.rules": "Aturan",
|
||||
"form.feed.label.allow_self_signed_certificates": "Perbolehkan sertifikat web tidak valid atau sertifikasi sendiri",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.blocklist_rules": "Aturan Blokir",
|
||||
"form.feed.label.apprise_service_urls": "Daftar yang dipisahkan koma untuk URL layanan Apprise",
|
||||
"form.feed.label.block_filter_entry_rules": "Aturan Pemblokiran Entri",
|
||||
"form.feed.label.blocklist_rules": "Filter Pemblokiran Berbasis Regex",
|
||||
"form.feed.label.category": "Kategori",
|
||||
"form.feed.label.cookie": "Atur Kuki",
|
||||
"form.feed.label.crawler": "Ambil konten asli",
|
||||
"form.feed.label.description": "Deskripsi",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.disable_http2": "Matikan HTTP/2 untuk menghindari pelacakan",
|
||||
"form.feed.label.disabled": "Jangan perbarui umpan ini",
|
||||
"form.feed.label.feed_password": "Kata Sandi Umpan",
|
||||
"form.feed.label.feed_url": "URL Umpan",
|
||||
"form.feed.label.feed_username": "Nama Pengguna Umpan",
|
||||
"form.feed.label.fetch_via_proxy": "Ambil via Proksi",
|
||||
"form.feed.label.fetch_via_proxy": "Gunakan proksi yang dikonfigurasi di tingkat aplikasi",
|
||||
"form.feed.label.hide_globally": "Sembunyikan entri di daftar belum dibaca global",
|
||||
"form.feed.label.ignore_http_cache": "Abaikan Tembolok HTTP",
|
||||
"form.feed.label.keeplist_rules": "Aturan Simpan",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "Aturan Tulis Ulang",
|
||||
"form.feed.label.keep_filter_entry_rules": "Aturan Izin Entri",
|
||||
"form.feed.label.keeplist_rules": "Filter Simpan Berbasis Regex",
|
||||
"form.feed.label.no_media_player": "Tidak ada pemutar media (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Kirim artikel ke ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Prioritas baku Ntfy",
|
||||
"form.feed.label.ntfy_high_priority": "Priroritas tinggi Ntfy",
|
||||
"form.feed.label.ntfy_low_priority": "Prioritas rendah Ntfy",
|
||||
"form.feed.label.ntfy_max_priority": "Prioritas maksimal Ntfy",
|
||||
"form.feed.label.ntfy_min_priority": "Prioritas minimal Ntfy",
|
||||
"form.feed.label.ntfy_priority": "Prioritas Ntfy",
|
||||
"form.feed.label.ntfy_topic": "Topik Ntfy (opsional)",
|
||||
"form.feed.label.proxy_url": "URL Proksi",
|
||||
"form.feed.label.pushover_activate": "Kirim artikel ke pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Prioritas baku Pushover",
|
||||
"form.feed.label.pushover_high_priority": "Prioritas tinggi Pushover",
|
||||
"form.feed.label.pushover_low_priority": "Prioritas rendah Pushover",
|
||||
"form.feed.label.pushover_max_priority": "Prioritas maksimal Pushover",
|
||||
"form.feed.label.pushover_min_priority": "Prioritas minimal Pushover",
|
||||
"form.feed.label.pushover_priority": "Prioritas pesan Pushover",
|
||||
"form.feed.label.rewrite_rules": "Aturan Penulisan Ulang Konten",
|
||||
"form.feed.label.scraper_rules": "Aturan Pengambil Data",
|
||||
"form.feed.label.site_url": "URL Situs",
|
||||
"form.feed.label.title": "Judul",
|
||||
"form.feed.label.urlrewrite_rules": "Aturan Tulis Ulang URL",
|
||||
"form.feed.label.user_agent": "Timpa User Agent Baku",
|
||||
"form.feed.label.webhook_url": "Override webhook url",
|
||||
"form.feed.label.webhook_url": "Timpa URL Webhook",
|
||||
"form.import.label.file": "Berkas OPML",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||
"form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.integration.discord_activate": "Push entries to Discord",
|
||||
"form.integration.discord_webhook_link": "Discord Webhook link",
|
||||
"form.integration.apprise_activate": "Kirim artikel ke Apprise",
|
||||
"form.integration.apprise_services_url": "Daftar yang dipisahkan koma untuk URL layanan Apprise",
|
||||
"form.integration.apprise_url": "URL API Apprise",
|
||||
"form.integration.betula_activate": "Simpan artikel ke Betula",
|
||||
"form.integration.betula_token": "Token Betula",
|
||||
"form.integration.betula_url": "URL Peladen Betula",
|
||||
"form.integration.cubox_activate": "Simpan artikel ke Cubox",
|
||||
"form.integration.cubox_api_link": "Tautan API Cubox",
|
||||
"form.integration.discord_activate": "Kirim artikel ke Discord",
|
||||
"form.integration.discord_webhook_link": "Tautan Webhook Discord",
|
||||
"form.integration.espial_activate": "Simpan artikel ke Espial",
|
||||
"form.integration.espial_api_key": "Kunci API Espial",
|
||||
"form.integration.espial_endpoint": "Titik URL API Espial",
|
||||
@@ -226,36 +233,39 @@
|
||||
"form.integration.instapaper_activate": "Simpan artikel ke Instapaper",
|
||||
"form.integration.instapaper_password": "Kata Sandi Instapaper",
|
||||
"form.integration.instapaper_username": "Nama Pengguna Instapaper",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_is_private": "Mark link as private",
|
||||
"form.integration.linkace_tags": "LinkAce Tags",
|
||||
"form.integration.karakeep_activate": "Simpan artikel ke Karakeep",
|
||||
"form.integration.karakeep_api_key": "Kunci API Karakeep",
|
||||
"form.integration.karakeep_url": "Titik URL API Karakeep",
|
||||
"form.integration.linkace_activate": "Simpan artikel ke LinkAce",
|
||||
"form.integration.linkace_api_key": "Kunci API LinkAce",
|
||||
"form.integration.linkace_check_disabled": "Matikan pemeriksaan tautan",
|
||||
"form.integration.linkace_endpoint": "Titik URL API LinkAce",
|
||||
"form.integration.linkace_is_private": "Tandai tautan sebagai pribadi",
|
||||
"form.integration.linkace_tags": "Tanda LinkAce",
|
||||
"form.integration.linkding_activate": "Simpan artikel ke Linkding",
|
||||
"form.integration.linkding_api_key": "Kunci API Linkding",
|
||||
"form.integration.linkding_bookmark": "Tandai markah sebagai belum dibaca",
|
||||
"form.integration.linkding_endpoint": "Titik URL API Linkding",
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkding_tags": "Tanda Linkding",
|
||||
"form.integration.linkwarden_activate": "Simpan artikel ke Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Kunci API Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Titik URL API Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "URL Dasar Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Kirim entri baru ke Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID Ruang Matrix",
|
||||
"form.integration.matrix_bot_password": "Kata Sandi Matrix",
|
||||
"form.integration.matrix_bot_url": "URL Peladen Matrix",
|
||||
"form.integration.matrix_bot_user": "Nama Pengguna Matrix",
|
||||
"form.integration.notion_activate": "Save entries to Notion",
|
||||
"form.integration.notion_page_id": "Notion Page ID",
|
||||
"form.integration.notion_token": "Notion Secret Token",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_topic": "Ntfy topic (default used if not set in feed)",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.notion_activate": "Simpan artikel ke Notion",
|
||||
"form.integration.notion_page_id": "ID Halaman Notion",
|
||||
"form.integration.notion_token": "Token Rahasia Notion",
|
||||
"form.integration.ntfy_activate": "Kirim artikel ke ntfy",
|
||||
"form.integration.ntfy_api_token": "Token API Ntfy (opsional)",
|
||||
"form.integration.ntfy_icon_url": "URL ikon Ntfy (opsional)",
|
||||
"form.integration.ntfy_internal_links": "Gunakan tautan internal ketika mengklik (opsional)",
|
||||
"form.integration.ntfy_password": "Kata sandi Ntfy (opsional)",
|
||||
"form.integration.ntfy_topic": "Topik Ntfy (yang akan digunakan jika tidak diatur di umpan)",
|
||||
"form.integration.ntfy_url": "URL Ntfy (opsional, bawaan ke ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Nama pengguna Ntfy (opsional)",
|
||||
"form.integration.nunux_keeper_activate": "Simpan artikel ke Nunux Keeper",
|
||||
"form.integration.nunux_keeper_api_key": "Kunci API Nunux Keeper",
|
||||
"form.integration.nunux_keeper_endpoint": "Titik URL API Nunux Keeper",
|
||||
@@ -266,45 +276,42 @@
|
||||
"form.integration.pinboard_bookmark": "Tandai markah sebagai belum dibaca",
|
||||
"form.integration.pinboard_tags": "Tanda di Pinboard",
|
||||
"form.integration.pinboard_token": "Token API Pinboard",
|
||||
"form.integration.pocket_access_token": "Token Akses Pocket",
|
||||
"form.integration.pocket_activate": "Simpan artikel ke Pocket",
|
||||
"form.integration.pocket_connect_link": "Hubungkan akun Pocket Anda",
|
||||
"form.integration.pocket_consumer_key": "Kunci Pelanggan Pocket",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
"form.integration.pushover_token": "Pushover application API token",
|
||||
"form.integration.pushover_user": "Pushover user key",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.pushover_activate": "Kirim artikel ke Pushover",
|
||||
"form.integration.pushover_device": "Perangkat Pushover (opsional)",
|
||||
"form.integration.pushover_prefix": "Prefiks URL Pushover (opsional)",
|
||||
"form.integration.pushover_token": "Token API aplikasi Pushover",
|
||||
"form.integration.pushover_user": "Kunci pengguna Pushover",
|
||||
"form.integration.raindrop_activate": "Simpan artikel ke Raindrop",
|
||||
"form.integration.raindrop_collection_id": "ID Koleksi",
|
||||
"form.integration.raindrop_tags": "Tanda (dipisahkan koma)",
|
||||
"form.integration.raindrop_token": "Token (Tes)",
|
||||
"form.integration.readeck_activate": "Simpan artikel ke Readeck",
|
||||
"form.integration.readeck_api_key": "Kunci API Readeck",
|
||||
"form.integration.readeck_endpoint": "Titik URL API Readeck",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_labels": "Tagar Readeck",
|
||||
"form.integration.readeck_only_url": "Kirim hanya URL (alih-alih konten penuh)",
|
||||
"form.integration.readwise_activate": "Save entries to Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shiori_activate": "Save articles to Shiori",
|
||||
"form.integration.shiori_endpoint": "Shiori API Endpoint",
|
||||
"form.integration.shiori_password": "Shiori Password",
|
||||
"form.integration.shiori_username": "Shiori Username",
|
||||
"form.integration.slack_activate": "Push entries to Slack",
|
||||
"form.integration.slack_webhook_link": "Slack Webhook link",
|
||||
"form.integration.readwise_activate": "Simpan artikel ke Readwise",
|
||||
"form.integration.readwise_api_key": "Token Akses Readwise",
|
||||
"form.integration.readwise_api_key_link": "Dapatkan Token Akses Readwise Anda",
|
||||
"form.integration.rssbridge_activate": "Periksa RSS-Bridge ketika menambahkan langganan",
|
||||
"form.integration.rssbridge_token": "Token autentikasi RSS-Bridge",
|
||||
"form.integration.rssbridge_url": "URL peladen RSS-Bridge",
|
||||
"form.integration.shaarli_activate": "Simpan artikel ke Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Rahasia API Shaarli",
|
||||
"form.integration.shaarli_endpoint": "URL Shaarli",
|
||||
"form.integration.shiori_activate": "Simpan artikel ke Shiori",
|
||||
"form.integration.shiori_endpoint": "Titik URL API Shiori",
|
||||
"form.integration.shiori_password": "Kata Sandi Shiori",
|
||||
"form.integration.shiori_username": "Nama Pengguna Shiori",
|
||||
"form.integration.slack_activate": "Kirim artikel ke Slack",
|
||||
"form.integration.slack_webhook_link": "Tautan Webhook Slack",
|
||||
"form.integration.telegram_bot_activate": "Kirim artikel baru ke percakapan Telegram",
|
||||
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||
"form.integration.telegram_bot_disable_notification": "Disable notification",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
|
||||
"form.integration.telegram_bot_disable_buttons": "Matikan tombol",
|
||||
"form.integration.telegram_bot_disable_notification": "Matikan notifikasi",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Matikan tinjauan halaman web",
|
||||
"form.integration.telegram_bot_token": "Token Bot",
|
||||
"form.integration.telegram_chat_id": "ID Obrolan",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
"form.integration.telegram_topic_id": "ID Topik",
|
||||
"form.integration.wallabag_activate": "Simpan artikel ke Wallabag",
|
||||
"form.integration.wallabag_client_id": "ID Klien Wallabag",
|
||||
"form.integration.wallabag_client_secret": "Rahasia Klien Wallabag",
|
||||
@@ -312,14 +319,15 @@
|
||||
"form.integration.wallabag_only_url": "Kirim hanya URL (alih-alih konten penuh)",
|
||||
"form.integration.wallabag_password": "Kata Sandi Wallabag",
|
||||
"form.integration.wallabag_username": "Nama Pengguna Wallabag",
|
||||
"form.integration.webhook_activate": "Enable Webhooks",
|
||||
"form.integration.webhook_secret": "Webhooks Secret",
|
||||
"form.integration.webhook_url": "Default Webhook URL",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.integration.webhook_activate": "Aktifkan Webhook",
|
||||
"form.integration.webhook_secret": "Rahasia Webhook",
|
||||
"form.integration.webhook_url": "URL Webhook baku",
|
||||
"form.prefs.fieldset.application_settings": "Pengaturan Aplikasi",
|
||||
"form.prefs.fieldset.authentication_settings": "Pengaturan Autentikasi",
|
||||
"form.prefs.fieldset.global_feed_settings": "Pengaturan Umpan Global",
|
||||
"form.prefs.fieldset.reader_settings": "Pengaturan Pembaca",
|
||||
"form.prefs.help.external_font_hosts": "Daftar yang dipisah spasi untuk peladen penyedia fonta eksternal yang diperbolehkan. Seperti: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Baca artikel dengan membuka tautan eksternal",
|
||||
"form.prefs.label.categories_sorting_order": "Pengurutan Kategori",
|
||||
"form.prefs.label.cjk_reading_speed": "Kecepatan membaca untuk bahasa Tiongkok, Korea, dan Jepang (karakter per menit)",
|
||||
"form.prefs.label.custom_css": "Modifikasi CSS",
|
||||
@@ -331,15 +339,16 @@
|
||||
"form.prefs.label.entry_order": "Pengurutan Kolom Entri",
|
||||
"form.prefs.label.entry_sorting": "Pengurutan Entri",
|
||||
"form.prefs.label.entry_swipe": "Aktifkan tindakan geser pada entri di ponsel",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.label.external_font_hosts": "Peladen penyedia fonta eksternal",
|
||||
"form.prefs.label.gesture_nav": "Isyarat untuk menavigasi antar entri",
|
||||
"form.prefs.label.keyboard_shortcuts": "Aktifkan pintasan papan tik",
|
||||
"form.prefs.label.language": "Bahasa",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Tandai entri sebagai telah dibaca secara manual",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Tandai entri sebagai telah dibaca ketika audio/video sudah 90% didengar/ditonton",
|
||||
"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_view_or_media_completion": "Tandai entri sebagai telah dibaca ketika dilihat. Untuk audio/video, tandai sebagai telah dibaca ketika sudah 90% didengar/ditonton.",
|
||||
"form.prefs.label.media_playback_rate": "Kecepatan pemutaran audio/video",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Buka tautan eksternal di tab baru (menambahkan target=\"_blank\" ke tautan)",
|
||||
"form.prefs.label.show_reading_time": "Tampilkan perkiraan waktu baca untuk artikel",
|
||||
"form.prefs.label.theme": "Tema",
|
||||
"form.prefs.label.timezone": "Zona Waktu",
|
||||
@@ -376,7 +385,7 @@
|
||||
"menu.feeds": "Umpan",
|
||||
"menu.flush_history": "Hapus riwayat",
|
||||
"menu.history": "Riwayat",
|
||||
"menu.home_page": "Home page",
|
||||
"menu.home_page": "Beranda",
|
||||
"menu.import": "Impor",
|
||||
"menu.integrations": "Integrasi",
|
||||
"menu.logout": "Keluar",
|
||||
@@ -390,7 +399,7 @@
|
||||
"menu.settings": "Pengaturan",
|
||||
"menu.shared_entries": "Entri yang Dibagikan",
|
||||
"menu.show_all_entries": "Tampilkan semua entri",
|
||||
"menu.show_only_starred_entries": "Show only starred entries",
|
||||
"menu.show_only_starred_entries": "Tampilkan hanya entri yang dimarkahkan",
|
||||
"menu.show_only_unread_entries": "Tampilkan hanya entri yang belum dibaca",
|
||||
"menu.starred": "Markah",
|
||||
"menu.title": "Menu",
|
||||
@@ -400,6 +409,7 @@
|
||||
"page.about.build_date": "Tanggal Penyusunan:",
|
||||
"page.about.credits": "Pengembang",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.global_config_options": "Pengaturan Konfigurasi Global",
|
||||
"page.about.go_version": "Versi Go:",
|
||||
"page.about.license": "Lisensi:",
|
||||
@@ -427,7 +437,7 @@
|
||||
"page.categories.no_feed": "Tidak ada umpan.",
|
||||
"page.categories.title": "Kategori",
|
||||
"page.categories_count": [
|
||||
"%d category"
|
||||
"%d kategori"
|
||||
],
|
||||
"page.category_label": "Category: %s",
|
||||
"page.edit_category.title": "Sunting Kategori: %s",
|
||||
@@ -443,7 +453,7 @@
|
||||
"%d galat"
|
||||
],
|
||||
"page.feeds.last_check": "Terakhir diperiksa:",
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.next_check": "Akan diperiksa kembali:",
|
||||
"page.feeds.read_counter": "Jumlah entri yang telah dibaca",
|
||||
"page.feeds.title": "Umpan",
|
||||
"page.history.title": "Riwayat",
|
||||
@@ -494,12 +504,12 @@
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Buka/tutup lampiran entri",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "Ubah status baca, fokus ke selanjutnya",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Ubah status baca, fokus ke sebelumnya",
|
||||
"page.login.google_signin": "Masuk dengan Google",
|
||||
"page.login.oidc_signin": "Masuk dengan %s",
|
||||
"page.login.google_signin": "Masuk menggunakan Google",
|
||||
"page.login.oidc_signin": "Masuk menggunakan %s",
|
||||
"page.login.title": "Masuk",
|
||||
"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.login.webauthn_login": "Masuk menggunakan passkey",
|
||||
"page.login.webauthn_login.error": "Tidak dapat masuk menggunakan passkey",
|
||||
"page.login.webauthn_login.help": "Mohon untuk memasukkan nama pengguna Anda jika Anda menggunakan kunci keamanan. Tidak diperlukan jika anda menggunakan Passkey (kredensial dapat ditemukan).",
|
||||
"page.new_api_key.title": "Kunci API Baru",
|
||||
"page.new_category.title": "Kategori Baru",
|
||||
"page.new_user.title": "Pengguna Baru",
|
||||
@@ -507,7 +517,7 @@
|
||||
"page.offline.refresh_page": "Coba untuk memuat ulang halaman ini",
|
||||
"page.offline.title": "Mode Luring",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry"
|
||||
"%d entri dibaca"
|
||||
],
|
||||
"page.search.title": "Hasil Pencarian",
|
||||
"page.sessions.table.actions": "Tindakan",
|
||||
@@ -521,30 +531,30 @@
|
||||
"page.settings.title": "Pengaturan",
|
||||
"page.settings.unlink_google_account": "Putuskan akun Google saya",
|
||||
"page.settings.unlink_oidc_account": "Putuskan akun %s saya",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.added_on": "Added On",
|
||||
"page.settings.webauthn.actions": "Tindakan",
|
||||
"page.settings.webauthn.added_on": "Ditambahkan Pada",
|
||||
"page.settings.webauthn.delete": [
|
||||
"Remove %d passkey"
|
||||
"Hapus %d passkey"
|
||||
],
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.register": "Register passkey",
|
||||
"page.settings.webauthn.register.error": "Unable to register passkey",
|
||||
"page.settings.webauthn.last_seen_on": "Terakhir Digunakan",
|
||||
"page.settings.webauthn.passkey_name": "Nama Passkey",
|
||||
"page.settings.webauthn.passkeys": "Passkey",
|
||||
"page.settings.webauthn.register": "Daftar passkey",
|
||||
"page.settings.webauthn.register.error": "Tidak dapat mendaftarkan passkey",
|
||||
"page.shared_entries.title": "Entri yang Dibagikan",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry"
|
||||
"%d entri yang dibagikan"
|
||||
],
|
||||
"page.starred.title": "Markah",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry"
|
||||
"%d entri dimarkahi"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total"
|
||||
"%d entri secara total"
|
||||
],
|
||||
"page.unread.title": "Belum Dibaca",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry"
|
||||
"%d entri belum dibaca"
|
||||
],
|
||||
"page.users.actions": "Tindakan",
|
||||
"page.users.admin.no": "Tidak",
|
||||
@@ -554,15 +564,15 @@
|
||||
"page.users.never_logged": "Tidak Pernah",
|
||||
"page.users.title": "Pengguna",
|
||||
"page.users.username": "Nama Pengguna",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"pagination.first": "First",
|
||||
"pagination.last": "Last",
|
||||
"page.webauthn_rename.title": "Ubah Nama Passkey",
|
||||
"pagination.first": "Pertama",
|
||||
"pagination.last": "Terakhir",
|
||||
"pagination.next": "Berikutnya",
|
||||
"pagination.previous": "Sebelumnya",
|
||||
"search.label": "Cari",
|
||||
"search.placeholder": "Cari...",
|
||||
"search.submit": "Search",
|
||||
"skip_to_content": "Skip to content",
|
||||
"search.submit": "Cari",
|
||||
"skip_to_content": "Langsung ke konten",
|
||||
"time_elapsed.days": [
|
||||
"%d hari yang lalu"
|
||||
],
|
||||
@@ -586,4 +596,4 @@
|
||||
"time_elapsed.yesterday": "kemarin",
|
||||
"tooltip.keyboard_shortcuts": "Pintasan Papan Tik: %s",
|
||||
"tooltip.logged_user": "Masuk sebagai %s"
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"action.update": "Aggiorna",
|
||||
"alert.account_linked": "Il tuo account esterno ora è collegato!",
|
||||
"alert.account_unlinked": "Il tuo account esterno ora è scollegato!",
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"alert.background_feed_refresh": "Tutti i feed vengono aggiornati in background. Puoi continuare a usare Miniflux mentre questo processo è in esecuzione.",
|
||||
"alert.feed_error": "Sembra ci sia un problema con questo feed",
|
||||
"alert.no_bookmark": "Nessun preferito disponibile.",
|
||||
"alert.no_category": "Nessuna categoria disponibile.",
|
||||
@@ -27,28 +27,27 @@
|
||||
"alert.no_tag_entry": "Non ci sono voci corrispondenti a questo tag.",
|
||||
"alert.no_unread_entry": "Nessun articolo da leggere.",
|
||||
"alert.no_user": "Tu sei l'unico utente.",
|
||||
"alert.pocket_linked": "Il tuo account Pocket ora è collegato!",
|
||||
"alert.prefs_saved": "Preferenze salvate!",
|
||||
"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."
|
||||
"Hai richiesto troppi aggiornamenti dei feed. Attendi %d minuto prima di riprovare.",
|
||||
"Hai richiesto troppi aggiornamenti dei feed. Attendi %d minuti prima di riprovare."
|
||||
],
|
||||
"confirm.loading": "In corso...",
|
||||
"confirm.no": "no",
|
||||
"confirm.question": "Sei sicuro?",
|
||||
"confirm.question.refresh": "Vuoi forzare l'aggiornamento?",
|
||||
"confirm.yes": "sì",
|
||||
"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.reset": "Reset",
|
||||
"enclosure_media_controls.speed.reset.title": "Reset speed to 1x",
|
||||
"enclosure_media_controls.speed.slower": "Slower",
|
||||
"enclosure_media_controls.speed.slower.title": "Slower by %sx",
|
||||
"entry.bookmark.toast.off": "Non speciali",
|
||||
"entry.bookmark.toast.on": "Ha recitato",
|
||||
"enclosure_media_controls.seek": "Sposta:",
|
||||
"enclosure_media_controls.seek.title": "Sposta di %s secondi",
|
||||
"enclosure_media_controls.speed": "Velocità:",
|
||||
"enclosure_media_controls.speed.faster": "Più veloce",
|
||||
"enclosure_media_controls.speed.faster.title": "Più veloce di %sx",
|
||||
"enclosure_media_controls.speed.reset": "Reimposta",
|
||||
"enclosure_media_controls.speed.reset.title": "Reimposta velocità a 1x",
|
||||
"enclosure_media_controls.speed.slower": "Più lento",
|
||||
"enclosure_media_controls.speed.slower.title": "Più lento di %sx",
|
||||
"entry.bookmark.toast.off": "Non preferito",
|
||||
"entry.bookmark.toast.on": "Preferito",
|
||||
"entry.bookmark.toggle.off": "Rimuovi dai preferiti",
|
||||
"entry.bookmark.toggle.on": "Aggiungi ai preferiti",
|
||||
"entry.comments.label": "Commenti",
|
||||
@@ -72,40 +71,44 @@
|
||||
"entry.state.loading": "Caricamento in corso...",
|
||||
"entry.state.saving": "Salvataggio in corso...",
|
||||
"entry.status.mark_as_read": "Segna come letto",
|
||||
"entry.status.mark_as_unread": "Segna come non letto",
|
||||
"entry.status.title": "Cambia lo stato dell'articolo",
|
||||
"entry.status.toast.read": "Contrassegnato come letto",
|
||||
"entry.status.toast.unread": "Contrassegnato come non letto",
|
||||
"entry.status.mark_as_unread": "Segna come non letto",
|
||||
"entry.tags.label": "Tag:",
|
||||
"entry.unshare.label": "Unshare",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Mostra %d altro tag",
|
||||
"Mostra %d altri tag"
|
||||
],
|
||||
"entry.unshare.label": "Rimuovi condivisione",
|
||||
"error.api_key_already_exists": "Questa chiave API esiste già.",
|
||||
"error.bad_credentials": "Nome utente o password non validi.",
|
||||
"error.category_already_exists": "Questa categoria esiste già.",
|
||||
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||
"error.database_error": "Database error: %v.",
|
||||
"error.category_not_found": "Questa categoria non esiste o non appartiene a questo utente.",
|
||||
"error.database_error": "Errore del database: %v.",
|
||||
"error.different_passwords": "Le password non coincidono.",
|
||||
"error.duplicate_fever_username": "Esiste già un account Fever con lo stesso nome utente!",
|
||||
"error.duplicate_googlereader_username": "Esiste già un account Google Reader con lo stesso nome utente!",
|
||||
"error.duplicate_linked_account": "Esiste già un account configurato per questo servizio!",
|
||||
"error.duplicated_feed": "This feed already exists.",
|
||||
"error.duplicated_feed": "Questo feed esiste già.",
|
||||
"error.empty_file": "Questo file è vuoto.",
|
||||
"error.entries_per_page_invalid": "Il numero di articoli per pagina non è valido.",
|
||||
"error.feed_already_exists": "Questo feed esiste già.",
|
||||
"error.feed_category_not_found": "Questa categoria non esiste o non appartiene a questo utente.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"error.feed_format_not_detected": "Impossibile rilevare il formato del feed: %v.",
|
||||
"error.feed_invalid_blocklist_rule": "La regola dell'elenco di blocco non è valida.",
|
||||
"error.feed_invalid_keeplist_rule": "La regola dell'elenco di conservazione non è valida.",
|
||||
"error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.feed_not_found": "Questo feed non esiste o non appartiene a questo utente.",
|
||||
"error.feed_title_not_empty": "Il titolo del feed non può essere vuoto.",
|
||||
"error.feed_url_not_empty": "L'URL del feed non può essere vuoto.",
|
||||
"error.fields_mandatory": "Tutti i campi sono obbligatori.",
|
||||
"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_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_client_error": "HTTP client error: %v.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||
"error.http_bad_gateway": "Il sito web non è disponibile al momento a causa di un errore di gateway. Il problema non è dal lato di Miniflux. Per favore, riprova più tardi.",
|
||||
"error.http_body_read": "Impossibile leggere il corpo HTTP: %v.",
|
||||
"error.http_client_error": "Errore del client HTTP: %v.",
|
||||
"error.http_empty_response": "La risposta HTTP è vuota. Forse questo sito web utilizza un meccanismo di protezione dai bot?",
|
||||
"error.http_empty_response_body": "Il corpo della risposta HTTP è vuoto.",
|
||||
"error.http_forbidden": "L'accesso a questo sito web è vietato. Forse questo sito web ha un meccanismo di protezione dai bot?",
|
||||
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
@@ -114,20 +117,22 @@
|
||||
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.invalid_categories_sorting_order": "L'ordinamento delle categorie non è valido.",
|
||||
"error.invalid_default_home_page": "Pagina iniziale predefinita non valida!",
|
||||
"error.invalid_display_mode": "Modalità di visualizzazione web app non valida.",
|
||||
"error.invalid_entry_direction": "Ordinamento non valido.",
|
||||
"error.invalid_entry_order": "L'ordinamento delle voci non è valido.",
|
||||
"error.invalid_feed_proxy_url": "URL del proxy non valido.",
|
||||
"error.invalid_feed_url": "URL del feed non valido.",
|
||||
"error.invalid_gesture_nav": "Navigazione gestuale non valida.",
|
||||
"error.invalid_language": "Lingua non valida.",
|
||||
"error.invalid_site_url": "URL del sito non valido.",
|
||||
"error.invalid_theme": "Tema non valido.",
|
||||
"error.invalid_timezone": "Fuso orario non valido.",
|
||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||
"error.network_operation": "Miniflux non riesce a raggiungere questo sito web a causa di un errore di rete: %v.",
|
||||
"error.network_timeout": "Questo sito web è troppo lento e la richiesta è scaduta: %v",
|
||||
"error.password_min_length": "La password deve contenere almeno 6 caratteri.",
|
||||
"error.pocket_access_token": "Non sono riuscito ad ottenere l'access token da Pocket!",
|
||||
"error.pocket_request_token": "Non sono riuscito ad ottenere il request token da Pocket!",
|
||||
"error.proxy_url_not_empty": "L'URL del proxy non può essere vuoto.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
@@ -147,8 +152,8 @@
|
||||
"error.unable_to_create_api_key": "Impossibile creare questa chiave API.",
|
||||
"error.unable_to_create_category": "Non sono riuscito ad aggiungere questa categoria.",
|
||||
"error.unable_to_create_user": "Non sono riuscito ad aggiungere questo user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.unable_to_detect_rssbridge": "Impossibile rilevare il feed usando RSS-Bridge: %v.",
|
||||
"error.unable_to_parse_feed": "Impossibile analizzare questo feed: %v.",
|
||||
"error.unable_to_update_category": "Non sono riuscito ad aggiornare questa categoria.",
|
||||
"error.unable_to_update_feed": "Non sono riuscito ad aggiornare questo feed.",
|
||||
"error.unable_to_update_user": "Non sono riuscito ad aggiornare questo utente.",
|
||||
@@ -164,7 +169,8 @@
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.label.allow_self_signed_certificates": "Consenti certificati autofirmati o non validi",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.blocklist_rules": "Regole di blocco",
|
||||
"form.feed.label.block_filter_entry_rules": "Regole di Blocco delle Voci",
|
||||
"form.feed.label.blocklist_rules": "Filtri di Blocco Basati su Regex",
|
||||
"form.feed.label.category": "Categoria",
|
||||
"form.feed.label.cookie": "Installare i cookies",
|
||||
"form.feed.label.crawler": "Scarica il contenuto integrale",
|
||||
@@ -174,10 +180,11 @@
|
||||
"form.feed.label.feed_password": "Password del feed",
|
||||
"form.feed.label.feed_url": "URL del feed",
|
||||
"form.feed.label.feed_username": "Nome utente del feed",
|
||||
"form.feed.label.fetch_via_proxy": "Recuperare tramite proxy",
|
||||
"form.feed.label.fetch_via_proxy": "Usa il proxy configurato a livello di applicazione",
|
||||
"form.feed.label.hide_globally": "Nascondere le voci nella lista globale dei non letti",
|
||||
"form.feed.label.ignore_http_cache": "Ignora cache HTTP",
|
||||
"form.feed.label.keeplist_rules": "Regole di autorizzazione",
|
||||
"form.feed.label.keep_filter_entry_rules": "Regole di Permesso delle Voci",
|
||||
"form.feed.label.keeplist_rules": "Filtri di Mantenimento Basati su Regex",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
@@ -187,6 +194,7 @@
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
@@ -194,7 +202,7 @@
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "Regole di impaginazione del contenuto",
|
||||
"form.feed.label.rewrite_rules": "Regole di Riscrittura del Contenuto",
|
||||
"form.feed.label.scraper_rules": "Regole di estrazione del contenuto",
|
||||
"form.feed.label.site_url": "URL del sito",
|
||||
"form.feed.label.title": "Titolo",
|
||||
@@ -228,6 +236,9 @@
|
||||
"form.integration.instapaper_activate": "Salva gli articoli su Instapaper",
|
||||
"form.integration.instapaper_password": "Password dell'account Instapaper",
|
||||
"form.integration.instapaper_username": "Nome utente dell'account Instapaper",
|
||||
"form.integration.karakeep_activate": "Salva gli articoli su Karakeep",
|
||||
"form.integration.karakeep_api_key": "API key dell'account Karakeep",
|
||||
"form.integration.karakeep_url": "Endpoint dell'API di Karakeep",
|
||||
"form.integration.linkace_activate": "Salva gli articoli su LinkAce",
|
||||
"form.integration.linkace_api_key": "API key dell'account LinkAce",
|
||||
"form.integration.linkace_check_disabled": "Disabilita i controlli",
|
||||
@@ -241,7 +252,7 @@
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkwarden_activate": "Salva gli articoli su Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "API key dell'account Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Endpoint dell'API di Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "URL di base di Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Trasferimento di nuovi articoli a Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID della stanza Matrix",
|
||||
"form.integration.matrix_bot_password": "Password per l'utente Matrix",
|
||||
@@ -268,10 +279,6 @@
|
||||
"form.integration.pinboard_bookmark": "Segna i preferiti come non letti",
|
||||
"form.integration.pinboard_tags": "Tag di Pinboard",
|
||||
"form.integration.pinboard_token": "Token dell'API di Pinboard",
|
||||
"form.integration.pocket_access_token": "Access token dell'account Pocket",
|
||||
"form.integration.pocket_activate": "Salva gli articoli su Pocket",
|
||||
"form.integration.pocket_connect_link": "Collega il tuo account Pocket",
|
||||
"form.integration.pocket_consumer_key": "Consumer key dell'account Pocket",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
@@ -290,6 +297,7 @@
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
@@ -322,6 +330,7 @@
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Read articles by opening external links",
|
||||
"form.prefs.label.categories_sorting_order": "Ordinamento delle categorie",
|
||||
"form.prefs.label.cjk_reading_speed": "Velocità di lettura per cinese, coreano e giapponese (caratteri al minuto)",
|
||||
"form.prefs.label.custom_css": "CSS personalizzati",
|
||||
@@ -342,6 +351,7 @@
|
||||
"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.media_playback_rate": "Velocità di riproduzione dell'audio/video",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Apri i link esterni in una nuova scheda (aggiunge target=\"_blank\" ai link)",
|
||||
"form.prefs.label.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli",
|
||||
"form.prefs.label.theme": "Tema",
|
||||
"form.prefs.label.timezone": "Fuso orario",
|
||||
@@ -402,6 +412,7 @@
|
||||
"page.about.build_date": "Data della build:",
|
||||
"page.about.credits": "Crediti",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.global_config_options": "Opzioni di configurazione globali",
|
||||
"page.about.go_version": "Go versione:",
|
||||
"page.about.license": "Licenza:",
|
||||
@@ -566,14 +577,14 @@
|
||||
"page.users.title": "Utenti",
|
||||
"page.users.username": "Nome utente",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"pagination.first": "First",
|
||||
"pagination.last": "Last",
|
||||
"pagination.first": "Primo",
|
||||
"pagination.last": "Ultimo",
|
||||
"pagination.next": "Successivo",
|
||||
"pagination.previous": "Precedente",
|
||||
"search.label": "Cerca",
|
||||
"search.placeholder": "Cerca...",
|
||||
"search.submit": "Search",
|
||||
"skip_to_content": "Skip to content",
|
||||
"search.submit": "Cerca",
|
||||
"skip_to_content": "Salta al contenuto",
|
||||
"time_elapsed.days": [
|
||||
"%d giorno fa",
|
||||
"%d giorni fa"
|
||||
@@ -603,4 +614,4 @@
|
||||
"time_elapsed.yesterday": "ieri",
|
||||
"tooltip.keyboard_shortcuts": "Scorciatoia da tastiera: %s",
|
||||
"tooltip.logged_user": "Autenticato come %s"
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"action.update": "更新",
|
||||
"alert.account_linked": "外部アカウントとリンクされました!",
|
||||
"alert.account_unlinked": "外部アカウントとのリンクが解除されました!",
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"alert.background_feed_refresh": "すべてのフィードがバックグラウンドで更新されています。この処理中も Miniflux を使い続けることができます。",
|
||||
"alert.feed_error": "このフィードには問題があります。",
|
||||
"alert.no_bookmark": "現在星付きはありません。",
|
||||
"alert.no_category": "カテゴリが存在しません。",
|
||||
@@ -27,25 +27,24 @@
|
||||
"alert.no_tag_entry": "このタグに一致するエントリーはありません。",
|
||||
"alert.no_unread_entry": "未読の記事はありません。",
|
||||
"alert.no_user": "あなたが唯一のユーザーです。",
|
||||
"alert.pocket_linked": "Pocket アカウントとリンクされました!",
|
||||
"alert.prefs_saved": "設定情報は保存されました!",
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again."
|
||||
"フィードの更新を要求しすぎました。%d 分後に再度お試しください。"
|
||||
],
|
||||
"confirm.loading": "実行中…",
|
||||
"confirm.no": "いいえ",
|
||||
"confirm.question": "よろしいですか?",
|
||||
"confirm.question.refresh": "強制的に更新しますか?",
|
||||
"confirm.yes": "はい",
|
||||
"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.reset": "Reset",
|
||||
"enclosure_media_controls.speed.reset.title": "Reset speed to 1x",
|
||||
"enclosure_media_controls.speed.slower": "Slower",
|
||||
"enclosure_media_controls.speed.slower.title": "Slower by %sx",
|
||||
"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.reset": "リセット",
|
||||
"enclosure_media_controls.speed.reset.title": "速度を1xにリセット",
|
||||
"enclosure_media_controls.speed.slower": "遅く",
|
||||
"enclosure_media_controls.speed.slower.title": "%sx 遅く",
|
||||
"entry.bookmark.toast.off": "星を外しました",
|
||||
"entry.bookmark.toast.on": "星を付けました",
|
||||
"entry.bookmark.toggle.off": "星を外す",
|
||||
@@ -70,40 +69,43 @@
|
||||
"entry.state.loading": "読み込み中…",
|
||||
"entry.state.saving": "保存中…",
|
||||
"entry.status.mark_as_read": "既読にする",
|
||||
"entry.status.mark_as_unread": "未読に戻す",
|
||||
"entry.status.title": "記事の状態を変更",
|
||||
"entry.status.toast.read": "既読にしました",
|
||||
"entry.status.toast.unread": "未読にしました",
|
||||
"entry.status.mark_as_unread": "未読に戻す",
|
||||
"entry.tags.label": "タグ:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"%d 個のタグ"
|
||||
],
|
||||
"entry.unshare.label": "共有を解除",
|
||||
"error.api_key_already_exists": "この API キーは既に存在します。",
|
||||
"error.bad_credentials": "ユーザー名かパスワードが間違っています。",
|
||||
"error.category_already_exists": "このカテゴリは既に存在します。",
|
||||
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||
"error.database_error": "Database error: %v.",
|
||||
"error.category_not_found": "このカテゴリは存在しないか、このユーザーに属していません。",
|
||||
"error.database_error": "データベースエラー: %v。",
|
||||
"error.different_passwords": "パスワードが一致しません。",
|
||||
"error.duplicate_fever_username": "既に同じ名前の Fever ユーザー名が使われています!",
|
||||
"error.duplicate_googlereader_username": "既に同じ名前の Google Reader ユーザー名が使われています!",
|
||||
"error.duplicate_linked_account": "別なユーザーが既にこのサービスの同じユーザーとリンクしています。",
|
||||
"error.duplicated_feed": "This feed already exists.",
|
||||
"error.duplicated_feed": "このフィードは既に存在します。",
|
||||
"error.empty_file": "このファイルは空です。",
|
||||
"error.entries_per_page_invalid": "ページあたりの記事数が無効です。",
|
||||
"error.feed_already_exists": "このフィードは既に存在します。",
|
||||
"error.feed_category_not_found": "このカテゴリは存在しないか、このユーザーに属していません。",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"error.feed_format_not_detected": "フィードの形式を検出できません: %v.",
|
||||
"error.feed_invalid_blocklist_rule": "ブロックリストルールが無効です。",
|
||||
"error.feed_invalid_keeplist_rule": "リストの保持ルールが無効です。",
|
||||
"error.feed_mandatory_fields": "URL と カテゴリが必要です。",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.feed_not_found": "このフィードは存在しないか、このユーザーに属していません。",
|
||||
"error.feed_title_not_empty": "フィードのタイトルを空にすることはできません。",
|
||||
"error.feed_url_not_empty": "フィード URL を空にすることはできません。",
|
||||
"error.fields_mandatory": "すべての項目が必要です。",
|
||||
"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_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_client_error": "HTTP client error: %v.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||
"error.http_bad_gateway": "ウェブサイトは、不正なゲートウェイエラーのため現在利用できません。問題はMiniflux側にはありません。後でもう一度お試しください。",
|
||||
"error.http_body_read": "HTTP本文を読み取れません: %v。",
|
||||
"error.http_client_error": "HTTPクライアントエラー: %v。",
|
||||
"error.http_empty_response": "HTTP応答が空です。おそらく、このウェブサイトはボット保護メカニズムを使用していますか?",
|
||||
"error.http_empty_response_body": "HTTP応答本文が空です。",
|
||||
"error.http_forbidden": "このウェブサイトへのアクセスは禁止されています。おそらく、このウェブサイトはボット保護メカニズムを持っていますか?",
|
||||
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
@@ -112,20 +114,22 @@
|
||||
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.invalid_categories_sorting_order": "カテゴリの表示順が無効です。",
|
||||
"error.invalid_default_home_page": "デフォルトのトップページが無効です",
|
||||
"error.invalid_display_mode": "Web アプリの表示モードが無効です。",
|
||||
"error.invalid_entry_direction": "記事の表示順が無効です。",
|
||||
"error.invalid_entry_order": "記事の表示順が無効です。",
|
||||
"error.invalid_feed_proxy_url": "プロキシURLが無効です。",
|
||||
"error.invalid_feed_url": "フィード URL が無効です。",
|
||||
"error.invalid_gesture_nav": "ジェスチャー ナビゲーションが無効です。",
|
||||
"error.invalid_language": "言語が無効です。",
|
||||
"error.invalid_site_url": "サイト URL が無効です。",
|
||||
"error.invalid_theme": "テーマが無効です。",
|
||||
"error.invalid_timezone": "タイムゾーンが無効です。",
|
||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||
"error.network_operation": "Miniflux はネットワークエラーのためこのウェブサイトに到達できません: %v.",
|
||||
"error.network_timeout": "このウェブサイトは応答が遅すぎるためタイムアウトしました: %v",
|
||||
"error.password_min_length": "パスワードは6文字以上である必要があります。",
|
||||
"error.pocket_access_token": "Pocket の access token が取得できません!",
|
||||
"error.pocket_request_token": "Pocket の request token が取得できません!",
|
||||
"error.proxy_url_not_empty": "プロキシURLを空にすることはできません。",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
@@ -145,8 +149,8 @@
|
||||
"error.unable_to_create_api_key": "この API キーを作成できません。",
|
||||
"error.unable_to_create_category": "このカテゴリは作成できません。",
|
||||
"error.unable_to_create_user": "このユーザーは作成できません。",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.unable_to_detect_rssbridge": "RSS-Bridge を使ってフィードを検出できません: %v.",
|
||||
"error.unable_to_parse_feed": "このフィードを解析できません: %v.",
|
||||
"error.unable_to_update_category": "このカテゴリは更新できません。",
|
||||
"error.unable_to_update_feed": "このフィードは更新できません。",
|
||||
"error.unable_to_update_user": "このユーザーは更新できません。",
|
||||
@@ -162,7 +166,8 @@
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.label.allow_self_signed_certificates": "自己署名証明書または無効な証明書を許可する",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.blocklist_rules": "Block ルール",
|
||||
"form.feed.label.block_filter_entry_rules": "エントリブロッキングルール",
|
||||
"form.feed.label.blocklist_rules": "正規表現ベースのブロッキングフィルター",
|
||||
"form.feed.label.category": "カテゴリ",
|
||||
"form.feed.label.cookie": "Cookie の設定",
|
||||
"form.feed.label.crawler": "オリジナルの内容を取得",
|
||||
@@ -172,10 +177,11 @@
|
||||
"form.feed.label.feed_password": "フィードのパスワード",
|
||||
"form.feed.label.feed_url": "フィード URL",
|
||||
"form.feed.label.feed_username": "フィードのユーザー名",
|
||||
"form.feed.label.fetch_via_proxy": "プロキシ経由で取得",
|
||||
"form.feed.label.fetch_via_proxy": "アプリケーションレベルで設定されたプロキシを使用する",
|
||||
"form.feed.label.hide_globally": "未読一覧に記事を表示しない",
|
||||
"form.feed.label.ignore_http_cache": "HTTPキャッシュを無視",
|
||||
"form.feed.label.keeplist_rules": "Keep ルール",
|
||||
"form.feed.label.keep_filter_entry_rules": "エントリ許可ルール",
|
||||
"form.feed.label.keeplist_rules": "正規表現ベースのキープフィルター",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
@@ -185,6 +191,7 @@
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
@@ -192,7 +199,7 @@
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "Rewrite ルール",
|
||||
"form.feed.label.rewrite_rules": "コンテンツ書き換えルール",
|
||||
"form.feed.label.scraper_rules": "Scraper ルール",
|
||||
"form.feed.label.site_url": "サイト URL",
|
||||
"form.feed.label.title": "タイトル",
|
||||
@@ -226,6 +233,9 @@
|
||||
"form.integration.instapaper_activate": "Instapaper に記事を保存する",
|
||||
"form.integration.instapaper_password": "Instapaper のパスワード",
|
||||
"form.integration.instapaper_username": "Instapaper のユーザー名",
|
||||
"form.integration.karakeep_activate": "Karakeep に記事を保存する",
|
||||
"form.integration.karakeep_api_key": "Karakeep の API key",
|
||||
"form.integration.karakeep_url": "Karakeep の API Endpoint",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
@@ -239,7 +249,7 @@
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkwarden_activate": "Linkwarden に記事を保存する",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden の API key",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden の API Endpoint",
|
||||
"form.integration.linkwarden_endpoint": "リンクワーデン ベース URL",
|
||||
"form.integration.matrix_bot_activate": "新しい記事をMatrixに転送する",
|
||||
"form.integration.matrix_bot_chat_id": "MatrixルームのID",
|
||||
"form.integration.matrix_bot_password": "Matrixユーザ用パスワード",
|
||||
@@ -266,10 +276,6 @@
|
||||
"form.integration.pinboard_bookmark": "ブックマークを未読にする",
|
||||
"form.integration.pinboard_tags": "Pinboard の Tag",
|
||||
"form.integration.pinboard_token": "Pinboard の API Token",
|
||||
"form.integration.pocket_access_token": "Pocket の Access Token",
|
||||
"form.integration.pocket_activate": "Pocket に記事を保存する",
|
||||
"form.integration.pocket_connect_link": "Pocket account に接続",
|
||||
"form.integration.pocket_consumer_key": "Pocket の Consumer Key",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
@@ -288,6 +294,7 @@
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
@@ -320,6 +327,7 @@
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Read articles by opening external links",
|
||||
"form.prefs.label.categories_sorting_order": "カテゴリの表示順",
|
||||
"form.prefs.label.cjk_reading_speed": "中国語、韓国語、日本語の読書速度(文字数/分)",
|
||||
"form.prefs.label.custom_css": "カスタム CSS",
|
||||
@@ -340,6 +348,7 @@
|
||||
"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.media_playback_rate": "オーディオ/ビデオの再生速度",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "外部リンクを新しいタブで開く(リンクに target=\"_blank\" を追加)",
|
||||
"form.prefs.label.show_reading_time": "記事の推定読書時間を表示する",
|
||||
"form.prefs.label.theme": "テーマ",
|
||||
"form.prefs.label.timezone": "タイムゾーン",
|
||||
@@ -400,6 +409,7 @@
|
||||
"page.about.build_date": "ビルド日時:",
|
||||
"page.about.credits": "著作権表示",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.global_config_options": "グローバル構成オプション",
|
||||
"page.about.go_version": "Go バージョン:",
|
||||
"page.about.license": "ライセンス:",
|
||||
@@ -427,7 +437,7 @@
|
||||
"page.categories.no_feed": "フィードはありません。",
|
||||
"page.categories.title": "カテゴリ",
|
||||
"page.categories_count": [
|
||||
"%d category"
|
||||
"%d 件のカテゴリ"
|
||||
],
|
||||
"page.category_label": "Category: %s",
|
||||
"page.edit_category.title": "カテゴリを編集: %s",
|
||||
@@ -507,7 +517,7 @@
|
||||
"page.offline.refresh_page": "ページを更新してみてください",
|
||||
"page.offline.title": "オフラインモード",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry"
|
||||
"%d 件の既読エントリ"
|
||||
],
|
||||
"page.search.title": "検索結果",
|
||||
"page.sessions.table.actions": "アクション",
|
||||
@@ -533,18 +543,18 @@
|
||||
"page.settings.webauthn.register.error": "パスキーを登録できません",
|
||||
"page.shared_entries.title": "共有エントリ",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry"
|
||||
"%d 件の共有エントリ"
|
||||
],
|
||||
"page.starred.title": "星付き",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry"
|
||||
"%d 件の星付きエントリ"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total"
|
||||
"合計 %d 件のエントリ"
|
||||
],
|
||||
"page.unread.title": "未読",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry"
|
||||
"%d 件の未読エントリ"
|
||||
],
|
||||
"page.users.actions": "アクション",
|
||||
"page.users.admin.no": "非管理者",
|
||||
@@ -555,14 +565,14 @@
|
||||
"page.users.title": "ユーザー一覧",
|
||||
"page.users.username": "ユーザー名",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"pagination.first": "First",
|
||||
"pagination.last": "Last",
|
||||
"pagination.first": "最初",
|
||||
"pagination.last": "最後",
|
||||
"pagination.next": "次",
|
||||
"pagination.previous": "前",
|
||||
"search.label": "検索",
|
||||
"search.placeholder": "…を検索",
|
||||
"search.submit": "Search",
|
||||
"skip_to_content": "Skip to content",
|
||||
"search.submit": "検索",
|
||||
"skip_to_content": "コンテンツへスキップ",
|
||||
"time_elapsed.days": [
|
||||
"%d 日前"
|
||||
],
|
||||
@@ -586,4 +596,4 @@
|
||||
"time_elapsed.yesterday": "昨日",
|
||||
"tooltip.keyboard_shortcuts": "キーボードショートカット: %s",
|
||||
"tooltip.logged_user": "%s としてログイン中"
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@
|
||||
"alert.no_tag_entry": "Bô kah chit ê khan-á ū hû-ha̍p ê siau-sit",
|
||||
"alert.no_unread_entry": "Chit-má ah-bô tha̍k kè ê siau-sit",
|
||||
"alert.no_user": "Lí sī ûi-it ê sú-iōng-lâng",
|
||||
"alert.pocket_linked": "Í-keng kā lí ê Pocket kháu-chō kiat chòe-hé--ah!",
|
||||
"alert.prefs_saved": "Siat-tēng í-keng pó-chûn--ah!",
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"Lí í-keng ín-khí siuⁿ chōe pái siau-sit lâi-goân ōaⁿ-sin, chhiáⁿ tán-hāu %d hun-cheng āu koh chhì-khòaⁿ-māi."
|
||||
@@ -37,8 +36,8 @@
|
||||
"confirm.question": "Kám ū khak-tēng?",
|
||||
"confirm.question.refresh": "Kám beh kiông-chè têng lia̍h?",
|
||||
"confirm.yes": "Sī",
|
||||
"enclosure_media_controls.seek": "sóa-ūi:",
|
||||
"enclosure_media_controls.seek.title": "sóa %s bió",
|
||||
"enclosure_media_controls.seek": "Sóa-ūi:",
|
||||
"enclosure_media_controls.seek.title": "Sóa %s bió",
|
||||
"enclosure_media_controls.speed": "Sok-tō͘",
|
||||
"enclosure_media_controls.speed.faster": "Cheng-ka sok-tō͘",
|
||||
"enclosure_media_controls.speed.faster.title": "Cheng-ka sok-tō͘ %sx",
|
||||
@@ -70,11 +69,14 @@
|
||||
"entry.state.loading": "Tng leh chip-hêng…",
|
||||
"entry.state.saving": "Tng leh pó-chûn…",
|
||||
"entry.status.mark_as_read": "Chù chòe tha̍k kè",
|
||||
"entry.status.mark_as_unread": "Chù chòe ah-bōe tha̍k",
|
||||
"entry.status.title": "Kái chōng-thài",
|
||||
"entry.status.toast.read": "Chù chòe tha̍k kè chòe soah",
|
||||
"entry.status.toast.unread": "Chù chòe ah-bōe tha̍k chòe soah",
|
||||
"entry.status.mark_as_unread": "Chù chòe ah-bōe tha̍k",
|
||||
"entry.tags.label": "Khan-á:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Kah %d khan-á"
|
||||
],
|
||||
"entry.unshare.label": "Chhú-siau hun-hióng",
|
||||
"error.api_key_already_exists": "Chit ê API só-sî í-keng chûn-chāi",
|
||||
"error.bad_credentials": "M̄-tio̍h ê kháu-chō miâ ah-sī bi̍t-bé.",
|
||||
@@ -112,9 +114,12 @@
|
||||
"error.http_service_unavailable": "Chit ê bāng-chām in-ūi in ka-kī lāi-pō͘ ū būn-tôe,m̄ sī Miniflux chia ê būn-tôe, chhiáⁿ tán--chi̍t-ē chiah koh chhì-khòaⁿ-māi.",
|
||||
"error.http_too_many_requests": "Miniflux tùi chit ê bāng-chām ê chhéng-kiû siuⁿ kè chōe, chhiáⁿ têng chhì-khòaⁿ-māi ah-sī tiâu-chéng thêng-sek siat-tēng.",
|
||||
"error.http_unexpected_status_code": "Chit ê bāng-chām chòe liáu chi̍t ê liāu-bōe-tio̍h ê HTTP chōng-thài bé: %d, chhiáⁿ tán--chi̍t-ē chiah koh chhì-khòaⁿ-māi.",
|
||||
"error.invalid_categories_sorting_order": "Lūi-pia̍t ê chōe pái bô-hāu, chhiáⁿ tán-hāu %d hun-cheng āu koh chhì-khòaⁿ-māi.",
|
||||
"error.invalid_default_home_page": "Ū-siat chú-ia̍h ū būn-tôe!",
|
||||
"error.invalid_display_mode": "Ū būn-tôe ê su-li̍p bô͘-sek.",
|
||||
"error.invalid_entry_direction": "Ū būn-tôe ê su-li̍p hong-hiòng.",
|
||||
"error.invalid_entry_order": "Siau-sit ê chōe pái bô-hāu, chhiáⁿ tán-hāu %d hun-cheng āu koh chhì-khòaⁿ-māi.",
|
||||
"error.invalid_feed_proxy_url": "Proxy URL ū būn-tôe.",
|
||||
"error.invalid_feed_url": "Beh tēng ê siau-sit lâi-goân ê bāng-chí ū būn-tôe.",
|
||||
"error.invalid_gesture_nav": "Chhiú-sè tō-lám ū būn-tôe.",
|
||||
"error.invalid_language": "Ū būn-tôe ê gú-giân.",
|
||||
@@ -124,8 +129,7 @@
|
||||
"error.network_operation": "Miniflux bô-hoat-tō͘ liân kàu chit ê bāng-chām, ū khó-lêng sī bāng-lō͘ būn-tôe: %v.",
|
||||
"error.network_timeout": "Chit ê bāng-chām ê hôe-èng siuⁿ bān, chhéng-kiû chhiau-kè sî-kan: %v.",
|
||||
"error.password_min_length": "Chhiáⁿ chì-chió ài su-li̍p la̍k ê lī goân.",
|
||||
"error.pocket_access_token": "Bô-hoat-tō͘ ùi Pocket thê tio̍h access token",
|
||||
"error.pocket_request_token": "Bô-hoat-tō͘ ùi Pocket thê tio̍h request token",
|
||||
"error.proxy_url_not_empty": "Proxy URL bōe-sái sī khang--ê.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Bô-hāu ê hong-só kui-chek: kui-chek #%d khiàm ū-hāu ê lân-ūi miâ (e-sai ê soán-hāng: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Bô-hāu ê hong-só kui-chek: kui-chek #%d ê bô͘-sek m̄ sī ha̍p-hoat ê chiàⁿ-kui piáu-ta̍t sek",
|
||||
"error.settings_block_rule_regex_required": "Bô-hāu ê hong-só kui-chek: kui-chek #%d bô thê-kiong chiàⁿ-kui piáu-ta̍t sek",
|
||||
@@ -162,7 +166,8 @@
|
||||
"form.feed.fieldset.rules": "Kui-chek",
|
||||
"form.feed.label.allow_self_signed_certificates": "ún-chún chū chhiam ah-sī bô-hāu ê pîn-chèng",
|
||||
"form.feed.label.apprise_service_urls": "Sú-iōng tō͘-tiám keh khui ê Apprise ho̍k-bū bāng-chí lia̍t-pió",
|
||||
"form.feed.label.blocklist_rules": "Hong-só kui-chek",
|
||||
"form.feed.label.block_filter_entry_rules": "Entry Blocking Rules",
|
||||
"form.feed.label.blocklist_rules": "Regex-Based Blocking Filters",
|
||||
"form.feed.label.category": "lūi-pia̍t",
|
||||
"form.feed.label.cookie": "Siat-tēng Cookies",
|
||||
"form.feed.label.crawler": "Lia̍h goân-tóe lōe-iông",
|
||||
@@ -172,10 +177,11 @@
|
||||
"form.feed.label.feed_password": "Siau-sit lâi-goân bi̍t-bé",
|
||||
"form.feed.label.feed_url": "Siau-sit lâi-goân bāng-chí",
|
||||
"form.feed.label.feed_username": "Siau-sit lâi-goân kháu-chō miâ",
|
||||
"form.feed.label.fetch_via_proxy": "Thàu-kè tāi-lí lia̍h",
|
||||
"form.feed.label.fetch_via_proxy": "Iōng tī su-hāu-khì siat-tēng ê proxy",
|
||||
"form.feed.label.hide_globally": "Tī choân-he̍k ah-bōe tha̍k--ê lia̍t-pió am-khàm siau-sit",
|
||||
"form.feed.label.ignore_http_cache": "Pàng-ba̍k HTTP cache",
|
||||
"form.feed.label.keeplist_rules": "Pó-liû kui-chek",
|
||||
"form.feed.label.keep_filter_entry_rules": "Entry Allow Rules",
|
||||
"form.feed.label.keeplist_rules": "Regex-Based Keep Filters",
|
||||
"form.feed.label.no_media_player": "Bô mûi-thé hòng-sàng khì (im-sìn, sī-sìn)",
|
||||
"form.feed.label.ntfy_activate": "Thui-sàng siau-sit khì ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy ū-siat iu-sian sūn-sū",
|
||||
@@ -185,14 +191,15 @@
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy siōng kē iu-sian sūn-sū",
|
||||
"form.feed.label.ntfy_priority": "Ntfy iu-sian sūn-sū",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (soán thiⁿ)",
|
||||
"form.feed.label.pushover_activate": "Push entries to Pushover",
|
||||
"form.feed.label.pushover_default_priority": "Default priority",
|
||||
"form.feed.label.pushover_high_priority": "High priority",
|
||||
"form.feed.label.pushover_low_priority": "Low priority",
|
||||
"form.feed.label.pushover_max_priority": "Max priority",
|
||||
"form.feed.label.pushover_min_priority": "Minimal priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "Têng siá kui-chek",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.pushover_activate": "Pó-chûn siau-sit kàu pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover ū-siat iu-sian sūn-sū",
|
||||
"form.feed.label.pushover_high_priority": "Pushover koân iu-sian sūn-sū",
|
||||
"form.feed.label.pushover_low_priority": "Pushover kē iu-sian sūn-sū",
|
||||
"form.feed.label.pushover_max_priority": "Pushover siōng koân iu-sian sūn-sū",
|
||||
"form.feed.label.pushover_min_priority": "Pushover siōng kē iu-sian sūn-sū",
|
||||
"form.feed.label.pushover_priority": "Pushover siau-sit iu-sian sūn-sū",
|
||||
"form.feed.label.rewrite_rules": "Content Rewrite Rules",
|
||||
"form.feed.label.scraper_rules": "Lia̍h ê kui-chek",
|
||||
"form.feed.label.site_url": "Bāng-chām bāng-chí",
|
||||
"form.feed.label.title": "Piau-tôe",
|
||||
@@ -226,6 +233,9 @@
|
||||
"form.integration.instapaper_activate": "Pó-chûn siau-sit kàu Instapaper",
|
||||
"form.integration.instapaper_password": "Instapaper bi̍t-bé",
|
||||
"form.integration.instapaper_username": "Instapaper Kháu-chō miâ",
|
||||
"form.integration.karakeep_activate": "Pó-chûn siau-sit kàu Karakeep",
|
||||
"form.integration.karakeep_api_key": "Karakeep API só-sî",
|
||||
"form.integration.karakeep_url": "Karakeep API thâu",
|
||||
"form.integration.linkace_activate": "Pó-chûn siau-sit kàu LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API só-sî",
|
||||
"form.integration.linkace_check_disabled": "Thêng iōng liân-kiat kiám-cha",
|
||||
@@ -239,7 +249,7 @@
|
||||
"form.integration.linkding_tags": "Linkding khan-á",
|
||||
"form.integration.linkwarden_activate": "Pó-chûn siau-sit kàu Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API só-sî",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden API thâu",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden Base URL",
|
||||
"form.integration.matrix_bot_activate": "Thui-sàng siau-sit kàu Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Matrix pâng-keng ID",
|
||||
"form.integration.matrix_bot_password": "Matrix bi̍t-bé",
|
||||
@@ -266,15 +276,11 @@
|
||||
"form.integration.pinboard_bookmark": "Chù chòe ah-bōe tha̍k",
|
||||
"form.integration.pinboard_tags": "Pinboard khan-á",
|
||||
"form.integration.pinboard_token": "Pinboard API Token",
|
||||
"form.integration.pocket_access_token": "Pocket token",
|
||||
"form.integration.pocket_activate": "Pó-chûn siau-sit kàu Pocket",
|
||||
"form.integration.pocket_connect_link": "Kah Pocket kháu-chō kiat chòe-hé",
|
||||
"form.integration.pocket_consumer_key": "Pocket sú-iōng-lâng só-sî",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
"form.integration.pushover_token": "Pushover application API token",
|
||||
"form.integration.pushover_user": "Pushover user key",
|
||||
"form.integration.pushover_activate": "Pó-chûn siau-sit kàu Pushover",
|
||||
"form.integration.pushover_device": "Pushover ki-hì (soán thiⁿ)",
|
||||
"form.integration.pushover_prefix": "Pushover URL tó͘-bí (soán thiⁿ)",
|
||||
"form.integration.pushover_token": "Pushover application API só-sî",
|
||||
"form.integration.pushover_user": "Pushover sú-iōng-lâng só-sî",
|
||||
"form.integration.raindrop_activate": "Pó-chûn siau-sit kàu Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "khan-á (iōng tō͘-tiám keh khui)",
|
||||
@@ -288,6 +294,7 @@
|
||||
"form.integration.readwise_api_key": "Readwise Reader Acess Token",
|
||||
"form.integration.readwise_api_key_link": "Chhú-tek lí ê Readwise Acess Token",
|
||||
"form.integration.rssbridge_activate": "Sin cheng-ka siau-sit lâi-goân ê sî tio̍h RSS-Bridge",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge su-hāu-khì的bāng-chí",
|
||||
"form.integration.shaarli_activate": "Pó-chûn siau-sit kàu Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API só-sî",
|
||||
@@ -320,6 +327,7 @@
|
||||
"form.prefs.fieldset.global_feed_settings": "Choân-he̍k siau-sit lâi-goân siat-tēng",
|
||||
"form.prefs.fieldset.reader_settings": "Ia̍t-tha̍k khì siat-tēng",
|
||||
"form.prefs.help.external_font_hosts": "Iōng khang-keh keh khui ún-chún ê gōa-pō͘ lī-hêng lâi-goân. Phì-lû \"fonts.gstatic.com fonts.googleapis.com\"",
|
||||
"form.prefs.label.always_open_external_links": "Chhiau-chhē bûn-chiong sī iōng gōa-pō͘ liân-kiat phah khui",
|
||||
"form.prefs.label.categories_sorting_order": "Lūi-pia̍t hián-sī sūn-sū",
|
||||
"form.prefs.label.cjk_reading_speed": "Tiong-bûn, Hân-bûn, Li̍t-bûn tha̍k ê sok-tō͘ (múi hun-cheng ē-sái tha̍k kúi ê lī-goân)",
|
||||
"form.prefs.label.custom_css": "Chū tēng ê CSS",
|
||||
@@ -340,14 +348,15 @@
|
||||
"form.prefs.label.mark_read_on_view": "Phah khui ê sî-chūn sūn-sòa kā siau-sit chù chòe tha̍k kè",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Phah khui ê sî-chūn sūn-sòa kā siau-sit chù chòe tha̍k kè, m̄-koh nā-sī im-sìn, sī-sìn tio̍h tī hòng-sàng kàu 90%% ê si-chun chiah lâi chù",
|
||||
"form.prefs.label.media_playback_rate": "Im-sìn, sī-sìn pàng ê sok-tō͘",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Chhiau-chhē gōa-pō͘ liân-kiat sī tī sin ê ia̍h phah khui (kā liân-kiat chhē target=\"_blank\")",
|
||||
"form.prefs.label.show_reading_time": "Hián-sī siau-sit àn-sǹg ài gōa-kú lâi tha̍k",
|
||||
"form.prefs.label.theme": "Chú-tôe",
|
||||
"form.prefs.label.timezone": "Sî-khu",
|
||||
"form.prefs.select.alphabetical": "Chiàu lī-bú pâi",
|
||||
"form.prefs.select.browser": "Tī iû-lám khì phah khui",
|
||||
"form.prefs.select.browser": "Iû-lâm-khì",
|
||||
"form.prefs.select.created_time": "Siau-sit kiàn-li̍p sî-kan",
|
||||
"form.prefs.select.fullscreen": "Choân êng-bō͘",
|
||||
"form.prefs.select.minimal_ui": "Siōng iông-chhun--ê",
|
||||
"form.prefs.select.minimal_ui": "Siōng sió UI",
|
||||
"form.prefs.select.none": "Bô",
|
||||
"form.prefs.select.older_first": "Ùi kū--ê khai-sí pâi",
|
||||
"form.prefs.select.publish_time": "Siau-sit hoat-pò͘ sî-kan",
|
||||
@@ -399,7 +408,8 @@
|
||||
"page.about.author": "Chok-chiá: ",
|
||||
"page.about.build_date": "Kiàn-tì li̍t-kî:",
|
||||
"page.about.credits": "Pán-koân",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.db_usage": "Database chhài-chhiú:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.global_config_options": "Choân-he̍k siat-tēng soán-hāng",
|
||||
"page.about.go_version": "Go pán-pún:",
|
||||
"page.about.license": "Pàng-koân:",
|
||||
@@ -562,7 +572,7 @@
|
||||
"search.label": "Chhiau-chhē",
|
||||
"search.placeholder": "Chhiau-chhē...",
|
||||
"search.submit": "Chhiau-chhē",
|
||||
"skip_to_content": "Sóa kah chú-iàu ê lōe-iông",
|
||||
"skip_to_content": "Thiaⁿ--khì chhòng-bûn",
|
||||
"time_elapsed.days": [
|
||||
"%d kang chêng"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"action.cancel": "annuleren",
|
||||
"action.download": "Download",
|
||||
"action.download": "Downloaden",
|
||||
"action.edit": "Bewerken",
|
||||
"action.home_screen": "Toevoegen aan startscherm",
|
||||
"action.import": "Importeren",
|
||||
@@ -27,7 +27,6 @@
|
||||
"alert.no_tag_entry": "Er zijn geen artikelen die overeenkomen met deze tag.",
|
||||
"alert.no_unread_entry": "Er zijn geen ongelezen artikelen.",
|
||||
"alert.no_user": "Je bent de enige gebruiker.",
|
||||
"alert.pocket_linked": "Jouw Pocket-account is nu gekoppeld!",
|
||||
"alert.prefs_saved": "Instellingen opgeslagen!",
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"Je hebt te veel feed-vernieuwingen getriggered. Wacht aub %d minuut voor opnieuw proberen.",
|
||||
@@ -43,7 +42,7 @@
|
||||
"enclosure_media_controls.speed": "Snelheid:",
|
||||
"enclosure_media_controls.speed.faster": "Versnel",
|
||||
"enclosure_media_controls.speed.faster.title": "Versnel met %sx",
|
||||
"enclosure_media_controls.speed.reset": "Reset",
|
||||
"enclosure_media_controls.speed.reset": "Resetten",
|
||||
"enclosure_media_controls.speed.reset.title": "Reset snelheid naar 1x",
|
||||
"enclosure_media_controls.speed.slower": "Vertraag",
|
||||
"enclosure_media_controls.speed.slower.title": "Vertraag met %sx",
|
||||
@@ -72,11 +71,15 @@
|
||||
"entry.state.loading": "Laden...",
|
||||
"entry.state.saving": "Opslaan...",
|
||||
"entry.status.mark_as_read": "Markeren als gelezen",
|
||||
"entry.status.mark_as_unread": "Markeren als ongelezen",
|
||||
"entry.status.title": "Verander artikelstatus",
|
||||
"entry.status.toast.read": "Gemarkeerd als gelezen",
|
||||
"entry.status.toast.unread": "Gemarkeerd als ongelezen",
|
||||
"entry.status.mark_as_unread": "Markeren als ongelezen",
|
||||
"entry.tags.label": "Tags:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Toon %d extra tag",
|
||||
"Toon %d extra tags"
|
||||
],
|
||||
"entry.unshare.label": "Delen ongedaan maken",
|
||||
"error.api_key_already_exists": "Deze API-sleutel bestaat al.",
|
||||
"error.bad_credentials": "Onjuiste gebruikersnaam of wachtwoord.",
|
||||
@@ -114,9 +117,12 @@
|
||||
"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_too_many_requests": "Miniflux heeft te veel aanvragen gegenereerd voor deze website. Probeer het later nog eens of wijzig de applicatieconfiguratie.",
|
||||
"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.invalid_categories_sorting_order": "Ongeldige volgorde van categorieën.",
|
||||
"error.invalid_default_home_page": "Ongeldige startpagina!",
|
||||
"error.invalid_display_mode": "Ongeldige weergavemodus voor de webapp.",
|
||||
"error.invalid_entry_direction": "Ongeldige sorteervolgorde.",
|
||||
"error.invalid_entry_order": "Ongeldige volgorde van artikelen.",
|
||||
"error.invalid_feed_proxy_url": "Ongeldige proxy-URL.",
|
||||
"error.invalid_feed_url": "Ongeldige feed URL.",
|
||||
"error.invalid_gesture_nav": "Ongeldige gebarennavigatie.",
|
||||
"error.invalid_language": "Ongeldige taal.",
|
||||
@@ -126,13 +132,12 @@
|
||||
"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.password_min_length": "Minimaal 6 tekens gebruiken.",
|
||||
"error.pocket_access_token": "Kon geen toegangstoken ophalen van Pocket!",
|
||||
"error.pocket_request_token": "Kon geen aanvraagtoken ophalen van Pocket!",
|
||||
"error.proxy_url_not_empty": "De proxy-URL mag niet leeg zijn.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Ongeldige blokkeerregel: regel #%d mist een geldige veldnaam (Opties: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Ongeldige blokkeerregel: het patroon van regel #%d is geen geldige regex",
|
||||
"error.settings_block_rule_regex_required": "Ongeldige blokkeerregel: het patroon van regel #%d is niet opgegeven",
|
||||
"error.settings_block_rule_separator_required": "Ongeldige blokkeerregel: het patroon van regel #%d moet worden gescheiden door een '='",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"error.settings_invalid_domain_list": "Ongeldige domeinlijst. Geef een spatiegescheiden lijst van domeinen op.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Ongeldige bewaarregel: regel #%d mist een geldige veldnaam (Options: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Ongeldige bewaarregel: het patroon van regel #%d is geen geldige regex",
|
||||
"error.settings_keep_rule_regex_required": "Ongeldige bewaarregel: het patroon van regel #%d is niet opgegeven",
|
||||
@@ -164,7 +169,8 @@
|
||||
"form.feed.fieldset.rules": "Regels",
|
||||
"form.feed.label.allow_self_signed_certificates": "Zelfondertekende of ongeldige certificaten toestaan",
|
||||
"form.feed.label.apprise_service_urls": "Door komma's gescheiden lijst van Apprise service URL's",
|
||||
"form.feed.label.blocklist_rules": "Blokkeerregels",
|
||||
"form.feed.label.block_filter_entry_rules": "Blokkeerregels voor Items",
|
||||
"form.feed.label.blocklist_rules": "Regex-gebaseerde Blokkeerfilters",
|
||||
"form.feed.label.category": "Categorie",
|
||||
"form.feed.label.cookie": "Cookies instellen",
|
||||
"form.feed.label.crawler": "Download originele inhoud",
|
||||
@@ -174,10 +180,11 @@
|
||||
"form.feed.label.feed_password": "Feed wachtwoord",
|
||||
"form.feed.label.feed_url": "Feed URL",
|
||||
"form.feed.label.feed_username": "Feed gebruikersnaam",
|
||||
"form.feed.label.fetch_via_proxy": "Ophalen via proxy",
|
||||
"form.feed.label.fetch_via_proxy": "Gebruik de proxy die op applicatieniveau is geconfigureerd",
|
||||
"form.feed.label.hide_globally": "Verberg artikelen in de globale ongelezen lijst",
|
||||
"form.feed.label.ignore_http_cache": "Negeer HTTP-cache",
|
||||
"form.feed.label.keeplist_rules": "Bewaarregels",
|
||||
"form.feed.label.keep_filter_entry_rules": "Toestaan Regels voor Items",
|
||||
"form.feed.label.keeplist_rules": "Regex-gebaseerde Bewaarfilters",
|
||||
"form.feed.label.no_media_player": "Geen mediaspeler (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Artikelen naar ntfy sturen",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy standaard prioriteit",
|
||||
@@ -186,15 +193,16 @@
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy maximale prioriteit",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy minimale prioriteit",
|
||||
"form.feed.label.ntfy_priority": "Ntfy prioriteit",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "Herschrijfregels",
|
||||
"form.feed.label.ntfy_topic": "Ntfy onderwerp (optioneel)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.pushover_activate": "Stuur artikelen naar pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover standaard prioriteit",
|
||||
"form.feed.label.pushover_high_priority": "Pushover hoge prioriteit",
|
||||
"form.feed.label.pushover_low_priority": "Pushover lage prioriteit",
|
||||
"form.feed.label.pushover_max_priority": "Pushover maximale prioriteit",
|
||||
"form.feed.label.pushover_min_priority": "Pushover minimale prioriteit",
|
||||
"form.feed.label.pushover_priority": "Pushover berichtprioriteit",
|
||||
"form.feed.label.rewrite_rules": "Inhoud Herschrijfregels",
|
||||
"form.feed.label.scraper_rules": "Extractieregels",
|
||||
"form.feed.label.site_url": "Website URL",
|
||||
"form.feed.label.title": "Titel",
|
||||
@@ -209,8 +217,8 @@
|
||||
"form.integration.betula_activate": "Artikelen opslaan in Betula",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.integration.cubox_activate": "Artikelen opslaan in Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API-link",
|
||||
"form.integration.discord_activate": "Artikelen opslaan in Discord",
|
||||
"form.integration.discord_webhook_link": "Discord Webhook link",
|
||||
"form.integration.espial_activate": "Artikelen opslaan in Espial",
|
||||
@@ -228,6 +236,9 @@
|
||||
"form.integration.instapaper_activate": "Artikelen opslaan in Instapaper",
|
||||
"form.integration.instapaper_password": "Instapaper wachtwoord",
|
||||
"form.integration.instapaper_username": "Instapaper gebruikersnaam",
|
||||
"form.integration.karakeep_activate": "Artikelen opslaan in Karakeep",
|
||||
"form.integration.karakeep_api_key": "Karakeep API-sleutel",
|
||||
"form.integration.karakeep_url": "Karakeep URL",
|
||||
"form.integration.linkace_activate": "Artikelen opslaan in LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API-sleutel",
|
||||
"form.integration.linkace_check_disabled": "Koppelingcontrole uitschakelen",
|
||||
@@ -241,7 +252,7 @@
|
||||
"form.integration.linkding_tags": "Linkding tags",
|
||||
"form.integration.linkwarden_activate": "Artikelen opslaan in Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API-sleutel",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden URL",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden Basis URL",
|
||||
"form.integration.matrix_bot_activate": "Nieuwe artikelen opslaan in Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID van Matrix-kamer",
|
||||
"form.integration.matrix_bot_password": "Wachtwoord voor Matrix-gebruiker",
|
||||
@@ -253,7 +264,7 @@
|
||||
"form.integration.ntfy_activate": "Stuur artikelen naar ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optioneel)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optioneel)",
|
||||
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
|
||||
"form.integration.ntfy_internal_links": "Gebruik interne links bij klikken (optioneel)",
|
||||
"form.integration.ntfy_password": "Ntfy wachtwoord (optioneel)",
|
||||
"form.integration.ntfy_topic": "Ntfy topic (standaard gebruikt als deze niet is ingesteld in feed)",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optioneel, standaard is ntfy.sh)",
|
||||
@@ -268,10 +279,6 @@
|
||||
"form.integration.pinboard_bookmark": "Markeer favoriet als ongelezen",
|
||||
"form.integration.pinboard_tags": "Pinboard tags",
|
||||
"form.integration.pinboard_token": "Pinboard API token",
|
||||
"form.integration.pocket_access_token": "Pocket Access Token",
|
||||
"form.integration.pocket_activate": "Artikelen opslaan in Pocket",
|
||||
"form.integration.pocket_connect_link": "Verbind je Pocket-account",
|
||||
"form.integration.pocket_consumer_key": "Pocket Consumer Key",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
@@ -290,6 +297,7 @@
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Readwise Access Token ophalen",
|
||||
"form.integration.rssbridge_activate": "Controleer RSS-Bridge bij het toevoegen van abonnementen",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Artikelen opslaan in Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
@@ -321,7 +329,8 @@
|
||||
"form.prefs.fieldset.authentication_settings": "Authenticatie Instellingen",
|
||||
"form.prefs.fieldset.global_feed_settings": "Globale Feed Instellingen",
|
||||
"form.prefs.fieldset.reader_settings": "Lees Instellingen",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.help.external_font_hosts": "Spatiegescheiden lijst van externe font-hosts die zijn toegestaan. Bijvoorbeeld: 'fonts.gstatic.com fonts.googleapis.com'.",
|
||||
"form.prefs.label.always_open_external_links": "Lees artikelen door externe links te openen",
|
||||
"form.prefs.label.categories_sorting_order": "Volgorde categorieën",
|
||||
"form.prefs.label.cjk_reading_speed": "Leessnelheid voor Chinees, Koreaans en Japans (tekens per minuut)",
|
||||
"form.prefs.label.custom_css": "Aangepaste CSS",
|
||||
@@ -333,7 +342,7 @@
|
||||
"form.prefs.label.entry_order": "Artikelen sorteren",
|
||||
"form.prefs.label.entry_sorting": "Volgorde van artikelen",
|
||||
"form.prefs.label.entry_swipe": "Vegen tussen artikelen inschakelen op aanraakschermen",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.label.external_font_hosts": "Externe font-hosts",
|
||||
"form.prefs.label.gesture_nav": "Gebaar om tussen artikelen te navigeren",
|
||||
"form.prefs.label.keyboard_shortcuts": "Sneltoetsen inschakelen",
|
||||
"form.prefs.label.language": "Taal",
|
||||
@@ -342,6 +351,7 @@
|
||||
"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.media_playback_rate": "Afspeelsnelheid van de audio/video",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Open externe links in een nieuw tabblad (voegt target=\"_blank\" toe aan links)",
|
||||
"form.prefs.label.show_reading_time": "Toon geschatte leestijd van artikelen",
|
||||
"form.prefs.label.theme": "Thema",
|
||||
"form.prefs.label.timezone": "Tijdzone",
|
||||
@@ -401,7 +411,8 @@
|
||||
"page.about.author": "Auteur:",
|
||||
"page.about.build_date": "Compilatiedatum:",
|
||||
"page.about.credits": "Credits",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.db_usage": "Databasegrootte:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.global_config_options": "Globale Configuratie Opties",
|
||||
"page.about.go_version": "Go versie:",
|
||||
"page.about.license": "Licentie:",
|
||||
@@ -504,7 +515,7 @@
|
||||
"page.login.title": "Inloggen",
|
||||
"page.login.webauthn_login": "Inloggen met passkey",
|
||||
"page.login.webauthn_login.error": "Kan niet inloggen met passkey",
|
||||
"page.login.webauthn_login.help": "Please enter your username if you're using a security key. This is not required if you are using a Passkey (discoverable credentials).",
|
||||
"page.login.webauthn_login.help": "Voer je gebruikersnaam in als je een beveiligingssleutel gebruikt. Dit is niet nodig als je een Passkey (ontdekkingsbare referenties) gebruikt.",
|
||||
"page.new_api_key.title": "Nieuwe API-sleutel",
|
||||
"page.new_category.title": "Nieuwe categorie",
|
||||
"page.new_user.title": "Nieuwe gebruiker",
|
||||
@@ -603,4 +614,4 @@
|
||||
"time_elapsed.yesterday": "gisteren",
|
||||
"tooltip.keyboard_shortcuts": "Sneltoets: %s",
|
||||
"tooltip.logged_user": "Ingelogd als %s"
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@
|
||||
"alert.no_tag_entry": "Brak wpisów pasujących do tego znacznika.",
|
||||
"alert.no_unread_entry": "Nie ma żadnych nieprzeczytanych wpisów.",
|
||||
"alert.no_user": "Jesteś jedynym użytkownikiem.",
|
||||
"alert.pocket_linked": "Twoje konto Pocket jest teraz połączone!",
|
||||
"alert.prefs_saved": "Ustawienia zapisane!",
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"Wykonano zbyt wiele odświeżeń kanału. Poczekaj %d minutę przed ponowną próbą.",
|
||||
@@ -74,11 +73,16 @@
|
||||
"entry.state.loading": "Ładowanie…",
|
||||
"entry.state.saving": "Zapisywanie…",
|
||||
"entry.status.mark_as_read": "Oznacz jako przeczytany",
|
||||
"entry.status.mark_as_unread": "Oznacz jako nieprzeczytany",
|
||||
"entry.status.title": "Zmień status wpisu",
|
||||
"entry.status.toast.read": "Oznaczono jako przeczytany",
|
||||
"entry.status.toast.unread": "Oznaczono jako nieprzeczytany",
|
||||
"entry.status.mark_as_unread": "Oznacz jako nieprzeczytany",
|
||||
"entry.tags.label": "Znaczniki:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Dodaj znacznik",
|
||||
"Dodaj %d znaczniki",
|
||||
"Dodaj %d znaczników"
|
||||
],
|
||||
"entry.unshare.label": "Cofnij udostępnianie",
|
||||
"error.api_key_already_exists": "Ten klucz API już istnieje.",
|
||||
"error.bad_credentials": "Nieprawidłowa nazwa użytkownika lub hasło.",
|
||||
@@ -96,7 +100,7 @@
|
||||
"error.feed_category_not_found": "Ta kategoria nie istnieje lub nie należy do tego użytkownika.",
|
||||
"error.feed_format_not_detected": "Nie można wykryć formatu kanału: %v.",
|
||||
"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.feed_invalid_keeplist_rule": "Reguła listy zachowywania jest nieprawidłowa.",
|
||||
"error.feed_mandatory_fields": "Adres URL i kategoria są obowiązkowe.",
|
||||
"error.feed_not_found": "Ten kanał nie istnieje lub nie należy do tego użytkownika.",
|
||||
"error.feed_title_not_empty": "Tytuł kanału nie może być pusty.",
|
||||
@@ -116,9 +120,12 @@
|
||||
"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_too_many_requests": "Miniflux wygenerował zbyt wiele żądań do tej witryny. Spróbuj ponownie później lub zmień konfigurację aplikacji.",
|
||||
"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.invalid_categories_sorting_order": "Nieprawidłowa kolejność sortowania kategorii.",
|
||||
"error.invalid_default_home_page": "Nieprawidłowa domyślna strona główna!",
|
||||
"error.invalid_display_mode": "Nieprawidłowy tryb wyświetlania aplikacji sieciowej.",
|
||||
"error.invalid_entry_direction": "Nieprawidłowa kolejność sortowania.",
|
||||
"error.invalid_entry_order": "Nieprawidłowa kolejność sortowania wpisów.",
|
||||
"error.invalid_feed_proxy_url": "Nieprawidłowy adres URL serwera proxy.",
|
||||
"error.invalid_feed_url": "Nieprawidłowy adres URL kanału.",
|
||||
"error.invalid_gesture_nav": "Nieprawidłowa nawigacja gestami.",
|
||||
"error.invalid_language": "Nieprawidłowy język.",
|
||||
@@ -128,8 +135,7 @@
|
||||
"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.password_min_length": "Musisz użyć co najmniej 6 znaków.",
|
||||
"error.pocket_access_token": "Nie można pobrać tokena dostępu z Pocket!",
|
||||
"error.pocket_request_token": "Nie można pobrać tokena żądania z Pocket!",
|
||||
"error.proxy_url_not_empty": "Adres URL serwera proxy nie może być pusty.",
|
||||
"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_invalid_regex": "Nieprawidłowa reguła blokowania: wzór reguły #%d nie jest prawidłowym wyrażeniem regularnym",
|
||||
"error.settings_block_rule_regex_required": "Nieprawidłowa reguła blokowania: nie podano wzorca reguły #%d",
|
||||
@@ -166,7 +172,8 @@
|
||||
"form.feed.fieldset.rules": "Reguły",
|
||||
"form.feed.label.allow_self_signed_certificates": "Zezwalaj na samopodpisane lub nieprawidłowe certyfikaty",
|
||||
"form.feed.label.apprise_service_urls": "Rozdzielana przecinkami lista adresów URL usług Appprise",
|
||||
"form.feed.label.blocklist_rules": "Reguły blokowania",
|
||||
"form.feed.label.block_filter_entry_rules": "Reguły blokowania wpisów",
|
||||
"form.feed.label.blocklist_rules": "Filtry blokowania oparte na wyrażeniach regularnych",
|
||||
"form.feed.label.category": "Kategoria",
|
||||
"form.feed.label.cookie": "Ustaw ciasteczka",
|
||||
"form.feed.label.crawler": "Pobierz oryginalną treść",
|
||||
@@ -176,10 +183,11 @@
|
||||
"form.feed.label.feed_password": "Hasło do subskrypcji",
|
||||
"form.feed.label.feed_url": "Adres URL kanału",
|
||||
"form.feed.label.feed_username": "Nazwa użytkownika subskrypcji",
|
||||
"form.feed.label.fetch_via_proxy": "Pobierz przez proxy",
|
||||
"form.feed.label.fetch_via_proxy": "Użyj serwera proxy skonfigurowanego na poziomie aplikacji",
|
||||
"form.feed.label.hide_globally": "Ukryj wpisy na globalnej liście nieprzeczytanych",
|
||||
"form.feed.label.ignore_http_cache": "Zignoruj pamięć podręczną HTTP",
|
||||
"form.feed.label.keeplist_rules": "Reguły utrzymywania",
|
||||
"form.feed.label.keep_filter_entry_rules": "Reguły zachowywania wpisów",
|
||||
"form.feed.label.keeplist_rules": "Filtry zachowywania oparte na wyrażeniach regularnych",
|
||||
"form.feed.label.no_media_player": "Brak odtwarzacza multimedialnego (audio i wideo)",
|
||||
"form.feed.label.ntfy_activate": "Prześlij wpisy do ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Domyślny priorytet ntfy",
|
||||
@@ -189,6 +197,7 @@
|
||||
"form.feed.label.ntfy_min_priority": "Minimalny priorytet ntfy",
|
||||
"form.feed.label.ntfy_priority": "Priorytet ntfy",
|
||||
"form.feed.label.ntfy_topic": "Temat ntfy (opcjonalny)",
|
||||
"form.feed.label.proxy_url": "Adres URL serwera proxy",
|
||||
"form.feed.label.pushover_activate": "Prześlij wpisy do pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Domyślny priorytet Pushover",
|
||||
"form.feed.label.pushover_high_priority": "Wysoki priorytet Pushover",
|
||||
@@ -196,7 +205,7 @@
|
||||
"form.feed.label.pushover_max_priority": "Maksymalny priorytet Pushover",
|
||||
"form.feed.label.pushover_min_priority": "Minimalny priorytet Pushover",
|
||||
"form.feed.label.pushover_priority": "Priorytet wiadomości Pushover",
|
||||
"form.feed.label.rewrite_rules": "Reguły zapisu",
|
||||
"form.feed.label.rewrite_rules": "Reguły przepisywania treści",
|
||||
"form.feed.label.scraper_rules": "Reguły ekstrakcji",
|
||||
"form.feed.label.site_url": "Adres URL strony",
|
||||
"form.feed.label.title": "Tytuł",
|
||||
@@ -230,6 +239,9 @@
|
||||
"form.integration.instapaper_activate": "Zapisuj wpisy w Instapaper",
|
||||
"form.integration.instapaper_password": "Hasło do Instapaper",
|
||||
"form.integration.instapaper_username": "Login do Instapaper",
|
||||
"form.integration.karakeep_activate": "Zapisuj wpisy w Karakeep",
|
||||
"form.integration.karakeep_api_key": "Klucz API do Karakeep",
|
||||
"form.integration.karakeep_url": "Punkt końcowy API Karakeep",
|
||||
"form.integration.linkace_activate": "Zapisuj wpisy w LinkAce",
|
||||
"form.integration.linkace_api_key": "Klucz API do LinkAce",
|
||||
"form.integration.linkace_check_disabled": "Wyłącz sprawdzanie łączy",
|
||||
@@ -243,7 +255,7 @@
|
||||
"form.integration.linkding_tags": "Znaczniki Linkding",
|
||||
"form.integration.linkwarden_activate": "Zapisuj wpisy w Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Klucz API do Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Punkt końcowy API Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Podstawowy adres URL Linkwardena",
|
||||
"form.integration.matrix_bot_activate": "Przesyłaj nowe wpisy do Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Identyfikator pokoju Matrix",
|
||||
"form.integration.matrix_bot_password": "Hasło do Matrix",
|
||||
@@ -270,10 +282,6 @@
|
||||
"form.integration.pinboard_bookmark": "Zaznacz zakładkę jako nieprzeczytaną",
|
||||
"form.integration.pinboard_tags": "Znaczniki Pinboard",
|
||||
"form.integration.pinboard_token": "Token API do Pinboard",
|
||||
"form.integration.pocket_access_token": "Token dostępu do Pocket",
|
||||
"form.integration.pocket_activate": "Zapisuj wpisy w Pocket",
|
||||
"form.integration.pocket_connect_link": "Połącz swoje konto Pocket",
|
||||
"form.integration.pocket_consumer_key": "Klucz klienta do Pocket",
|
||||
"form.integration.pushover_activate": "Prześlij wpisy do Pushover",
|
||||
"form.integration.pushover_device": "Urządzenie Pushover (opcjonalne)",
|
||||
"form.integration.pushover_prefix": "Prefiks adresu URL Pushover (opcjonalny)",
|
||||
@@ -292,6 +300,7 @@
|
||||
"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.rssbridge_activate": "Sprawdź RSS-Bridge podczas dodawania subskrypcji",
|
||||
"form.integration.rssbridge_token": "Token uwierzytelniający RSS-Bridge",
|
||||
"form.integration.rssbridge_url": "Adres URL serwera RSS-Bridge",
|
||||
"form.integration.shaarli_activate": "Zapisuj artykuły w Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Tajny klucz API do Shaarli",
|
||||
@@ -301,7 +310,7 @@
|
||||
"form.integration.shiori_password": "Hasło do Shiori",
|
||||
"form.integration.shiori_username": "Login do Shiori",
|
||||
"form.integration.slack_activate": "Przesyłaj wpisy do Slack",
|
||||
"form.integration.slack_webhook_link": "Slack Webhook link",
|
||||
"form.integration.slack_webhook_link": "Łącze webhooka Slack",
|
||||
"form.integration.telegram_bot_activate": "Przesyłaj nowe wpisy do czatu Telegram",
|
||||
"form.integration.telegram_bot_disable_buttons": "Wyłącz przyciski",
|
||||
"form.integration.telegram_bot_disable_notification": "Wyłącz powiadomienie",
|
||||
@@ -324,6 +333,7 @@
|
||||
"form.prefs.fieldset.global_feed_settings": "Globalne ustawienia kanałów",
|
||||
"form.prefs.fieldset.reader_settings": "Ustawienia czytnika",
|
||||
"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”.",
|
||||
"form.prefs.label.always_open_external_links": "Czytaj artykuły, otwierając łącza zewnętrzne",
|
||||
"form.prefs.label.categories_sorting_order": "Sortowanie kategorii",
|
||||
"form.prefs.label.cjk_reading_speed": "Szybkość czytania w języku chińskim, koreańskim i japońskim (znaki na minutę)",
|
||||
"form.prefs.label.custom_css": "Niestandardowy CSS",
|
||||
@@ -344,6 +354,7 @@
|
||||
"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.media_playback_rate": "Szybkość odtwarzania audio i wideo",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Otwieraj łącza zewnętrzne w nowej karcie (dodaje target=\"_blank\" do łączy)",
|
||||
"form.prefs.label.show_reading_time": "Pokaż szacowany czas czytania wpisów",
|
||||
"form.prefs.label.theme": "Wygląd",
|
||||
"form.prefs.label.timezone": "Strefa czasowa",
|
||||
@@ -404,6 +415,7 @@
|
||||
"page.about.build_date": "Data opracowania:",
|
||||
"page.about.credits": "Prawa autorskie",
|
||||
"page.about.db_usage": "Rozmiar bazy danych:",
|
||||
"page.about.git_commit": "Zatwierdzenie Git:",
|
||||
"page.about.global_config_options": "Globalne opcje konfiguracji",
|
||||
"page.about.go_version": "Wersja Go:",
|
||||
"page.about.license": "Licencja:",
|
||||
@@ -620,4 +632,4 @@
|
||||
"time_elapsed.yesterday": "wczoraj",
|
||||
"tooltip.keyboard_shortcuts": "Skróty klawiszowe: %s",
|
||||
"tooltip.logged_user": "Zalogowany jako %s"
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"action.update": "Atualizar",
|
||||
"alert.account_linked": "Sua conta externa está vinculada!",
|
||||
"alert.account_unlinked": "Sua conta externa está desvinculada!",
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"alert.background_feed_refresh": "Todas as fontes estão sendo atualizadas em segundo plano. Você pode continuar usando o Miniflux enquanto este processo está em execução.",
|
||||
"alert.feed_error": "Ocorreu um problema com esta fonte.",
|
||||
"alert.no_bookmark": "Não há favorito neste momento.",
|
||||
"alert.no_category": "Não há categoria.",
|
||||
@@ -27,26 +27,25 @@
|
||||
"alert.no_tag_entry": "Não há itens que correspondam a esta etiqueta.",
|
||||
"alert.no_unread_entry": "Não há itens não lidos.",
|
||||
"alert.no_user": "Você é o único usuário.",
|
||||
"alert.pocket_linked": "Sua conta do Pocket está vinculada!",
|
||||
"alert.prefs_saved": "Suas preferências foram salvas!",
|
||||
"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."
|
||||
"Você acionou muitas atualizações de fontes. Por favor, aguarde %d minuto antes de tentar novamente.",
|
||||
"Você acionou muitas atualizações de fontes. Por favor, aguarde %d minutos antes de tentar novamente."
|
||||
],
|
||||
"confirm.loading": "Carregando...",
|
||||
"confirm.no": "Não",
|
||||
"confirm.question": "Tem certeza?",
|
||||
"confirm.question.refresh": "Você deseja forçar a atualização?",
|
||||
"confirm.yes": "Sim",
|
||||
"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.reset": "Reset",
|
||||
"enclosure_media_controls.speed.reset.title": "Reset speed to 1x",
|
||||
"enclosure_media_controls.speed.slower": "Slower",
|
||||
"enclosure_media_controls.speed.slower.title": "Slower by %sx",
|
||||
"enclosure_media_controls.seek": "Procurar:",
|
||||
"enclosure_media_controls.seek.title": "Procurar %s segundos",
|
||||
"enclosure_media_controls.speed": "Velocidade:",
|
||||
"enclosure_media_controls.speed.faster": "Mais Rápido",
|
||||
"enclosure_media_controls.speed.faster.title": "Mais rápido em %sx",
|
||||
"enclosure_media_controls.speed.reset": "Resetar",
|
||||
"enclosure_media_controls.speed.reset.title": "Resetar velocidade para 1x",
|
||||
"enclosure_media_controls.speed.slower": "Mais Lento",
|
||||
"enclosure_media_controls.speed.slower.title": "Mais lento em %sx",
|
||||
"entry.bookmark.toast.off": "Desfavoritado",
|
||||
"entry.bookmark.toast.on": "Favoritado",
|
||||
"entry.bookmark.toggle.off": "Remover dos Favoritos",
|
||||
@@ -72,78 +71,84 @@
|
||||
"entry.state.loading": "Carregando...",
|
||||
"entry.state.saving": "Salvando...",
|
||||
"entry.status.mark_as_read": "Marcar como lido",
|
||||
"entry.status.mark_as_unread": "Marcar como não lido",
|
||||
"entry.status.title": "Modificar estado deste item",
|
||||
"entry.status.toast.read": "Marcado como lido",
|
||||
"entry.status.toast.unread": "Marcado como não lido",
|
||||
"entry.status.mark_as_unread": "Marcar como não lido",
|
||||
"entry.tags.label": "Etiquetas:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Mostrar mais %d etiqueta",
|
||||
"Mostrar mais %d etiquetas"
|
||||
],
|
||||
"entry.unshare.label": "Descompartilhar",
|
||||
"error.api_key_already_exists": "Essa chave de API já existe.",
|
||||
"error.bad_credentials": "Usuário ou senha são inválidos.",
|
||||
"error.category_already_exists": "Esta categoria já existe.",
|
||||
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||
"error.database_error": "Database error: %v.",
|
||||
"error.category_not_found": "Esta categoria não existe ou não pertence a este usuário.",
|
||||
"error.database_error": "Erro no banco de dados: %v.",
|
||||
"error.different_passwords": "As senhas não são iguais.",
|
||||
"error.duplicate_fever_username": "Alguém já está utilizando esse nome de usuário do Fever!",
|
||||
"error.duplicate_googlereader_username": "Alguém já está utilizando esse nome de usuário do Google Reader!",
|
||||
"error.duplicate_linked_account": "Alguém já está vinculado a esse serviço!",
|
||||
"error.duplicated_feed": "This feed already exists.",
|
||||
"error.duplicated_feed": "Esta fonte já existe.",
|
||||
"error.empty_file": "Esse arquivo está vazio.",
|
||||
"error.entries_per_page_invalid": "O número de itens por página é inválido.",
|
||||
"error.feed_already_exists": "Este feed já existe.",
|
||||
"error.feed_category_not_found": "Esta categoria não existe ou não pertence a este usuário.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"error.feed_format_not_detected": "Não foi possível detectar o formato da fonte: %v.",
|
||||
"error.feed_invalid_blocklist_rule": "A regra da lista de bloqueio é inválida.",
|
||||
"error.feed_invalid_keeplist_rule": "A regra de manutenção da lista é inválida.",
|
||||
"error.feed_mandatory_fields": "O campo de URL e categoria são obrigatórios.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.feed_not_found": "Esta fonte não existe ou não pertence a este usuário.",
|
||||
"error.feed_title_not_empty": "O título do feed não pode estar vazio.",
|
||||
"error.feed_url_not_empty": "O URL do feed não pode estar vazio.",
|
||||
"error.fields_mandatory": "Todos os campos são obrigatórios.",
|
||||
"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_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_client_error": "HTTP client error: %v.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||
"error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).",
|
||||
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_bad_gateway": "O site não está disponível no momento devido a um erro de gateway. O problema não está no Miniflux. Por favor, tente novamente mais tarde.",
|
||||
"error.http_body_read": "Não foi possível ler o corpo HTTP: %v.",
|
||||
"error.http_client_error": "Erro do cliente HTTP: %v.",
|
||||
"error.http_empty_response": "A resposta HTTP está vazia. Talvez este site esteja usando um mecanismo de proteção contra bots?",
|
||||
"error.http_empty_response_body": "O corpo da resposta HTTP está vazio.",
|
||||
"error.http_forbidden": "O acesso a este site está proibido. Talvez este site tenha um mecanismo de proteção contra bots?",
|
||||
"error.http_gateway_timeout": "O site não está disponível no momento devido a um erro de tempo limite do gateway. O problema não está no Miniflux. Por favor, tente novamente mais tarde.",
|
||||
"error.http_internal_server_error": "O site não está disponível no momento devido a um erro interno do servidor. O problema não está no Miniflux. Por favor, tente novamente mais tarde.",
|
||||
"error.http_not_authorized": "O acesso a este site não está autorizado. Pode ser um nome de usuário ou senha incorretos.",
|
||||
"error.http_resource_not_found": "O recurso solicitado não foi encontrado. Por favor, verifique a URL.",
|
||||
"error.http_response_too_large": "A resposta HTTP é muito grande. Você pode aumentar o limite de tamanho da resposta HTTP nas configurações globais (requer reinício do servidor).",
|
||||
"error.http_service_unavailable": "O site não está disponível no momento devido a um erro interno do servidor. O problema não está no Miniflux. Por favor, tente novamente mais tarde.",
|
||||
"error.http_too_many_requests": "O Miniflux gerou muitas solicitações para este site. Por favor, tente novamente mais tarde ou altere a configuração do aplicativo.",
|
||||
"error.http_unexpected_status_code": "O site não está disponível no momento devido a um código de status HTTP inesperado: %d. O problema não está no Miniflux. Por favor, tente novamente mais tarde.",
|
||||
"error.invalid_categories_sorting_order": "A ordem de classificação das categorias não é válida.",
|
||||
"error.invalid_default_home_page": "Página inicial por defeito inválida!",
|
||||
"error.invalid_display_mode": "Modo de exibição de aplicativo inválido da web.",
|
||||
"error.invalid_entry_direction": "Direção de entrada inválida.",
|
||||
"error.invalid_entry_order": "A ordem de entrada é inválida.",
|
||||
"error.invalid_feed_proxy_url": "URL de proxy inválido.",
|
||||
"error.invalid_feed_url": "URL de feed inválido.",
|
||||
"error.invalid_gesture_nav": "Navegação por gestos inválida.",
|
||||
"error.invalid_language": "Idioma inválido.",
|
||||
"error.invalid_site_url": "URL de site inválido.",
|
||||
"error.invalid_theme": "Tema inválido.",
|
||||
"error.invalid_timezone": "Fuso horário inválido.",
|
||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||
"error.network_operation": "O Miniflux não conseguiu acessar este site devido a um erro de rede: %v.",
|
||||
"error.network_timeout": "Este site está muito lento e a solicitação expirou: %v",
|
||||
"error.password_min_length": "A senha deve ter no mínimo 6 caracteres.",
|
||||
"error.pocket_access_token": "Não foi possível obter um token de acesso no Pocket!",
|
||||
"error.pocket_request_token": "Não foi possível obter um pedido de token no Pocket!",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.proxy_url_not_empty": "A URL do proxy não pode estar vazia.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Regra de bloqueio inválida: a regra #%d está sem um nome de campo válido (Opções: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Regra de bloqueio inválida: o padrão da regra #%d não é uma expressão regular válida",
|
||||
"error.settings_block_rule_regex_required": "Regra de bloqueio inválida: o padrão da regra #%d não foi fornecido",
|
||||
"error.settings_block_rule_separator_required": "Regra de bloqueio inválida: o padrão da regra #%d deve ser separado por um '='",
|
||||
"error.settings_invalid_domain_list": "Lista de domínios inválida. Por favor, forneça uma lista de domínios separados por espaço.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Regra de permissão inválida: a regra #%d está sem um nome de campo válido (Opções: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Regra de permissão inválida: o padrão da regra #%d não é uma expressão regular válida",
|
||||
"error.settings_keep_rule_regex_required": "Regra de permissão inválida: o padrão da regra #%d não foi fornecido",
|
||||
"error.settings_keep_rule_separator_required": "Regra de permissão inválida: o padrão da regra #%d deve ser separado por um '='",
|
||||
"error.settings_mandatory_fields": "Os campos de nome de usuário, tema, idioma e fuso horário são obrigatórios.",
|
||||
"error.settings_media_playback_rate_range": "A velocidade de reprodução está fora do intervalo",
|
||||
"error.settings_reading_speed_is_positive": "As velocidades de leitura devem ser inteiros positivos.",
|
||||
"error.site_url_not_empty": "O URL do site não pode estar vazio.",
|
||||
"error.subscription_not_found": "Não foi possível encontrar uma inscrição.",
|
||||
"error.title_required": "O título é obrigatório.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "Erro TLS: %q. Você pode desabilitar a verificação TLS nas configurações do feed se desejar.",
|
||||
"error.unable_to_create_api_key": "Não foi possível criar uma chave de API.",
|
||||
"error.unable_to_create_category": "Não foi possível criar essa categoria.",
|
||||
"error.unable_to_create_user": "Não foi possível criar esse usuário.",
|
||||
@@ -158,53 +163,56 @@
|
||||
"form.api_key.label.description": "Etiqueta da chave de API",
|
||||
"form.category.hide_globally": "Ocultar entradas na lista global não lida",
|
||||
"form.category.label.title": "Título",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.integration": "Third-Party Services",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.general": "Geral",
|
||||
"form.feed.fieldset.integration": "Serviços de Terceiros",
|
||||
"form.feed.fieldset.network_settings": "Configurações de Rede",
|
||||
"form.feed.fieldset.rules": "Regras",
|
||||
"form.feed.label.allow_self_signed_certificates": "Permitir certificados autoassinados ou inválidos",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.blocklist_rules": "Regras de bloqueio",
|
||||
"form.feed.label.apprise_service_urls": "Lista de URLs de serviços Apprise separadas por vírgula",
|
||||
"form.feed.label.block_filter_entry_rules": "Regras de Bloqueio de Entradas",
|
||||
"form.feed.label.blocklist_rules": "Filtros de Bloqueio Baseados em Regex",
|
||||
"form.feed.label.category": "Categoria",
|
||||
"form.feed.label.cookie": "Definir Cookies",
|
||||
"form.feed.label.crawler": "Obter conteúdo original",
|
||||
"form.feed.label.description": "Descrição",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.disable_http2": "Desativar HTTP/2 para evitar fingerprinting",
|
||||
"form.feed.label.disabled": "Não atualizar esta fonte",
|
||||
"form.feed.label.feed_password": "Senha da fonte",
|
||||
"form.feed.label.feed_url": "URL da fonte",
|
||||
"form.feed.label.feed_username": "Nome de usuário da fonte",
|
||||
"form.feed.label.fetch_via_proxy": "Buscar via proxy",
|
||||
"form.feed.label.fetch_via_proxy": "Usar o proxy configurado no nível da aplicação",
|
||||
"form.feed.label.hide_globally": "Ocultar entradas na lista global não lida",
|
||||
"form.feed.label.ignore_http_cache": "Ignorar cache HTTP",
|
||||
"form.feed.label.keeplist_rules": "Regras de permissão",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "Regras para o Rewrite",
|
||||
"form.feed.label.keep_filter_entry_rules": "Regras de Permissão de Entradas",
|
||||
"form.feed.label.keeplist_rules": "Filtros de Manutenção Baseados em Regex",
|
||||
"form.feed.label.no_media_player": "Sem reprodutor de mídia (áudio/vídeo)",
|
||||
"form.feed.label.ntfy_activate": "Enviar itens para o ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Prioridade padrão do ntfy",
|
||||
"form.feed.label.ntfy_high_priority": "Alta prioridade do ntfy",
|
||||
"form.feed.label.ntfy_low_priority": "Baixa prioridade do ntfy",
|
||||
"form.feed.label.ntfy_max_priority": "Prioridade máxima do ntfy",
|
||||
"form.feed.label.ntfy_min_priority": "Prioridade mínima do ntfy",
|
||||
"form.feed.label.ntfy_priority": "Prioridade do ntfy",
|
||||
"form.feed.label.ntfy_topic": "Tópico do ntfy (opcional)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.pushover_activate": "Enviar itens para o pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Prioridade padrão do Pushover",
|
||||
"form.feed.label.pushover_high_priority": "Alta prioridade do Pushover",
|
||||
"form.feed.label.pushover_low_priority": "Baixa prioridade do Pushover",
|
||||
"form.feed.label.pushover_max_priority": "Prioridade máxima do Pushover",
|
||||
"form.feed.label.pushover_min_priority": "Prioridade mínima do Pushover",
|
||||
"form.feed.label.pushover_priority": "Prioridade da mensagem do Pushover",
|
||||
"form.feed.label.rewrite_rules": "Regras de Reescrita de Conteúdo",
|
||||
"form.feed.label.scraper_rules": "Regras do scraper",
|
||||
"form.feed.label.site_url": "URL do site",
|
||||
"form.feed.label.title": "Título",
|
||||
"form.feed.label.urlrewrite_rules": "Regras de reescrita de URL",
|
||||
"form.feed.label.user_agent": "Sobrescrever o agente de usuário (user-agent) padrão",
|
||||
"form.feed.label.webhook_url": "Override webhook url",
|
||||
"form.feed.label.webhook_url": "Sobrescrever URL do webhook",
|
||||
"form.import.label.file": "Arquivo OPML",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||
"form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
|
||||
"form.integration.apprise_activate": "Enviar itens para o Apprise",
|
||||
"form.integration.apprise_services_url": "Lista de URLs de serviços Apprise separadas por vírgula",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
@@ -228,12 +236,15 @@
|
||||
"form.integration.instapaper_activate": "Salvar itens no Instapaper",
|
||||
"form.integration.instapaper_password": "Senha do Instapaper",
|
||||
"form.integration.instapaper_username": "Nome do usuário do Instapaper",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_is_private": "Mark link as private",
|
||||
"form.integration.linkace_tags": "LinkAce Tags",
|
||||
"form.integration.karakeep_activate": "Salvar itens no Karakeep",
|
||||
"form.integration.karakeep_api_key": "Chave de API do Karakeep",
|
||||
"form.integration.karakeep_url": "Endpoint de API do Karakeep",
|
||||
"form.integration.linkace_activate": "Salvar itens no LinkAce",
|
||||
"form.integration.linkace_api_key": "Chave de API do LinkAce",
|
||||
"form.integration.linkace_check_disabled": "Desativar verificação de link",
|
||||
"form.integration.linkace_endpoint": "Endpoint de API do LinkAce",
|
||||
"form.integration.linkace_is_private": "Marcar link como privado",
|
||||
"form.integration.linkace_tags": "Etiquetas do LinkAce",
|
||||
"form.integration.linkding_activate": "Salvar itens no Linkding",
|
||||
"form.integration.linkding_api_key": "Chave de API do Linkding",
|
||||
"form.integration.linkding_bookmark": "Salvar marcador como não lido",
|
||||
@@ -241,20 +252,20 @@
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkwarden_activate": "Salvar itens no Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Chave de API do Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Endpoint de API do Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "URL base do Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Transferir novos artigos para o Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Identificação da sala Matrix",
|
||||
"form.integration.matrix_bot_password": "Palavra-passe para utilizador da Matrix",
|
||||
"form.integration.matrix_bot_url": "URL do servidor Matrix",
|
||||
"form.integration.matrix_bot_user": "Nome de utilizador para Matrix",
|
||||
"form.integration.notion_activate": "Save entries to Notion",
|
||||
"form.integration.notion_page_id": "Notion Page ID",
|
||||
"form.integration.notion_token": "Notion Secret Token",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.notion_activate": "Salvar itens no Notion",
|
||||
"form.integration.notion_page_id": "ID da página do Notion",
|
||||
"form.integration.notion_token": "Token secreto do Notion",
|
||||
"form.integration.ntfy_activate": "Enviar itens para o ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (opcional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (opcional)",
|
||||
"form.integration.ntfy_internal_links": "Usar links internos ao clicar (opcional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (opcional)",
|
||||
"form.integration.ntfy_topic": "Ntfy topic (default if not set in feed)",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
@@ -268,32 +279,29 @@
|
||||
"form.integration.pinboard_bookmark": "Salvar marcador como não lido",
|
||||
"form.integration.pinboard_tags": "Etiquetas (tags) do Pinboard",
|
||||
"form.integration.pinboard_token": "Token de API do Pinboard",
|
||||
"form.integration.pocket_access_token": "Token de acesso do Pocket",
|
||||
"form.integration.pocket_activate": "Salvar itens no Pocket",
|
||||
"form.integration.pocket_connect_link": "Conectar a conta do Pocket",
|
||||
"form.integration.pocket_consumer_key": "Chave de consumo (Consumer Key) do Pocket",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
"form.integration.pushover_token": "Pushover application API token",
|
||||
"form.integration.pushover_user": "Pushover user key",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.pushover_activate": "Enviar itens para o Pushover",
|
||||
"form.integration.pushover_device": "Dispositivo Pushover (opcional)",
|
||||
"form.integration.pushover_prefix": "Prefixo da URL do Pushover (opcional)",
|
||||
"form.integration.pushover_token": "Token de API do aplicativo Pushover",
|
||||
"form.integration.pushover_user": "Chave do usuário Pushover",
|
||||
"form.integration.raindrop_activate": "Salvar itens no Raindrop",
|
||||
"form.integration.raindrop_collection_id": "ID da coleção",
|
||||
"form.integration.raindrop_tags": "Etiquetas (separadas por vírgula)",
|
||||
"form.integration.raindrop_token": "Token (teste)",
|
||||
"form.integration.readeck_activate": "Salvar itens no Readeck",
|
||||
"form.integration.readeck_api_key": "Chave de API do Readeck",
|
||||
"form.integration.readeck_endpoint": "Endpoint de API do Readeck",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "Enviar apenas URL (em vez de conteúdo completo)",
|
||||
"form.integration.readwise_activate": "Save entries to Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.readwise_activate": "Salvar itens no Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Token de acesso do Readwise Reader",
|
||||
"form.integration.readwise_api_key_link": "Obtenha seu token de acesso do Readwise",
|
||||
"form.integration.rssbridge_activate": "Verificar RSS-Bridge ao adicionar inscrições",
|
||||
"form.integration.rssbridge_token": "Token de autenticação do RSS-Bridge",
|
||||
"form.integration.rssbridge_url": "URL do servidor RSS-Bridge",
|
||||
"form.integration.shaarli_activate": "Salvar artigos no Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Segredo da API do Shaarli",
|
||||
"form.integration.shaarli_endpoint": "URL do Shaarli",
|
||||
"form.integration.shiori_activate": "Salvar itens no Shiori",
|
||||
"form.integration.shiori_endpoint": "Endpoint da API do Shiori",
|
||||
"form.integration.shiori_password": "Senha do Shiori",
|
||||
@@ -317,11 +325,12 @@
|
||||
"form.integration.webhook_activate": "Enable Webhooks",
|
||||
"form.integration.webhook_secret": "Webhooks Secret",
|
||||
"form.integration.webhook_url": "Default Webhook URL",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.fieldset.application_settings": "Configurações do aplicativo",
|
||||
"form.prefs.fieldset.authentication_settings": "Configurações de autenticação",
|
||||
"form.prefs.fieldset.global_feed_settings": "Configurações globais de fontes",
|
||||
"form.prefs.fieldset.reader_settings": "Configurações do leitor",
|
||||
"form.prefs.help.external_font_hosts": "Lista separada por espaço de hosts de fontes externas permitidos. Por exemplo: 'fonts.gstatic.com fonts.googleapis.com'.",
|
||||
"form.prefs.label.always_open_external_links": "Ler artigos abrindo links externos",
|
||||
"form.prefs.label.categories_sorting_order": "Classificação das categorias",
|
||||
"form.prefs.label.cjk_reading_speed": "Velocidade de leitura para chinês, coreano e japonês (caracteres por minuto)",
|
||||
"form.prefs.label.custom_css": "CSS customizado",
|
||||
@@ -333,15 +342,16 @@
|
||||
"form.prefs.label.entry_order": "Coluna de Ordenação de Entrada",
|
||||
"form.prefs.label.entry_sorting": "Ordenação dos itens",
|
||||
"form.prefs.label.entry_swipe": "Ativar entrada de furto em telas sensíveis ao toque",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.label.external_font_hosts": "Hosts de fontes externas",
|
||||
"form.prefs.label.gesture_nav": "Gesto para navegar entre as entradas",
|
||||
"form.prefs.label.keyboard_shortcuts": "Habilitar atalhos do teclado",
|
||||
"form.prefs.label.language": "Idioma",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Marcar itens como lidos manualmente",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Marcar como lido apenas quando a reprodução de áudio/vídeo atingir 90%% de conclusão",
|
||||
"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_view_or_media_completion": "Marcar itens como lidos quando visualizados. Para áudio/vídeo, marcar como lido em 90%% de conclusão",
|
||||
"form.prefs.label.media_playback_rate": "Velocidade de reprodução do áudio/vídeo",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Abrir links externos em uma nova aba (adiciona target=\"_blank\" aos links)",
|
||||
"form.prefs.label.show_reading_time": "Mostrar tempo estimado de leitura de artigos",
|
||||
"form.prefs.label.theme": "Tema",
|
||||
"form.prefs.label.timezone": "Fuso horário",
|
||||
@@ -402,6 +412,7 @@
|
||||
"page.about.build_date": "Compilado em:",
|
||||
"page.about.credits": "Créditos",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.global_config_options": "opções de configuração global",
|
||||
"page.about.go_version": "Go versão:",
|
||||
"page.about.license": "Licença:",
|
||||
@@ -430,10 +441,10 @@
|
||||
"page.categories.no_feed": "Sem fonte.",
|
||||
"page.categories.title": "Categorias",
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories"
|
||||
"%d categoria",
|
||||
"%d categorias"
|
||||
],
|
||||
"page.category_label": "Category: %s",
|
||||
"page.category_label": "Categoria: %s",
|
||||
"page.edit_category.title": "Editar categoria: %s",
|
||||
"page.edit_feed.etag_header": "Cabeçalho 'ETag':",
|
||||
"page.edit_feed.last_check": "Última verificação:",
|
||||
@@ -448,7 +459,7 @@
|
||||
"%d erros"
|
||||
],
|
||||
"page.feeds.last_check": "Última verificação:",
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.next_check": "Próxima verificação:",
|
||||
"page.feeds.read_counter": "Número de itens lidos",
|
||||
"page.feeds.title": "Fontes",
|
||||
"page.history.title": "Histórico",
|
||||
@@ -496,7 +507,7 @@
|
||||
"page.keyboard_shortcuts.subtitle.sections": "Navegação de seções",
|
||||
"page.keyboard_shortcuts.title": "Atalhos de teclado",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Marcar ou desmarcar como favorito",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Alternar abrir/fechar anexos do item",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "Inverter estado de leitura do item, focar próximo item",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Inverter estado de leitura do item, focar item anterior",
|
||||
"page.login.google_signin": "Iniciar Sessão com sua conta do Google",
|
||||
@@ -512,8 +523,8 @@
|
||||
"page.offline.refresh_page": "Tente atualizar a página",
|
||||
"page.offline.title": "Modo offline",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries"
|
||||
"%d item lido",
|
||||
"%d itens lidos"
|
||||
],
|
||||
"page.search.title": "Resultados da busca",
|
||||
"page.sessions.table.actions": "Ações",
|
||||
@@ -527,35 +538,35 @@
|
||||
"page.settings.title": "Ajustes",
|
||||
"page.settings.unlink_google_account": "Desvincular minha conta do Google",
|
||||
"page.settings.unlink_oidc_account": "Desvincular minha conta do %s",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.added_on": "Added On",
|
||||
"page.settings.webauthn.actions": "Ações",
|
||||
"page.settings.webauthn.added_on": "Adicionado em",
|
||||
"page.settings.webauthn.delete": [
|
||||
"Remover %d senha",
|
||||
"Remover %d senhas"
|
||||
],
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.last_seen_on": "Último uso",
|
||||
"page.settings.webauthn.passkey_name": "Nome da senha",
|
||||
"page.settings.webauthn.passkeys": "Senhas",
|
||||
"page.settings.webauthn.register": "Registrar senha",
|
||||
"page.settings.webauthn.register.error": "Não foi possível registrar a senha",
|
||||
"page.shared_entries.title": "Itens compartilhados",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries"
|
||||
"%d item compartilhado",
|
||||
"%d itens compartilhados"
|
||||
],
|
||||
"page.starred.title": "Favoritos",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries"
|
||||
"%d item favorito",
|
||||
"%d itens favoritos"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total"
|
||||
"%d item no total",
|
||||
"%d itens no total"
|
||||
],
|
||||
"page.unread.title": "Não lidos",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries"
|
||||
"%d item não lido",
|
||||
"%d itens não lidos"
|
||||
],
|
||||
"page.users.actions": "Ações",
|
||||
"page.users.admin.no": "Não",
|
||||
@@ -565,15 +576,15 @@
|
||||
"page.users.never_logged": "Nunca",
|
||||
"page.users.title": "Usuários",
|
||||
"page.users.username": "Nome de usuário",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"pagination.first": "First",
|
||||
"pagination.last": "Last",
|
||||
"page.webauthn_rename.title": "Renomear senha",
|
||||
"pagination.first": "Primeira",
|
||||
"pagination.last": "Última",
|
||||
"pagination.next": "Próximo",
|
||||
"pagination.previous": "Anterior",
|
||||
"search.label": "Buscar",
|
||||
"search.placeholder": "Buscar por...",
|
||||
"search.submit": "Search",
|
||||
"skip_to_content": "Skip to content",
|
||||
"search.submit": "Buscar",
|
||||
"skip_to_content": "Pular para o conteúdo",
|
||||
"time_elapsed.days": [
|
||||
"há %d dia",
|
||||
"há %d dias"
|
||||
@@ -603,4 +614,4 @@
|
||||
"time_elapsed.yesterday": "ontem",
|
||||
"tooltip.keyboard_shortcuts": "Atalho do teclado: %s",
|
||||
"tooltip.logged_user": "Autenticado como %s"
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@
|
||||
"alert.no_tag_entry": "Nu sunt înregistrări pentru această etichetă.",
|
||||
"alert.no_unread_entry": "Nu sunt intrări necitite.",
|
||||
"alert.no_user": "Sunteți singurul utilizator.",
|
||||
"alert.pocket_linked": "Contul dvs. Pocket este atașat!",
|
||||
"alert.prefs_saved": "Preferințe salvate!",
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"Ați activat actualizarea a prea multe fluxuri de informații. Vă rog să așteptați %d minut înainte de a reîncerca.",
|
||||
@@ -74,11 +73,16 @@
|
||||
"entry.state.loading": "Încarc…",
|
||||
"entry.state.saving": "Salvez…",
|
||||
"entry.status.mark_as_read": "Marcați ca citit",
|
||||
"entry.status.mark_as_unread": "Marcați ca necitit",
|
||||
"entry.status.title": "Modifică starea intrării",
|
||||
"entry.status.toast.read": "Marcat ca citit",
|
||||
"entry.status.toast.unread": "Marcat ca necitit",
|
||||
"entry.status.mark_as_unread": "Marcați ca necitit",
|
||||
"entry.tags.label": "Etichete:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Afișează încă o etichetă",
|
||||
"Afișează încă %d etichete",
|
||||
"Afișează încă %d de etichete"
|
||||
],
|
||||
"entry.unshare.label": "Elimină partajarea",
|
||||
"error.api_key_already_exists": "Această cheie API există deja.",
|
||||
"error.bad_credentials": "Utilizator sau parolă invalide.",
|
||||
@@ -116,9 +120,12 @@
|
||||
"error.http_service_unavailable": "Acest site web nu este disponibil momentan din cauza unei erori generată de server. Problema nu este de la Miniflux. Vă rugăm să reîncercați mai târziu.",
|
||||
"error.http_too_many_requests": "Miniflux a generat prea multe solicitări pe acest site web. Vă rog, încercați mai tîrziu sau modificați configurațiile aplicației.",
|
||||
"error.http_unexpected_status_code": "Acest site web nu este disponibil momentan din cauza unei erori HTTP: %d. Problema nu este de la Miniflux. Vă rugăm să reîncercați mai târziu.",
|
||||
"error.invalid_categories_sorting_order": "Ordinea de sortare a categoriilor nu este validă.",
|
||||
"error.invalid_default_home_page": "Pagină de start invalidă!",
|
||||
"error.invalid_display_mode": "Mod invalid de afișare în aplicația web.",
|
||||
"error.invalid_entry_direction": "Direcție invalidă ăn intrare.",
|
||||
"error.invalid_entry_order": "Direcție de sortare invalidă.",
|
||||
"error.invalid_feed_proxy_url": "URL proxy invalid.",
|
||||
"error.invalid_feed_url": "Adresa URL a fluxului este invalidă.",
|
||||
"error.invalid_gesture_nav": "Gest de navigare invalid.",
|
||||
"error.invalid_language": "Limbă invalidă.",
|
||||
@@ -128,13 +135,12 @@
|
||||
"error.network_operation": "Miniflux nu poate ajunge la acest site din cauza unei erori de rețea: %v.",
|
||||
"error.network_timeout": "Acest site web este prea lent și conexiunea nu s-a realizat: %v",
|
||||
"error.password_min_length": "Parola trebuie să aibă cel puțin 6 caractere.",
|
||||
"error.pocket_access_token": "Nu poate obține token-ul de acces de la Pocket!",
|
||||
"error.pocket_request_token": "Nu poate obține token-ul solicitat de la Pocket!",
|
||||
"error.proxy_url_not_empty": "URL-ul proxy nu poate fi gol.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Regulă de bloc invalidă: regulii #%d îi lipsește un nume valid de câmp (Opțiuni: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Regulă de bloc invalidă: modelul regulii #%d's nu este regex valid",
|
||||
"error.settings_block_rule_regex_required": "Regulă de bloc invalidă: modelul regulii #%d's nu este furnizat",
|
||||
"error.settings_block_rule_separator_required": "Regulă de bloc invalidă: modelul regulii #%d's trebuie separat de '='",
|
||||
"error.settings_invalid_domain_list": "Lista domeniilor invalidă. Vă rugăm să ne furnizați o listă separată de virgulă a domeniilor.",
|
||||
"error.settings_invalid_domain_list": "Lista domeniilor este invalidă. Vă rugăm să furnizați o listă de domenii separate prin spațiu.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Regulă Keep invalidă: regulii #%d îi lipsește un nume valid (Opțiuni: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Regulă Keep invalidă: modelul regulii #%d's nu este regex valid",
|
||||
"error.settings_keep_rule_regex_required": "Regulă Keep invalidă: modelul regulii #%d nu este furnizat",
|
||||
@@ -166,7 +172,8 @@
|
||||
"form.feed.fieldset.rules": "Reguli",
|
||||
"form.feed.label.allow_self_signed_certificates": "Permite certificatele auto-semnate sau invalide",
|
||||
"form.feed.label.apprise_service_urls": "Lista de URL-uri ale serviciilor Apprise separate prin virgule",
|
||||
"form.feed.label.blocklist_rules": "Reguli de Blocare",
|
||||
"form.feed.label.block_filter_entry_rules": "Reguli de Blocare a Intrărilor",
|
||||
"form.feed.label.blocklist_rules": "Filtre de Blocare Bazate pe Regex",
|
||||
"form.feed.label.category": "Categorie",
|
||||
"form.feed.label.cookie": "Setare Cookie-uri",
|
||||
"form.feed.label.crawler": "Aduce conținutul original",
|
||||
@@ -176,10 +183,11 @@
|
||||
"form.feed.label.feed_password": "Parolă Flux",
|
||||
"form.feed.label.feed_url": "Flux URL",
|
||||
"form.feed.label.feed_username": "Nume user Flux",
|
||||
"form.feed.label.fetch_via_proxy": "Actualizare prin proxy",
|
||||
"form.feed.label.fetch_via_proxy": "Utilizați proxy-ul configurat la nivelul aplicației",
|
||||
"form.feed.label.hide_globally": "Ascunde intrările în lista globală de articole necitite",
|
||||
"form.feed.label.ignore_http_cache": "Ignoră cache HTTP",
|
||||
"form.feed.label.keeplist_rules": "Reguli de Păstrare",
|
||||
"form.feed.label.keep_filter_entry_rules": "Reguli de Permitere a Intrărilor",
|
||||
"form.feed.label.keeplist_rules": "Filtre de Păstrare Bazate pe Regex",
|
||||
"form.feed.label.no_media_player": "Nu există player media (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Împinge intrările la ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Prioritate predefinită Ntfy",
|
||||
@@ -189,6 +197,7 @@
|
||||
"form.feed.label.ntfy_min_priority": "Prioritate minimă Ntfy",
|
||||
"form.feed.label.ntfy_priority": "Prioritate Ntfy",
|
||||
"form.feed.label.ntfy_topic": "Subiect Ntfy (opțional)",
|
||||
"form.feed.label.proxy_url": "URL Proxy",
|
||||
"form.feed.label.pushover_activate": "Activează Pushover",
|
||||
"form.feed.label.pushover_default_priority": "Prioritate implicită Pushover",
|
||||
"form.feed.label.pushover_high_priority": "Prioritate ridicată Pushover",
|
||||
@@ -196,7 +205,7 @@
|
||||
"form.feed.label.pushover_max_priority": "Prioritate maximă Pushover",
|
||||
"form.feed.label.pushover_min_priority": "Prioritate minimă Pushover",
|
||||
"form.feed.label.pushover_priority": "Prioritate Pushover",
|
||||
"form.feed.label.rewrite_rules": "Rescrie Regulile",
|
||||
"form.feed.label.rewrite_rules": "Reguli de Rescriere a Conținutului",
|
||||
"form.feed.label.scraper_rules": "Reguli de Eliminare",
|
||||
"form.feed.label.site_url": "Adresă URL",
|
||||
"form.feed.label.title": "Titlu",
|
||||
@@ -215,7 +224,7 @@
|
||||
"form.integration.cubox_api_link": "Link APi Cubox",
|
||||
"form.integration.discord_activate": "Împinge intrările pe Discord",
|
||||
"form.integration.discord_webhook_link": "Link Webhook Discord",
|
||||
"form.integration.espial_activate": "Save entries to Espial",
|
||||
"form.integration.espial_activate": "Salvează intrările în Espial",
|
||||
"form.integration.espial_api_key": "Cheie API Espial",
|
||||
"form.integration.espial_endpoint": "Punct acces API Espial",
|
||||
"form.integration.espial_tags": "Etichete Espial",
|
||||
@@ -230,6 +239,9 @@
|
||||
"form.integration.instapaper_activate": "Salvează înregistrările pe Instapaper",
|
||||
"form.integration.instapaper_password": "Parolă Instapaper",
|
||||
"form.integration.instapaper_username": "Utilizator Instapaper",
|
||||
"form.integration.karakeep_activate": "Salvare înregistrări în Karakeep",
|
||||
"form.integration.karakeep_api_key": "Cheie API Karakeep",
|
||||
"form.integration.karakeep_url": "Punct acces API Karakeep",
|
||||
"form.integration.linkace_activate": "Salvează intrările în LinkAce",
|
||||
"form.integration.linkace_api_key": "Cheie API LinkAce",
|
||||
"form.integration.linkace_check_disabled": "Dezactivează verificarea link-urilor",
|
||||
@@ -243,7 +255,7 @@
|
||||
"form.integration.linkding_tags": "TAG-uri Linkding",
|
||||
"form.integration.linkwarden_activate": "Salvează intrările în Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Cheie API Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Endpoint API Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "URL-ul de bază Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Împinge intrările noi pe Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID-ul Camerei Matrix",
|
||||
"form.integration.matrix_bot_password": "Parola utilizatorului Matrix",
|
||||
@@ -270,10 +282,6 @@
|
||||
"form.integration.pinboard_bookmark": "Marchează bookmark ca necitit",
|
||||
"form.integration.pinboard_tags": "Etichete Pinboard",
|
||||
"form.integration.pinboard_token": "Token API Pinboard",
|
||||
"form.integration.pocket_access_token": "Token Acces Pocket",
|
||||
"form.integration.pocket_activate": "Salvează înregistrările în Pocket",
|
||||
"form.integration.pocket_connect_link": "Conectează contul Pocket personal",
|
||||
"form.integration.pocket_consumer_key": "Cheie Consumator Pocket",
|
||||
"form.integration.pushover_activate": "Activează Pushover",
|
||||
"form.integration.pushover_device": "Dispozitiv Pushover (opțional)",
|
||||
"form.integration.pushover_prefix": "Prefix Pushover (opțional)",
|
||||
@@ -292,6 +300,7 @@
|
||||
"form.integration.readwise_api_key": "Token Acces Readwise Reader",
|
||||
"form.integration.readwise_api_key_link": "Obțineți Token-ul de Acess pe Readwise",
|
||||
"form.integration.rssbridge_activate": "Verifică RSS-Bridge la adăugarea de abonamente",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "URL server RSS-Bridge",
|
||||
"form.integration.shaarli_activate": "Salvează articolele în Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Secret API Shaarli",
|
||||
@@ -324,6 +333,7 @@
|
||||
"form.prefs.fieldset.global_feed_settings": "Setări Globale pt. Flux",
|
||||
"form.prefs.fieldset.reader_settings": "Setări Citire",
|
||||
"form.prefs.help.external_font_hosts": "Lista fonturilor de pe gazdă separate de virgulă care poate fi utilizate. De exemplu: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Citește articolele deschizând linkurile externe",
|
||||
"form.prefs.label.categories_sorting_order": "Sortare categorii",
|
||||
"form.prefs.label.cjk_reading_speed": "Viteză de citire pentru Chineză, Coreană și Japoneză (caractere pe minut)",
|
||||
"form.prefs.label.custom_css": "CSS personalizat",
|
||||
@@ -344,6 +354,7 @@
|
||||
"form.prefs.label.mark_read_on_view": "Marchează intrările ca citite la vizualizare",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Marchează intrările ca citite la vizualizare. Pentru audio/video, marchează ca citit la redarea a 90%% de conținut",
|
||||
"form.prefs.label.media_playback_rate": "Viteza de rulare audio/video",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Deschide linkurile externe într-o filă nouă (adaugă target=\"_blank\" la linkuri)",
|
||||
"form.prefs.label.show_reading_time": "Afișare timp estimat de citire pentru înregistrări",
|
||||
"form.prefs.label.theme": "Temă",
|
||||
"form.prefs.label.timezone": "Fus orar",
|
||||
@@ -404,6 +415,7 @@
|
||||
"page.about.build_date": "Dată Build:",
|
||||
"page.about.credits": "Credit",
|
||||
"page.about.db_usage": "Utilizare Bază de Date",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.global_config_options": "Opțiuni globale de configurare",
|
||||
"page.about.go_version": "Versiune Go:",
|
||||
"page.about.license": "Licență:",
|
||||
@@ -423,11 +435,6 @@
|
||||
"page.api_keys.table.last_used_at": "Utilizat ultima dată",
|
||||
"page.api_keys.table.token": "Token",
|
||||
"page.api_keys.title": "Chei API",
|
||||
"page.categories_count": [
|
||||
"%d categorie",
|
||||
"%d categorii",
|
||||
"%d categorie găsită"
|
||||
],
|
||||
"page.categories.entries": "Intrări",
|
||||
"page.categories.feed_count": [
|
||||
"Este %d flux.",
|
||||
@@ -437,6 +444,11 @@
|
||||
"page.categories.feeds": "Fluxuri",
|
||||
"page.categories.no_feed": "Nici un flux.",
|
||||
"page.categories.title": "Categorii",
|
||||
"page.categories_count": [
|
||||
"%d categorie",
|
||||
"%d categorii",
|
||||
"%d categorie găsită"
|
||||
],
|
||||
"page.category_label": "Categorie: %s",
|
||||
"page.edit_category.title": "Editare Categorie: %s",
|
||||
"page.edit_feed.etag_header": "Antet ETag:",
|
||||
@@ -545,29 +557,29 @@
|
||||
"page.settings.webauthn.passkeys": "Chei Acces",
|
||||
"page.settings.webauthn.register": "Înregistrare cheie acces",
|
||||
"page.settings.webauthn.register.error": "Eroare la înregistrarea cheii de acces",
|
||||
"page.shared_entries.title": "Înregistrări partajate",
|
||||
"page.shared_entries_count": [
|
||||
"%d înregistrare partajată",
|
||||
"%d înregistrări partajate",
|
||||
"%d înregistrări partajate"
|
||||
],
|
||||
"page.shared_entries.title": "Înregistrări partajate",
|
||||
"page.starred.title": "Marcate",
|
||||
"page.starred_entry_count": [
|
||||
"%d înregistrare marcată",
|
||||
"%d Înregistrări marcate",
|
||||
"%d Înregistrări marcate"
|
||||
],
|
||||
"page.starred.title": "Marcate",
|
||||
"page.total_entry_count": [
|
||||
"%d intrare în total",
|
||||
"%d intrări în total",
|
||||
"%d intrări în total"
|
||||
],
|
||||
"page.unread.title": "Necitite",
|
||||
"page.unread_entry_count": [
|
||||
"%d înregistrare necitită",
|
||||
"%d înregistrări necitite",
|
||||
"%d înregistrări necitite"
|
||||
],
|
||||
"page.unread.title": "Necitite",
|
||||
"page.users.actions": "Acțiuni",
|
||||
"page.users.admin.no": "Nu",
|
||||
"page.users.admin.yes": "Da",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"action.update": "Обновить",
|
||||
"alert.account_linked": "Ваш внешний аккаунт теперь привязан!",
|
||||
"alert.account_unlinked": "Ваш внешний аккаунт теперь отвязан!",
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"alert.background_feed_refresh": "Все подписки обновляются в фоновом режиме. Вы можете продолжать использовать Miniflux пока идёт этот процесс.",
|
||||
"alert.feed_error": "С этой подпиской есть проблема",
|
||||
"alert.no_bookmark": "Избранное отсутствует.",
|
||||
"alert.no_category": "Категории отсутствуют.",
|
||||
@@ -27,27 +27,26 @@
|
||||
"alert.no_tag_entry": "Нет записей, соответствующих этому тегу.",
|
||||
"alert.no_unread_entry": "Нет непрочитанных статей.",
|
||||
"alert.no_user": "Вы единственный пользователь.",
|
||||
"alert.pocket_linked": "Ваш Pocket аккаунт теперь привязан!",
|
||||
"alert.prefs_saved": "Предпочтения сохранены!",
|
||||
"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."
|
||||
"Вы запустили слишком много обновлений подписок. Подождите %d минуту для нового запуска",
|
||||
"Вы запустили слишком много обновлений подписок. Подождите %d минут для нового запуска",
|
||||
"Вы запустили слишком много обновлений подписок. Подождите %d минут для нового запуска"
|
||||
],
|
||||
"confirm.loading": "В процессе…",
|
||||
"confirm.no": "нет",
|
||||
"confirm.question": "Вы уверены?",
|
||||
"confirm.question.refresh": "Вы хотите выполнить принудительное обновление?",
|
||||
"confirm.yes": "да",
|
||||
"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.reset": "Reset",
|
||||
"enclosure_media_controls.speed.reset.title": "Reset speed to 1x",
|
||||
"enclosure_media_controls.speed.slower": "Slower",
|
||||
"enclosure_media_controls.speed.slower.title": "Slower by %sx",
|
||||
"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": "Ускорить в %s раз",
|
||||
"enclosure_media_controls.speed.reset": "Сбросить",
|
||||
"enclosure_media_controls.speed.reset.title": "Сбросить скорость до 1x",
|
||||
"enclosure_media_controls.speed.slower": "Медленнее",
|
||||
"enclosure_media_controls.speed.slower.title": "Замедлить в %s раз",
|
||||
"entry.bookmark.toast.off": "Без пометок",
|
||||
"entry.bookmark.toast.on": "Помеченные",
|
||||
"entry.bookmark.toggle.off": "Удалить из Избранного",
|
||||
@@ -74,83 +73,90 @@
|
||||
"entry.state.loading": "Загрузка…",
|
||||
"entry.state.saving": "Сохранение…",
|
||||
"entry.status.mark_as_read": "Отметить как прочитанное",
|
||||
"entry.status.mark_as_unread": "Пометить как непрочитанное",
|
||||
"entry.status.title": "Изменить статус записи",
|
||||
"entry.status.toast.read": "Помечено как прочитанное",
|
||||
"entry.status.toast.unread": "Помечено как непрочитанное",
|
||||
"entry.status.mark_as_unread": "Пометить как непрочитанное",
|
||||
"entry.tags.label": "Теги:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Ещё %d тег",
|
||||
"Ещё %d тега",
|
||||
"Ещё %d тегов"
|
||||
],
|
||||
"entry.unshare.label": "Удалить из общедоступных",
|
||||
"error.api_key_already_exists": "Этот API-ключ уже существует.",
|
||||
"error.bad_credentials": "Неверное имя пользователя или пароль.",
|
||||
"error.category_already_exists": "Эта категория уже существует.",
|
||||
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||
"error.database_error": "Database error: %v.",
|
||||
"error.category_not_found": "Эта категория не существует или не принадлежит этому пользователю.",
|
||||
"error.database_error": "Ошибка базы данных: %v.",
|
||||
"error.different_passwords": "Пароли не совпадают.",
|
||||
"error.duplicate_fever_username": "Уже есть кто-то с таким же именем пользователя Fever!",
|
||||
"error.duplicate_googlereader_username": "Уже есть кто-то с таким же именем пользователя Google Reader!",
|
||||
"error.duplicate_linked_account": "Уже есть кто-то, кто ассоциирован с этим аккаунтом!",
|
||||
"error.duplicated_feed": "This feed already exists.",
|
||||
"error.duplicated_feed": "Эта подписка уже существует.",
|
||||
"error.empty_file": "Этот файл пуст.",
|
||||
"error.entries_per_page_invalid": "Недопустимое значение количества записей на странице.",
|
||||
"error.feed_already_exists": "Эта подписка уже существует.",
|
||||
"error.feed_category_not_found": "Эта категория не существует или не принадлежит этому пользователю.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"error.feed_format_not_detected": "Не удалось определить формат подписки: %v.",
|
||||
"error.feed_invalid_blocklist_rule": "Правило черного списка некорректно.",
|
||||
"error.feed_invalid_keeplist_rule": "Правило белого списка некорректно.",
|
||||
"error.feed_mandatory_fields": "Ссылка и категория обязательны.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.feed_not_found": "Эта подписка не существует или не принадлежит этому пользователю.",
|
||||
"error.feed_title_not_empty": "Заголовок подписки не может быть пустым.",
|
||||
"error.feed_url_not_empty": "URL-адрес подписки не может быть пустым.",
|
||||
"error.fields_mandatory": "Все поля обязательны.",
|
||||
"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_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_client_error": "HTTP client error: %v.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||
"error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).",
|
||||
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_bad_gateway": "В данный момент сайт недоступен из-за ошибки шлюза. Проблема не связана с Miniflux. Пожалуйста, попробуйте позже.",
|
||||
"error.http_body_read": "Невозможно прочитать тело HTTP-сообщения: %v.",
|
||||
"error.http_client_error": "Ошибка HTTP-клиента: %v.",
|
||||
"error.http_empty_response": "Пустой ответ HTTP. Возможно этот сайт использует защиту от ботов?",
|
||||
"error.http_empty_response_body": "Пустое тело HTTP-ответа.",
|
||||
"error.http_forbidden": "Доступ к сайту запрещён. Возможно этот сайт использует защиту от ботов?",
|
||||
"error.http_gateway_timeout": "В данный момент сайт недоступен из-за превышения времени ожидания ответа от шлюза. Проблема не связана с Miniflux. Пожалуйста, попробуйте позже.",
|
||||
"error.http_internal_server_error": "В данный момент сайт недоступен из-за ошибки сервера. Проблема не связана с Miniflux. Пожалуйста, попробуйте позже.",
|
||||
"error.http_not_authorized": "Доступ к сайту запрещён. Возможно используется неправильное имя пользователя или пароль.",
|
||||
"error.http_resource_not_found": "Запрашиваемый ресурс не найден. Пожалуйста, проверьте URL.",
|
||||
"error.http_response_too_large": "Превышен размер HTTP-ответа. Вы можете увеличить лимит размера HTTP-ответа в настройках (для применения новых настроек потребуется перезагрузка приложения).",
|
||||
"error.http_service_unavailable": "В данный момент сайт недоступен из-за ошибки сервера. Проблема не связана с Miniflux. Пожалуйста, попробуйте позже.",
|
||||
"error.http_too_many_requests": "Miniflux отправил слишком много запросов к этому сайту. Пожалуйста, попробуйте позже или измените настройки приложения.",
|
||||
"error.http_unexpected_status_code": "В данный момент сайт недоступен из-за непредвиденного кода HTTP-ответа: %d. Проблема не связана с Miniflux. Пожалуйста, попробуйте позже.",
|
||||
"error.invalid_categories_sorting_order": "Недопустимый порядок сортировки категорий.",
|
||||
"error.invalid_default_home_page": "Недопустимая домашняя страница по умолчанию!",
|
||||
"error.invalid_display_mode": "Недопустимый режим отображения веб-приложения.",
|
||||
"error.invalid_entry_direction": "Недопустимая сортировка записей.",
|
||||
"error.invalid_entry_order": "Недопустимый порядок статей.",
|
||||
"error.invalid_feed_proxy_url": "Недействительный URL прокси.",
|
||||
"error.invalid_feed_url": "Недействительная ссылка подписки.",
|
||||
"error.invalid_gesture_nav": "Недопустимая навигация жестами.",
|
||||
"error.invalid_language": "Недопустимый язык.",
|
||||
"error.invalid_site_url": "Недействительный ссылка сайта.",
|
||||
"error.invalid_theme": "Недопустимая тема.",
|
||||
"error.invalid_timezone": "Недопустымый часовой пояс.",
|
||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||
"error.invalid_timezone": "Недопустимый часовой пояс.",
|
||||
"error.network_operation": "Miniflux не может открыть сайт из-за ошибки сети: %v.",
|
||||
"error.network_timeout": "Этот сайт слишком медленный и время ожидания запроса истекло: %v",
|
||||
"error.password_min_length": "Вы должны использовать минимум 6 символов.",
|
||||
"error.pocket_access_token": "Не удалось получить ключ доступа от Pocket!",
|
||||
"error.pocket_request_token": "Не удалось получить request token от Pocket!",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.proxy_url_not_empty": "URL прокси не может быть пустым.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Недопустимое правило блокировки: у правила #%d отсутствует корректное имя поля (Возможные варианты: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Недопустимое правило блокировки: шаблон правила #%d не является корректным регулярным выражением",
|
||||
"error.settings_block_rule_regex_required": "Недопустимое правило блокировки: не указан шаблон для правила #%d",
|
||||
"error.settings_block_rule_separator_required": "Недопустимое правило блокировки: шаблон правила #%d должен быть отделен символом '='",
|
||||
"error.settings_invalid_domain_list": "Недопустимый список доменов. Пожалуйста, укажите список доменов, разделенных пробелами.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Недопустимое правило сохранения: у правила #%d отсутствует корректное имя поля (Возможные варианты: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Недопустимое правило сохранения: шаблон правила #%d не является корректным регулярным выражением",
|
||||
"error.settings_keep_rule_regex_required": "Недопустимое правило сохранения: не указан шаблон для правила #%d",
|
||||
"error.settings_keep_rule_separator_required": "Недопустимое правило сохранения: шаблон правила #%d должен быть отделен символом '='",
|
||||
"error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.",
|
||||
"error.settings_media_playback_rate_range": "Скорость воспроизведения выходит за пределы диапазона",
|
||||
"error.settings_reading_speed_is_positive": "Скорость чтения должна быть целым положительным числом.",
|
||||
"error.site_url_not_empty": "Ссылка на сайт не может быть пустой.",
|
||||
"error.subscription_not_found": "Не удалось найти подписки.",
|
||||
"error.title_required": "Название обязательно.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "Ошибка TLS: %q. Вы можете отключить проверку TLS в настройках подписки.",
|
||||
"error.unable_to_create_api_key": "Невозможно создать этот API-ключ.",
|
||||
"error.unable_to_create_category": "Не удалось создать эту категорию.",
|
||||
"error.unable_to_create_user": "Не удалось создать этого пользователя.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.unable_to_detect_rssbridge": "Не удалось обнаружить подписку с помощью RSS-Bridge: %v.",
|
||||
"error.unable_to_parse_feed": "Не удалось обработать эту подписку: %v.",
|
||||
"error.unable_to_update_category": "Не удалось обновить эту категорию.",
|
||||
"error.unable_to_update_feed": "Не удалось обновить эту подписку.",
|
||||
"error.unable_to_update_user": "Не удалось обновить этого пользователя.",
|
||||
@@ -160,59 +166,62 @@
|
||||
"form.api_key.label.description": "Описание API-ключа",
|
||||
"form.category.hide_globally": "Скрыть записи в глобальном списке непрочитанных",
|
||||
"form.category.label.title": "Название",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.integration": "Third-Party Services",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.general": "Общие",
|
||||
"form.feed.fieldset.integration": "Сторонние сервисы",
|
||||
"form.feed.fieldset.network_settings": "Настройки сети",
|
||||
"form.feed.fieldset.rules": "Правила",
|
||||
"form.feed.label.allow_self_signed_certificates": "Разрешить самоподписанные или недействительные сертификаты",
|
||||
"form.feed.label.apprise_service_urls": "Список ссылок сервисов Apprise, разделенный запятой",
|
||||
"form.feed.label.blocklist_rules": "Правила черного списка",
|
||||
"form.feed.label.block_filter_entry_rules": "Правила блокировки записей",
|
||||
"form.feed.label.blocklist_rules": "Фильтры блокировки на основе регулярных выражений",
|
||||
"form.feed.label.category": "Категория",
|
||||
"form.feed.label.cookie": "Установить куки",
|
||||
"form.feed.label.crawler": "Извлечь оригинальное содержимое",
|
||||
"form.feed.label.description": "Описание",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.disable_http2": "Отключить HTTP/2 для предотвращения фингерпринтинга",
|
||||
"form.feed.label.disabled": "Не обновлять эту подписку",
|
||||
"form.feed.label.feed_password": "Пароль подписки",
|
||||
"form.feed.label.feed_url": "Адрес подписки",
|
||||
"form.feed.label.feed_username": "Имя пользователя подписки",
|
||||
"form.feed.label.fetch_via_proxy": "Использовать прокси",
|
||||
"form.feed.label.fetch_via_proxy": "Использовать прокси, настроенный на уровне приложения",
|
||||
"form.feed.label.hide_globally": "Скрыть записи в глобальном списке непрочитанных",
|
||||
"form.feed.label.ignore_http_cache": "Игнорировать HTTP кеш",
|
||||
"form.feed.label.keeplist_rules": "Правила белого списка",
|
||||
"form.feed.label.keep_filter_entry_rules": "Правила разрешения записей",
|
||||
"form.feed.label.keeplist_rules": "Фильтры сохранения на основе регулярных выражений",
|
||||
"form.feed.label.no_media_player": "Отключить медиаплеер (аудио и видео)",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "Правила перезаписи",
|
||||
"form.feed.label.ntfy_activate": "Отправлять статьи в ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "По умолчанию",
|
||||
"form.feed.label.ntfy_high_priority": "Высший",
|
||||
"form.feed.label.ntfy_low_priority": "Низкий",
|
||||
"form.feed.label.ntfy_max_priority": "Высокий",
|
||||
"form.feed.label.ntfy_min_priority": "Минимальный",
|
||||
"form.feed.label.ntfy_priority": "Приоритет ntfy",
|
||||
"form.feed.label.ntfy_topic": "Топик ntfy (опционально)",
|
||||
"form.feed.label.proxy_url": "URL прокси",
|
||||
"form.feed.label.pushover_activate": "Отправлять статьи в pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "По умолчанию",
|
||||
"form.feed.label.pushover_high_priority": "Высокий",
|
||||
"form.feed.label.pushover_low_priority": "Низкий",
|
||||
"form.feed.label.pushover_max_priority": "Высший",
|
||||
"form.feed.label.pushover_min_priority": "Минимальный",
|
||||
"form.feed.label.pushover_priority": "Приоритет сообщений Pushover",
|
||||
"form.feed.label.rewrite_rules": "Правила переписывания содержимого",
|
||||
"form.feed.label.scraper_rules": "Правила сборщика",
|
||||
"form.feed.label.site_url": "Адрес сайта",
|
||||
"form.feed.label.title": "Название",
|
||||
"form.feed.label.urlrewrite_rules": "Правила перезаписи URL",
|
||||
"form.feed.label.user_agent": "Переопределить User-Agent по умолчанию",
|
||||
"form.feed.label.webhook_url": "Override webhook url",
|
||||
"form.feed.label.webhook_url": "Переопределить URL вебхука",
|
||||
"form.import.label.file": "OPML файл",
|
||||
"form.import.label.url": "Ссылка",
|
||||
"form.integration.apprise_activate": "Отправить статьи в Apprise",
|
||||
"form.integration.apprise_services_url": "Список ссылок сервисов Apprise, разделенный запятой",
|
||||
"form.integration.apprise_url": "Ссылка на Apprise API",
|
||||
"form.integration.betula_activate": "Сохранять статьи в Бетулу",
|
||||
"form.integration.betula_token": "Токен Бетулы",
|
||||
"form.integration.betula_url": "Адрес сервера Бетулы",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.integration.betula_activate": "Сохранять статьи в Betula",
|
||||
"form.integration.betula_token": "Токен Betula",
|
||||
"form.integration.betula_url": "Адрес сервера Betula",
|
||||
"form.integration.cubox_activate": "Сохранять статьи в Cubox",
|
||||
"form.integration.cubox_api_link": "Ссылка на Cubox API",
|
||||
"form.integration.discord_activate": "Отправить статьи в Discord",
|
||||
"form.integration.discord_webhook_link": "Ссылка на Discord Webhook",
|
||||
"form.integration.espial_activate": "Сохранять статьи в Espial",
|
||||
@@ -230,12 +239,15 @@
|
||||
"form.integration.instapaper_activate": "Сохранять статьи в Instapaper",
|
||||
"form.integration.instapaper_password": "Пароль Instapaper",
|
||||
"form.integration.instapaper_username": "Имя пользователя Instapaper",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_is_private": "Mark link as private",
|
||||
"form.integration.linkace_tags": "LinkAce Tags",
|
||||
"form.integration.karakeep_activate": "Сохранять статьи в Karakeep",
|
||||
"form.integration.karakeep_api_key": "API-ключ Karakeep",
|
||||
"form.integration.karakeep_url": "Конечная точка Karakeep API",
|
||||
"form.integration.linkace_activate": "Сохранять статьи в LinkAce",
|
||||
"form.integration.linkace_api_key": "API-ключ LinkAce",
|
||||
"form.integration.linkace_check_disabled": "Отключить проверку ссылок",
|
||||
"form.integration.linkace_endpoint": "Конечная точка LinkAce API",
|
||||
"form.integration.linkace_is_private": "Отмечать ссылки как приватные",
|
||||
"form.integration.linkace_tags": "Теги LinkAce",
|
||||
"form.integration.linkding_activate": "Сохранять статьи в Linkding",
|
||||
"form.integration.linkding_api_key": "API-ключ Linkding",
|
||||
"form.integration.linkding_bookmark": "Помечать закладки как непрочитанное",
|
||||
@@ -243,8 +255,8 @@
|
||||
"form.integration.linkding_tags": "Теги Linkding",
|
||||
"form.integration.linkwarden_activate": "Сохранять статьи в Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "API-ключ Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Конечная точка Linkwarden API",
|
||||
"form.integration.matrix_bot_activate": "Репостить новые статьи в Matrix",
|
||||
"form.integration.linkwarden_endpoint": "Базовый URL-адрес Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Отправлять статьи в Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "ID комнаты Matrix",
|
||||
"form.integration.matrix_bot_password": "Пароль пользователя Matrix",
|
||||
"form.integration.matrix_bot_url": "Ссылка на сервер Matrix",
|
||||
@@ -252,14 +264,14 @@
|
||||
"form.integration.notion_activate": "Сохранить статьи в Notion",
|
||||
"form.integration.notion_page_id": "Идентификатор страницы Notion",
|
||||
"form.integration.notion_token": "Секретный токен Notion",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_topic": "Ntfy topic (default if not set in feed)",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_activate": "Отправлять статьи в ntfy",
|
||||
"form.integration.ntfy_api_token": "API-токен ntfy (опционально)",
|
||||
"form.integration.ntfy_icon_url": "URL иконки ntfy (опционально)",
|
||||
"form.integration.ntfy_internal_links": "Использовать внутренние ссылки по клику (опционально)",
|
||||
"form.integration.ntfy_password": "Пароль ntfy (опционально)",
|
||||
"form.integration.ntfy_topic": "Тема ntfy (по умолчанию, если не задана в подписке)",
|
||||
"form.integration.ntfy_url": "URL ntfy (опционально, по умолчанию ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Имя пользователя ntfy (опционально)",
|
||||
"form.integration.nunux_keeper_activate": "Сохранять статьи в Nunux Keeper",
|
||||
"form.integration.nunux_keeper_api_key": "API-ключ Nunux Keeper",
|
||||
"form.integration.nunux_keeper_endpoint": "Конечная точка Nunux Keeper API",
|
||||
@@ -270,19 +282,15 @@
|
||||
"form.integration.pinboard_bookmark": "Помечать закладки как непрочитанное",
|
||||
"form.integration.pinboard_tags": "Теги Pinboard",
|
||||
"form.integration.pinboard_token": "Токен Pinboard API",
|
||||
"form.integration.pocket_access_token": "Ключ доступа к Pocket",
|
||||
"form.integration.pocket_activate": "Сохранять статьи в Pocket",
|
||||
"form.integration.pocket_connect_link": "Подключить аккаунт Pocket",
|
||||
"form.integration.pocket_consumer_key": "Ключ пользователя Pocket",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
"form.integration.pushover_token": "Pushover application API token",
|
||||
"form.integration.pushover_user": "Pushover user key",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.pushover_activate": "Отправлять статьи Pushover",
|
||||
"form.integration.pushover_device": "Устройство Pushover (опционально)",
|
||||
"form.integration.pushover_prefix": "URL-префикс Pushover (опционально)",
|
||||
"form.integration.pushover_token": "API-токен приложения Pushover",
|
||||
"form.integration.pushover_user": "Пользовательский ключ Pushover",
|
||||
"form.integration.raindrop_activate": "Сохранять статьи в Raindrop",
|
||||
"form.integration.raindrop_collection_id": "ID коллекции",
|
||||
"form.integration.raindrop_tags": "Теги (через запятую)",
|
||||
"form.integration.raindrop_token": "Токен (тестовый)",
|
||||
"form.integration.readeck_activate": "Сохранять статьи в Readeck",
|
||||
"form.integration.readeck_api_key": "API-ключ Readeck",
|
||||
"form.integration.readeck_endpoint": "Конечная точка Readeck API",
|
||||
@@ -291,8 +299,9 @@
|
||||
"form.integration.readwise_activate": "Сохранить статьи в Readwise",
|
||||
"form.integration.readwise_api_key": "Токен доступа в Readwise",
|
||||
"form.integration.readwise_api_key_link": "Получить токен доступа Readwise",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.rssbridge_activate": "Проверять RSS-Bridge при добавлении подписок",
|
||||
"form.integration.rssbridge_token": "Токен аутентификации RSS-Bridge",
|
||||
"form.integration.rssbridge_url": "URL сервера RSS-Bridge",
|
||||
"form.integration.shaarli_activate": "Сохранить статьи в Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Секретный ключ Shaarli API",
|
||||
"form.integration.shaarli_endpoint": "Ссылка Shaarli",
|
||||
@@ -302,13 +311,13 @@
|
||||
"form.integration.shiori_username": "Имя пользователя Shiori",
|
||||
"form.integration.slack_activate": "Отправить статьи в Slack",
|
||||
"form.integration.slack_webhook_link": "Ссылка на Slack Webhook",
|
||||
"form.integration.telegram_bot_activate": "Репостить новые статьи в Telegram-чат",
|
||||
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||
"form.integration.telegram_bot_disable_notification": "Disable notification",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
|
||||
"form.integration.telegram_bot_activate": "Отправлять статьи в Telegram-чат",
|
||||
"form.integration.telegram_bot_disable_buttons": "Отключить кнопки",
|
||||
"form.integration.telegram_bot_disable_notification": "Отключить уведомления",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Отключить предпросмотр веб-страниц",
|
||||
"form.integration.telegram_bot_token": "Токен бота",
|
||||
"form.integration.telegram_chat_id": "ID чата",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
"form.integration.telegram_topic_id": "ID топика",
|
||||
"form.integration.wallabag_activate": "Сохранять статьи в Wallabag",
|
||||
"form.integration.wallabag_client_id": "Номер клиента Wallabag",
|
||||
"form.integration.wallabag_client_secret": "Секретный код клиента Wallabag",
|
||||
@@ -319,11 +328,12 @@
|
||||
"form.integration.webhook_activate": "Включить вебхуки",
|
||||
"form.integration.webhook_secret": "Секретный ключ для вебхуков",
|
||||
"form.integration.webhook_url": "Адрес вебхуков",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.fieldset.application_settings": "Настройки приложения",
|
||||
"form.prefs.fieldset.authentication_settings": "Настройки аутентификации",
|
||||
"form.prefs.fieldset.global_feed_settings": "Глобальные настройки подписок",
|
||||
"form.prefs.fieldset.reader_settings": "Настройки чтения",
|
||||
"form.prefs.help.external_font_hosts": "Список разрешённых внешних хостов для шрифтов, разделенных пробелами. Например: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.label.always_open_external_links": "Читать статьи, открывая внешние ссылки",
|
||||
"form.prefs.label.categories_sorting_order": "Сортировка категорий",
|
||||
"form.prefs.label.cjk_reading_speed": "Скорость чтения на китайском, корейском и японском языках (знаков в минуту)",
|
||||
"form.prefs.label.custom_css": "Пользовательский CSS",
|
||||
@@ -335,15 +345,16 @@
|
||||
"form.prefs.label.entry_order": "Столбец сортировки статей",
|
||||
"form.prefs.label.entry_sorting": "Сортировка статей",
|
||||
"form.prefs.label.entry_swipe": "Включить пролистывание свайпом на сенсорных экранах",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.label.external_font_hosts": "Внешние хосты шрифтов",
|
||||
"form.prefs.label.gesture_nav": "Жест для перехода между статьями",
|
||||
"form.prefs.label.keyboard_shortcuts": "Включить горячие клавиши",
|
||||
"form.prefs.label.language": "Язык",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Отмечать статьи как прочитанные вручную",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Отмечать как прочитанное только когда воспроизведение аудио/видео достигает 90%% завершения",
|
||||
"form.prefs.label.mark_read_on_view": "Автоматически отмечать записи как прочитанные при просмотре",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Отмечать статьи как прочитанные при просмотре. Для аудио/видео - при 90%% завершения воспроизведения",
|
||||
"form.prefs.label.media_playback_rate": "Скорость воспроизведения аудио/видео",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Открывать внешние ссылки в новой вкладке (добавляет target=\"_blank\" к ссылкам)",
|
||||
"form.prefs.label.show_reading_time": "Показать примерное время чтения статей",
|
||||
"form.prefs.label.theme": "Тема",
|
||||
"form.prefs.label.timezone": "Часовой пояс",
|
||||
@@ -380,7 +391,7 @@
|
||||
"menu.feeds": "Подписки",
|
||||
"menu.flush_history": "Очистить историю",
|
||||
"menu.history": "История",
|
||||
"menu.home_page": "Home page",
|
||||
"menu.home_page": "Главная",
|
||||
"menu.import": "Импорт",
|
||||
"menu.integrations": "Интеграции",
|
||||
"menu.logout": "Выйти",
|
||||
@@ -397,17 +408,18 @@
|
||||
"menu.show_only_starred_entries": "Показывать только избранные статьи",
|
||||
"menu.show_only_unread_entries": "Показывать только непрочитанные статьи",
|
||||
"menu.starred": "Избранное",
|
||||
"menu.title": "Menu",
|
||||
"menu.title": "Меню",
|
||||
"menu.unread": "Непрочитанное",
|
||||
"menu.users": "Пользователи",
|
||||
"page.about.author": "Автор:",
|
||||
"page.about.build_date": "Дата сборки:",
|
||||
"page.about.credits": "Авторы",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.db_usage": "Размер базы данных:",
|
||||
"page.about.git_commit": "Git-коммит:",
|
||||
"page.about.global_config_options": "Глобальные параметры конфигурации",
|
||||
"page.about.go_version": "Версия Go:",
|
||||
"page.about.license": "Лицензия:",
|
||||
"page.about.postgres_version": "Версия Postgres:",
|
||||
"page.about.postgres_version": "Версия PostgreSQL:",
|
||||
"page.about.title": "О приложении",
|
||||
"page.about.version": "Версия:",
|
||||
"page.add_feed.choose_feed": "Выберите подписку",
|
||||
@@ -423,7 +435,7 @@
|
||||
"page.api_keys.table.last_used_at": "Последнее использование",
|
||||
"page.api_keys.table.token": "Токен",
|
||||
"page.api_keys.title": "API-ключи",
|
||||
"page.categories.entries": "Cтатьи",
|
||||
"page.categories.entries": "Статьи",
|
||||
"page.categories.feed_count": [
|
||||
"Есть %d подписка.",
|
||||
"Есть %d подписки.",
|
||||
@@ -433,11 +445,11 @@
|
||||
"page.categories.no_feed": "Нет подписок.",
|
||||
"page.categories.title": "Категории",
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories",
|
||||
"%d categories"
|
||||
"%d категория",
|
||||
"%d категории",
|
||||
"%d категорий"
|
||||
],
|
||||
"page.category_label": "Category: %s",
|
||||
"page.category_label": "Категории: %s",
|
||||
"page.edit_category.title": "Изменить категорию: %s",
|
||||
"page.edit_feed.etag_header": "Заголовок ETag:",
|
||||
"page.edit_feed.last_check": "Последняя проверка:",
|
||||
@@ -452,8 +464,8 @@
|
||||
"%d ошибки",
|
||||
"%d ошибок"
|
||||
],
|
||||
"page.feeds.last_check": "Последняя проверка:",
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.last_check": "Последнее обновление:",
|
||||
"page.feeds.next_check": "Следующее обновление:",
|
||||
"page.feeds.read_counter": "Количество прочитанных статей",
|
||||
"page.feeds.title": "Подписки",
|
||||
"page.history.title": "История",
|
||||
@@ -462,7 +474,7 @@
|
||||
"page.integration.bookmarklet.help": "Эта специальная ссылка позволит вам подписаться на сайт, используя обыкновенную закладку в вашем браузере.",
|
||||
"page.integration.bookmarklet.instructions": "Перетащите эту ссылку в ваши закладки.",
|
||||
"page.integration.bookmarklet.name": "Добавить в Miniflux",
|
||||
"page.integration.miniflux_api": "Miniflux API",
|
||||
"page.integration.miniflux_api": "API Miniflux",
|
||||
"page.integration.miniflux_api_endpoint": "Конечная точка API",
|
||||
"page.integration.miniflux_api_password": "Пароль",
|
||||
"page.integration.miniflux_api_password_value": "Пароль вашего аккаунта",
|
||||
@@ -509,7 +521,7 @@
|
||||
"page.login.title": "Войти",
|
||||
"page.login.webauthn_login": "Войти с паролем",
|
||||
"page.login.webauthn_login.error": "Невозможно войти с паролем",
|
||||
"page.login.webauthn_login.help": "Please enter your username if you're using a security key. This is not required if you are using a Passkey (discoverable credentials).",
|
||||
"page.login.webauthn_login.help": "Пожалуйста, введите имя пользователя, если вы используете ключ безопасности. Это не требуется при использовании Passkey (обнаруживаемые учетные данные).",
|
||||
"page.new_api_key.title": "Новый API-ключ",
|
||||
"page.new_category.title": "Новая категория",
|
||||
"page.new_user.title": "Новый пользователь",
|
||||
@@ -517,9 +529,9 @@
|
||||
"page.offline.refresh_page": "Попробуйте обновить страницу",
|
||||
"page.offline.title": "Автономный режим",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries",
|
||||
"%d read entries"
|
||||
"%d прочитанная статья",
|
||||
"%d прочитанных статьи",
|
||||
"%d прочитанных статей"
|
||||
],
|
||||
"page.search.title": "Результаты поиска",
|
||||
"page.sessions.table.actions": "Действия",
|
||||
@@ -533,40 +545,40 @@
|
||||
"page.settings.title": "Настройки",
|
||||
"page.settings.unlink_google_account": "Отвязать мой Google аккаунт",
|
||||
"page.settings.unlink_oidc_account": "Отвязать мой %s аккаунт",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.added_on": "Added On",
|
||||
"page.settings.webauthn.actions": "Действия",
|
||||
"page.settings.webauthn.added_on": "Добавлен",
|
||||
"page.settings.webauthn.delete": [
|
||||
"Удалить %d пароль",
|
||||
"Удалить %d пароля",
|
||||
"Удалить %d пароля"
|
||||
],
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.last_seen_on": "Последнее использование",
|
||||
"page.settings.webauthn.passkey_name": "Название ключа доступа",
|
||||
"page.settings.webauthn.passkeys": "Ключи доступа",
|
||||
"page.settings.webauthn.register": "Зарегистрировать пароль",
|
||||
"page.settings.webauthn.register.error": "Не удается зарегистрировать пароль",
|
||||
"page.shared_entries.title": "Общедоступные статьи",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries",
|
||||
"%d shared entries"
|
||||
"%d общедоступная статья",
|
||||
"%d общедоступных статьи",
|
||||
"%d общедоступных статей"
|
||||
],
|
||||
"page.starred.title": "Избранное",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries",
|
||||
"%d starred entries"
|
||||
"%d избранная статья",
|
||||
"%d избранные статьи",
|
||||
"%d избранных статей"
|
||||
],
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total",
|
||||
"%d entries in total"
|
||||
"%d статья всего",
|
||||
"%d статьи всего",
|
||||
"%d статей всего"
|
||||
],
|
||||
"page.unread.title": "Непрочитанное",
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries",
|
||||
"%d unread entries"
|
||||
"%d непрочитанная статья",
|
||||
"%d непрочитанных статьи",
|
||||
"%d непрочитанных статей"
|
||||
],
|
||||
"page.users.actions": "Действия",
|
||||
"page.users.admin.no": "Нет",
|
||||
@@ -576,15 +588,15 @@
|
||||
"page.users.never_logged": "Никогда",
|
||||
"page.users.title": "Пользователи",
|
||||
"page.users.username": "Имя пользователя",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"pagination.first": "First",
|
||||
"pagination.last": "Last",
|
||||
"page.webauthn_rename.title": "Переименовать ключ доступа",
|
||||
"pagination.first": "Первая",
|
||||
"pagination.last": "Последняя",
|
||||
"pagination.next": "Следующая",
|
||||
"pagination.previous": "Предыдущая",
|
||||
"search.label": "Поиск",
|
||||
"search.placeholder": "Поиск…",
|
||||
"search.submit": "Search",
|
||||
"skip_to_content": "Skip to content",
|
||||
"search.submit": "Искать",
|
||||
"skip_to_content": "Перейти к содержимому",
|
||||
"time_elapsed.days": [
|
||||
"%d день назад",
|
||||
"%d дня назад",
|
||||
@@ -620,4 +632,4 @@
|
||||
"time_elapsed.yesterday": "вчера",
|
||||
"tooltip.keyboard_shortcuts": "Сочетания клавиш: %s",
|
||||
"tooltip.logged_user": "Авторизован как %s"
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@
|
||||
"alert.no_tag_entry": "Bu etiketle eşleşen hiçbir giriş yok.",
|
||||
"alert.no_unread_entry": "Okunmamış makele yok",
|
||||
"alert.no_user": "Tek kullanıcı sizsiniz",
|
||||
"alert.pocket_linked": "Pocket hesabınız artık bağlandı.",
|
||||
"alert.prefs_saved": "Tercihler kaydedildi!",
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"Çok fazla besleme yenilemesi başlattınız. Tekrar denemeden önce lütfen %d dakika bekleyin.",
|
||||
@@ -72,11 +71,15 @@
|
||||
"entry.state.loading": "Yükleniyor...",
|
||||
"entry.state.saving": "Kaydediliyor...",
|
||||
"entry.status.mark_as_read": "Okundu olarak işaretle",
|
||||
"entry.status.mark_as_unread": "Okunmadı olarak işaretle",
|
||||
"entry.status.title": "Makele okundu durumunu değiştir",
|
||||
"entry.status.toast.read": "Okundu olarak işaretlendi",
|
||||
"entry.status.toast.unread": "Okunmamış olarak işaretlendi",
|
||||
"entry.status.mark_as_unread": "Okunmadı olarak işaretle",
|
||||
"entry.tags.label": "Etiketler:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"%d tane daha etiket göster",
|
||||
"%d tane daha etiket göster"
|
||||
],
|
||||
"entry.unshare.label": "Paylaşma",
|
||||
"error.api_key_already_exists": "Bu API anahtarı zaten mevcut.",
|
||||
"error.bad_credentials": "Geçersiz kullanıcı veya parola.",
|
||||
@@ -114,9 +117,12 @@
|
||||
"error.http_service_unavailable": "Dahili sunucu hatası nedeniyle web sitesi şu anda kullanılamıyor. Sorun Miniflux tarafında değil. Lütfen daha sonra tekrar deneyiniz.",
|
||||
"error.http_too_many_requests": "Miniflux bu web sitesine çok fazla istek oluşturdu. Lütfen daha sonra tekrar deneyin veya uygulama yapılandırmasını değiştirin.",
|
||||
"error.http_unexpected_status_code": "Beklenmeyen bir HTTP durum kodu nedeniyle bu websitesi şu anda kullanılamıyor: %d. Sorun Miniflux tarafında değil. Lütfen daha sonra tekrar deneyiniz.",
|
||||
"error.invalid_categories_sorting_order": "Geçersiz kategori sıralama düzeni.",
|
||||
"error.invalid_default_home_page": "Geçersiz varsayılan ana sayfa!",
|
||||
"error.invalid_display_mode": "Geçersiz web uygulaması görüntüleme modu.",
|
||||
"error.invalid_entry_direction": "Geçersiz makele sıralaması.",
|
||||
"error.invalid_entry_order": "Geçersiz makele sıralaması.",
|
||||
"error.invalid_feed_proxy_url": "Geçersiz proxy URL'si.",
|
||||
"error.invalid_feed_url": "Geçersiz besleme URL'si.",
|
||||
"error.invalid_gesture_nav": "Hareketle gezinme geçersiz.",
|
||||
"error.invalid_language": "Geçersiz dil.",
|
||||
@@ -126,13 +132,12 @@
|
||||
"error.network_operation": "Miniflux bir ağ hatası nedeniyle bu websitesine erişemiyor: %v.",
|
||||
"error.network_timeout": "Bu websitesi çok yavaş ve istek zaman aşımına uğradı: %v",
|
||||
"error.password_min_length": "Parola en az 6 karakter içermeli.",
|
||||
"error.pocket_access_token": "Pocket'tan access tokeni alınamıyor!",
|
||||
"error.pocket_request_token": "Pocket'tan request tokeni alınamıyor!",
|
||||
"error.proxy_url_not_empty": "Proxy URL'si boş olamaz.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Geçersiz Engelleme kuralı: #%d kuralında geçerli bir alan adı eksik (Seçenekler: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Geçersiz Engelleme kuralı: #%d kuralı modeli geçerli bir düzenli ifade değil",
|
||||
"error.settings_block_rule_regex_required": "Geçersiz Engelleme kuralı: #%d kuralı modeli sağlanmadı",
|
||||
"error.settings_block_rule_separator_required": "Geçersiz Engelleme kuralı: #%d kuralı modelinin '=' ile ayrılması gerekiyor",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"error.settings_invalid_domain_list": "Geçersiz alan adı listesi. Lütfen boşlukla ayrılmış bir alan adı listesi girin.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Geçersiz Koruma kuralı: #%d kuralında geçerli bir alan adı eksik (Seçenekler: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Geçersiz Koruma kuralı: #%d kuralı modeli geçerli bir düzenli ifade değil",
|
||||
"error.settings_keep_rule_regex_required": "Geçersiz Koruma kuralı: #%d kuralı modeli sağlanmadı",
|
||||
@@ -164,7 +169,8 @@
|
||||
"form.feed.fieldset.rules": "Kurallar",
|
||||
"form.feed.label.allow_self_signed_certificates": "Kendinden imzalı veya geçersiz sertifikalara izin ver",
|
||||
"form.feed.label.apprise_service_urls": "Apprise hizmet URL'lerinin virgülle ayrılmış listesi",
|
||||
"form.feed.label.blocklist_rules": "Engelleme Kuralları",
|
||||
"form.feed.label.block_filter_entry_rules": "Giriş Engelleme Kuralları",
|
||||
"form.feed.label.blocklist_rules": "Regex Tabanlı Engelleme Filtreleri",
|
||||
"form.feed.label.category": "Kategori",
|
||||
"form.feed.label.cookie": "Çerezleri Ayarla",
|
||||
"form.feed.label.crawler": "Orijinal içeriği çek",
|
||||
@@ -174,33 +180,35 @@
|
||||
"form.feed.label.feed_password": "Besleme Parolası",
|
||||
"form.feed.label.feed_url": "Besleme URL'si",
|
||||
"form.feed.label.feed_username": "Besleme Kullanıcı Adı",
|
||||
"form.feed.label.fetch_via_proxy": "Proxy ile çek",
|
||||
"form.feed.label.fetch_via_proxy": "Uygulama düzeyinde yapılandırılmış proxy'yi kullan",
|
||||
"form.feed.label.hide_globally": "Genel okunmamış listesindeki girişleri gizle",
|
||||
"form.feed.label.ignore_http_cache": "HTTP önbelleğini yoksay",
|
||||
"form.feed.label.keeplist_rules": "Saklama Kuralları",
|
||||
"form.feed.label.keep_filter_entry_rules": "Giriş İzin Kuralları",
|
||||
"form.feed.label.keeplist_rules": "Regex Tabanlı Tutma Filtreleri",
|
||||
"form.feed.label.no_media_player": "Medya oynatıcı yok (ses/video)",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "Yeniden Yazma Kuralları",
|
||||
"form.feed.label.ntfy_activate": "Makaleleri ntfy'ye gönder",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy varsayılan öncelik",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy yüksek öncelik",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy düşük öncelik",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy maksimum öncelik",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy minimum öncelik",
|
||||
"form.feed.label.ntfy_priority": "Ntfy öncelik",
|
||||
"form.feed.label.ntfy_topic": "Ntfy konusu (isteğe bağlı)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.pushover_activate": "Makaleleri pushover.net'e gönder",
|
||||
"form.feed.label.pushover_default_priority": "Pushover varsayılan öncelik",
|
||||
"form.feed.label.pushover_high_priority": "Pushover yüksek öncelik",
|
||||
"form.feed.label.pushover_low_priority": "Pushover düşük öncelik",
|
||||
"form.feed.label.pushover_max_priority": "Pushover maksimum öncelik",
|
||||
"form.feed.label.pushover_min_priority": "Pushover minimum öncelik",
|
||||
"form.feed.label.pushover_priority": "Pushover mesaj önceliği",
|
||||
"form.feed.label.rewrite_rules": "İçerik Yeniden Yazma Kuralları",
|
||||
"form.feed.label.scraper_rules": "Scrapper Kuralları",
|
||||
"form.feed.label.site_url": "Site URL'si",
|
||||
"form.feed.label.title": "Başlık",
|
||||
"form.feed.label.urlrewrite_rules": "URL Yeniden Yazma Kuralları",
|
||||
"form.feed.label.user_agent": "Varsayılan User Agent'i Geçersiz Kıl",
|
||||
"form.feed.label.webhook_url": "Override webhook url",
|
||||
"form.feed.label.webhook_url": "Webhook URL'sini geçersiz kıl",
|
||||
"form.import.label.file": "OPML dosyası",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.apprise_activate": "Makaleleri Apprise'a gönder",
|
||||
@@ -209,8 +217,8 @@
|
||||
"form.integration.betula_activate": "Makaleleri Betula'ya kaydet",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.betula_url": "Betula sunucu URLsi",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.integration.cubox_activate": "Makaleleri Cubox'a kaydet",
|
||||
"form.integration.cubox_api_link": "Cubox API bağlantısı",
|
||||
"form.integration.discord_activate": "Makaleleri Discord'a gönder",
|
||||
"form.integration.discord_webhook_link": "Discord hizmet Webhook'lerinin virgülle ayrılmış listesi",
|
||||
"form.integration.espial_activate": "Makaleleri Espial'e kaydet",
|
||||
@@ -228,6 +236,9 @@
|
||||
"form.integration.instapaper_activate": "Makaleleri Instapaper'a kaydet",
|
||||
"form.integration.instapaper_password": "Instapaper Parolası",
|
||||
"form.integration.instapaper_username": "Instapaper Kullanıcı Adı",
|
||||
"form.integration.karakeep_activate": "Makaleleri Karakeep'a kaydet",
|
||||
"form.integration.karakeep_api_key": "Karakeep API anahtarı",
|
||||
"form.integration.karakeep_url": "Karakeep API Uç Noktası",
|
||||
"form.integration.linkace_activate": "Makaleleri LinkAce'e kaydet",
|
||||
"form.integration.linkace_api_key": "LinkAce API anahtarı",
|
||||
"form.integration.linkace_check_disabled": "Link kontrolünü devre dışı bırak",
|
||||
@@ -241,7 +252,7 @@
|
||||
"form.integration.linkding_tags": "Linkding Etiketleri",
|
||||
"form.integration.linkwarden_activate": "Makaleleri Linkwarden'e kaydet",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API Anahtarı",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden API Uç Noktası",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden Temel URL'si",
|
||||
"form.integration.matrix_bot_activate": "Yeni makaleleri Matrix'e aktarın",
|
||||
"form.integration.matrix_bot_chat_id": "Matrix odasının kimliği",
|
||||
"form.integration.matrix_bot_password": "Matrix kullanıcısı için parola",
|
||||
@@ -253,7 +264,7 @@
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
|
||||
"form.integration.ntfy_internal_links": "Tıklamada dahili bağlantıları kullan (isteğe bağlı)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_topic": "Ntfy topic (default if not set in feed)",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
@@ -268,10 +279,6 @@
|
||||
"form.integration.pinboard_bookmark": "Yer imini okunmadı olarak işaretle",
|
||||
"form.integration.pinboard_tags": "Pinboard Etiketleri",
|
||||
"form.integration.pinboard_token": "Pinboard API Token",
|
||||
"form.integration.pocket_access_token": "Pocket Access Token",
|
||||
"form.integration.pocket_activate": "Makaleleri Pocket'a kaydet",
|
||||
"form.integration.pocket_connect_link": "Pocket hesabını bağla",
|
||||
"form.integration.pocket_consumer_key": "Pocket Consumer Anahtarı",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
@@ -290,6 +297,7 @@
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Readwise Access Token'ınızı alın",
|
||||
"form.integration.rssbridge_activate": "Abonelik eklerken RSS-Bridge'i kontrol edin",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Makaleleri Shaarli'ye kaydet",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
@@ -321,7 +329,8 @@
|
||||
"form.prefs.fieldset.authentication_settings": "Kimlik Doğrulama Ayarları",
|
||||
"form.prefs.fieldset.global_feed_settings": "Genel Besleme Ayarları",
|
||||
"form.prefs.fieldset.reader_settings": "Okuyucu Ayarları",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.help.external_font_hosts": "İzin verilecek harici font sunucularının boşlukla ayrılmış listesi. Örneğin: 'fonts.gstatic.com fonts.googleapis.com'.",
|
||||
"form.prefs.label.always_open_external_links": "Makaleleri harici bağlantıları açarak oku",
|
||||
"form.prefs.label.categories_sorting_order": "Kategori sıralaması",
|
||||
"form.prefs.label.cjk_reading_speed": "Çince, Korece ve Japonca için okuma hızı (dakika başına karakter)",
|
||||
"form.prefs.label.custom_css": "Özel CSS",
|
||||
@@ -333,7 +342,7 @@
|
||||
"form.prefs.label.entry_order": "Makale Sıralama Sütunu",
|
||||
"form.prefs.label.entry_sorting": "Makale Sıralaması",
|
||||
"form.prefs.label.entry_swipe": "Dokunmatik ekranlarda makale kaydırmayı etkinleştir",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.label.external_font_hosts": "Harici font sunucuları",
|
||||
"form.prefs.label.gesture_nav": "Makaleler arasında gezinmek için dokunma hareketi",
|
||||
"form.prefs.label.keyboard_shortcuts": "Klavye kısayollarını etkinleştir",
|
||||
"form.prefs.label.language": "Dil",
|
||||
@@ -342,6 +351,7 @@
|
||||
"form.prefs.label.mark_read_on_view": "Makaleler görüntülendiğinde otomatik olarak okundu olarak işaretle",
|
||||
"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.media_playback_rate": "Ses/video oynatma hızı",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Harici bağlantıları yeni bir sekmede aç (bağlantılara target=\"_blank\" ekler)",
|
||||
"form.prefs.label.show_reading_time": "Makaleler için tahmini okuma süresini göster",
|
||||
"form.prefs.label.theme": "Tema",
|
||||
"form.prefs.label.timezone": "Saat Dilimi",
|
||||
@@ -402,6 +412,7 @@
|
||||
"page.about.build_date": "Oluşturulma Tarihi:",
|
||||
"page.about.credits": "Katkıda Bulunanlar",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.global_config_options": "Global yapılandırma seçenekleri",
|
||||
"page.about.go_version": "Go sürümü:",
|
||||
"page.about.license": "Lisans:",
|
||||
@@ -603,4 +614,4 @@
|
||||
"time_elapsed.yesterday": "dün",
|
||||
"tooltip.keyboard_shortcuts": "Klavye Kısayolu: %s",
|
||||
"tooltip.logged_user": "%s olarak giriş yapıldı"
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"action.update": "Зберегти",
|
||||
"alert.account_linked": "Тепер ваш зовнішній обліковий запис від’єднано!",
|
||||
"alert.account_unlinked": "Тепер ваш зовнішній обліковий запис підключено!",
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"alert.background_feed_refresh": "Всі стрічки оновлюються у фоновому режимі. Ви можете продовжувати користуватися Miniflux, поки триває цей процес.",
|
||||
"alert.feed_error": "З цією стрічкою трапилась помилка",
|
||||
"alert.no_bookmark": "Наразі закладки відсутні.",
|
||||
"alert.no_category": "Немає категорії.",
|
||||
@@ -27,27 +27,26 @@
|
||||
"alert.no_tag_entry": "Немає записів, що відповідають цьому тегу.",
|
||||
"alert.no_unread_entry": "Немає непрочитаних статей.",
|
||||
"alert.no_user": "Ви єдиний користувач.",
|
||||
"alert.pocket_linked": "Тепер ваш обліковий запис Pocket підключено!",
|
||||
"alert.prefs_saved": "Уподобання збережено!",
|
||||
"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."
|
||||
"Ви запустили надто багато оновлень стрічок. Будь ласка, зачекайте %d хвилину перед повторною спробою.",
|
||||
"Ви запустили надто багато оновлень стрічок. Будь ласка, зачекайте %d хвилини перед повторною спробою.",
|
||||
"Ви запустили надто багато оновлень стрічок. Будь ласка, зачекайте %d хвилин перед повторною спробою."
|
||||
],
|
||||
"confirm.loading": "В процесі...",
|
||||
"confirm.no": "ні",
|
||||
"confirm.question": "Ви впевнені?",
|
||||
"confirm.question.refresh": "Ви хочете змусити оновити?",
|
||||
"confirm.yes": "так",
|
||||
"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.reset": "Reset",
|
||||
"enclosure_media_controls.speed.reset.title": "Reset speed to 1x",
|
||||
"enclosure_media_controls.speed.slower": "Slower",
|
||||
"enclosure_media_controls.speed.slower.title": "Slower by %sx",
|
||||
"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.reset": "Скинути",
|
||||
"enclosure_media_controls.speed.reset.title": "Скинути швидкість до 1x",
|
||||
"enclosure_media_controls.speed.slower": "Повільніше",
|
||||
"enclosure_media_controls.speed.slower.title": "Повільніше на %sx",
|
||||
"entry.bookmark.toast.off": "Без зірочки",
|
||||
"entry.bookmark.toast.on": "З зірочкою",
|
||||
"entry.bookmark.toggle.off": "Прибрати зірочку",
|
||||
@@ -74,78 +73,85 @@
|
||||
"entry.state.loading": "Завантаження...",
|
||||
"entry.state.saving": "Зберігаю...",
|
||||
"entry.status.mark_as_read": "Позначити як прочитане",
|
||||
"entry.status.mark_as_unread": "Позначити як непрочитане",
|
||||
"entry.status.title": "Змінити стан запису",
|
||||
"entry.status.toast.read": "Відмічено прочитаним",
|
||||
"entry.status.toast.unread": "Відмічено непрочитаним",
|
||||
"entry.status.mark_as_unread": "Позначити як непрочитане",
|
||||
"entry.tags.label": "Теги:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"Ще %d тег",
|
||||
"Ще %d теги",
|
||||
"Ще %d тегів"
|
||||
],
|
||||
"entry.unshare.label": "Не ділитися",
|
||||
"error.api_key_already_exists": "Такий ключ API вже існує.",
|
||||
"error.bad_credentials": "Невірне ім’я користувача або пароль.",
|
||||
"error.category_already_exists": "Така категорія вже існує.",
|
||||
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||
"error.database_error": "Database error: %v.",
|
||||
"error.category_not_found": "Ця категорія не існує або не належить цьому користувачу.",
|
||||
"error.database_error": "Помилка бази даних: %v.",
|
||||
"error.different_passwords": "Паролі не співпадають.",
|
||||
"error.duplicate_fever_username": "Вже є обліковий запис з таким самим користувачем Fever!",
|
||||
"error.duplicate_googlereader_username": "Вже є обліковий запис з таким самим користувачем Google Reader!",
|
||||
"error.duplicate_linked_account": "Вже є обліковий запис, під’єднаний до цього провайдера!",
|
||||
"error.duplicated_feed": "This feed already exists.",
|
||||
"error.duplicated_feed": "Ця стрічка вже існує.",
|
||||
"error.empty_file": "Цей файл порожній.",
|
||||
"error.entries_per_page_invalid": "Число записів на сторінку недійсне.",
|
||||
"error.feed_already_exists": "Така стрічка вже існує.",
|
||||
"error.feed_category_not_found": "Категорія не існує або належить до іншого користувача.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"error.feed_format_not_detected": "Не вдалося визначити формат стрічки: %v.",
|
||||
"error.feed_invalid_blocklist_rule": "Правило списку блокувань недійсне.",
|
||||
"error.feed_invalid_keeplist_rule": "Правило списку дозволень недійсне.",
|
||||
"error.feed_mandatory_fields": "URL та категорія є обов’язковими.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.feed_not_found": "Ця стрічка не існує або не належить цьому користувачу.",
|
||||
"error.feed_title_not_empty": "Назва стрічки не може бути порожньою.",
|
||||
"error.feed_url_not_empty": "URL-адреса стрічки не може бути порожньою.",
|
||||
"error.fields_mandatory": "Всі поля є обов’язковими.",
|
||||
"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_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_client_error": "HTTP client error: %v.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||
"error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).",
|
||||
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_bad_gateway": "Сайт наразі недоступний через помилку шлюзу. Проблема не на стороні Miniflux. Будь ласка, спробуйте пізніше.",
|
||||
"error.http_body_read": "Не вдалося прочитати HTTP-вміст: %v.",
|
||||
"error.http_client_error": "Помилка HTTP-клієнта: %v.",
|
||||
"error.http_empty_response": "Відповідь HTTP порожня. Можливо, цей сайт використовує захист від ботів?",
|
||||
"error.http_empty_response_body": "Тіло відповіді HTTP порожнє.",
|
||||
"error.http_forbidden": "Доступ до цього сайту заборонено. Можливо, сайт має захист від ботів?",
|
||||
"error.http_gateway_timeout": "Сайт наразі недоступний через помилку тайм-ауту шлюзу. Проблема не на стороні Miniflux. Будь ласка, спробуйте пізніше.",
|
||||
"error.http_internal_server_error": "Сайт наразі недоступний через внутрішню помилку сервера. Проблема не на стороні Miniflux. Будь ласка, спробуйте пізніше.",
|
||||
"error.http_not_authorized": "Доступ до цього сайту не дозволено. Можливо, неправильне ім’я користувача або пароль.",
|
||||
"error.http_resource_not_found": "Запитаний ресурс не знайдено. Будь ласка, перевірте URL.",
|
||||
"error.http_response_too_large": "Відповідь HTTP занадто велика. Ви можете збільшити ліміт розміру HTTP-відповіді у глобальних налаштуваннях (потрібен перезапуск сервера).",
|
||||
"error.http_service_unavailable": "Сайт наразі недоступний через внутрішню помилку сервера. Проблема не на стороні Miniflux. Будь ласка, спробуйте пізніше.",
|
||||
"error.http_too_many_requests": "Miniflux згенерував надто багато запитів до цього сайту. Будь ласка, спробуйте пізніше або змініть налаштування програми.",
|
||||
"error.http_unexpected_status_code": "Сайт наразі недоступний через неочікуваний HTTP-код: %d. Проблема не на стороні Miniflux. Будь ласка, спробуйте пізніше.",
|
||||
"error.invalid_categories_sorting_order": "Недійсний порядок сортування категорій.",
|
||||
"error.invalid_default_home_page": "Недійсна домашня сторінка за замовчуванням!",
|
||||
"error.invalid_display_mode": "Недійсний режим відображення.",
|
||||
"error.invalid_entry_direction": "Недійсний напрямок запису.",
|
||||
"error.invalid_entry_order": "Недійсний порядок запису.",
|
||||
"error.invalid_feed_proxy_url": "Недійсний proxy URL.",
|
||||
"error.invalid_feed_url": "Недійсна URL-адреса стрічки.",
|
||||
"error.invalid_gesture_nav": "Недійсна навігація жестами.",
|
||||
"error.invalid_language": "Недійсна мова.",
|
||||
"error.invalid_site_url": "Недійсна URL-адреса сайту.",
|
||||
"error.invalid_theme": "Недійсна тема.",
|
||||
"error.invalid_timezone": "Недійсний часовий пояс.",
|
||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||
"error.network_operation": "Miniflux не може отримати доступ до цього сайту через помилку мережі: %v.",
|
||||
"error.network_timeout": "Цей сайт занадто повільний і запит перевищив час очікування: %v",
|
||||
"error.password_min_length": "Пароль має складати щонайменше 6 символів.",
|
||||
"error.pocket_access_token": "Не вдалося отримати токен доступу з Pocket!",
|
||||
"error.pocket_request_token": "Не вдалося отримати токен доступу з Pocket!",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.proxy_url_not_empty": "Proxy URL не може бути порожнім.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Недійсне правило блокування: у правилі #%d відсутнє коректне ім’я поля (Опції: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "Недійсне правило блокування: шаблон правила #%d не є коректним регулярним виразом",
|
||||
"error.settings_block_rule_regex_required": "Недійсне правило блокування: не вказано шаблон для правила #%d",
|
||||
"error.settings_block_rule_separator_required": "Недійсне правило блокування: шаблон правила #%d має бути розділений знаком '='",
|
||||
"error.settings_invalid_domain_list": "Недійсний список доменів. Будь ласка, вкажіть список доменів, розділених пробілами.",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Недійсне правило дозволення: у правилі #%d відсутнє коректне ім’я поля (Опції: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "Недійсне правило дозволення: шаблон правила #%d не є коректним регулярним виразом",
|
||||
"error.settings_keep_rule_regex_required": "Недійсне правило дозволення: не вказано шаблон для правила #%d",
|
||||
"error.settings_keep_rule_separator_required": "Недійсне правило дозволення: шаблон правила #%d має бути розділений знаком '='",
|
||||
"error.settings_mandatory_fields": "Поля імені, теми, мови та часового поясу є обов’язковими.",
|
||||
"error.settings_media_playback_rate_range": "Швидкість відтворення виходить за межі діапазону",
|
||||
"error.settings_reading_speed_is_positive": "Швидкість читання має бути додатнім цілим числом.",
|
||||
"error.site_url_not_empty": "URL-адреса сайту не може бути порожньою.",
|
||||
"error.subscription_not_found": "Не знайшлося жодної підписки.",
|
||||
"error.title_required": "Назва є обов’язковою.",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.tls_error": "Помилка TLS: %q. Ви можете відключити перевірку TLS в налаштуваннях фіду, якщо хочете.",
|
||||
"error.unable_to_create_api_key": "Не вдається створити такий ключ API",
|
||||
"error.unable_to_create_category": "Не вдається сворити категорію.",
|
||||
"error.unable_to_create_user": "Не вдається створити користувача.",
|
||||
@@ -160,59 +166,62 @@
|
||||
"form.api_key.label.description": "Назва ключа API",
|
||||
"form.category.hide_globally": "Приховати записи в глобальному списку непрочитаного",
|
||||
"form.category.label.title": "Назва",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.integration": "Third-Party Services",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.general": "Загальні",
|
||||
"form.feed.fieldset.integration": "Сторонні сервіси",
|
||||
"form.feed.fieldset.network_settings": "Налаштування мережі",
|
||||
"form.feed.fieldset.rules": "Правила",
|
||||
"form.feed.label.allow_self_signed_certificates": "Дозволити сертифікати з власним підписом або недійсні",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.blocklist_rules": "Правила блокування",
|
||||
"form.feed.label.apprise_service_urls": "Список URL сервісів Apprise, розділених комами",
|
||||
"form.feed.label.block_filter_entry_rules": "Правила блокування записів",
|
||||
"form.feed.label.blocklist_rules": "Фільтри блокування на основі регулярних виразів",
|
||||
"form.feed.label.category": "Категорія",
|
||||
"form.feed.label.cookie": "Встановити кукі",
|
||||
"form.feed.label.crawler": "Завантажувати оригінальний вміст",
|
||||
"form.feed.label.description": "Опис",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.disable_http2": "Вимкнути HTTP/2 для уникнення відбитків",
|
||||
"form.feed.label.disabled": "Не оновлювати цю стрічку",
|
||||
"form.feed.label.feed_password": "Пароль для завантаження",
|
||||
"form.feed.label.feed_url": "URL-адреса стрічки",
|
||||
"form.feed.label.feed_username": "Ім’я користувача для завантаження",
|
||||
"form.feed.label.fetch_via_proxy": "Використати проксі-сервер",
|
||||
"form.feed.label.fetch_via_proxy": "Використовувати проксі, налаштований на рівні програми",
|
||||
"form.feed.label.hide_globally": "Приховати записи в глобальному списку непрочитаного",
|
||||
"form.feed.label.ignore_http_cache": "Ігнорувати кеш HTTP",
|
||||
"form.feed.label.keeplist_rules": "Правила дозволення",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "Правила Rewrite",
|
||||
"form.feed.label.keep_filter_entry_rules": "Правила дозволу записів",
|
||||
"form.feed.label.keeplist_rules": "Фільтри збереження на основі регулярних виразів",
|
||||
"form.feed.label.no_media_player": "Немає медіаплеєра (аудіо/відео)",
|
||||
"form.feed.label.ntfy_activate": "Надсилати записи у ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Стандартний пріоритет ntfy",
|
||||
"form.feed.label.ntfy_high_priority": "Високий пріоритет ntfy",
|
||||
"form.feed.label.ntfy_low_priority": "Низький пріоритет ntfy",
|
||||
"form.feed.label.ntfy_max_priority": "Максимальний пріоритет ntfy",
|
||||
"form.feed.label.ntfy_min_priority": "Мінімальний пріоритет ntfy",
|
||||
"form.feed.label.ntfy_priority": "Пріоритет ntfy",
|
||||
"form.feed.label.ntfy_topic": "Тема ntfy (необов’язково)",
|
||||
"form.feed.label.proxy_url": "Proxy URL",
|
||||
"form.feed.label.pushover_activate": "Надсилати записи у pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Стандартний пріоритет Pushover",
|
||||
"form.feed.label.pushover_high_priority": "Високий пріоритет Pushover",
|
||||
"form.feed.label.pushover_low_priority": "Низький пріоритет Pushover",
|
||||
"form.feed.label.pushover_max_priority": "Максимальний пріоритет Pushover",
|
||||
"form.feed.label.pushover_min_priority": "Мінімальний пріоритет Pushover",
|
||||
"form.feed.label.pushover_priority": "Пріоритет повідомлення Pushover",
|
||||
"form.feed.label.rewrite_rules": "Правила перезапису вмісту",
|
||||
"form.feed.label.scraper_rules": "Правила Scraper",
|
||||
"form.feed.label.site_url": "URL-адреса сайту",
|
||||
"form.feed.label.title": "Назва",
|
||||
"form.feed.label.urlrewrite_rules": "Правила перезапису URL-адрес",
|
||||
"form.feed.label.user_agent": "Назначити User Agent",
|
||||
"form.feed.label.webhook_url": "Override webhook url",
|
||||
"form.feed.label.webhook_url": "Перевизначити URL вебхука",
|
||||
"form.import.label.file": "Файл OPML",
|
||||
"form.import.label.url": "URL-адреса",
|
||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||
"form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
|
||||
"form.integration.apprise_activate": "Надсилати записи у Apprise",
|
||||
"form.integration.apprise_services_url": "Список URL сервісів Apprise, розділених комами",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.integration.cubox_activate": "Зберігати статті до Cubox",
|
||||
"form.integration.cubox_api_link": "Посилання на Cubox API",
|
||||
"form.integration.discord_activate": "Push entries to Discord",
|
||||
"form.integration.discord_webhook_link": "Discord Webhook link",
|
||||
"form.integration.espial_activate": "Зберігати статті до Espial",
|
||||
@@ -230,6 +239,9 @@
|
||||
"form.integration.instapaper_activate": "Зберігати статті до Instapaper",
|
||||
"form.integration.instapaper_password": "Пароль Instapaper",
|
||||
"form.integration.instapaper_username": "Ім’я користувача Instapaper",
|
||||
"form.integration.karakeep_activate": "Зберігати статті до Karakeep",
|
||||
"form.integration.karakeep_api_key": "Ключ API Karakeep",
|
||||
"form.integration.karakeep_url": "Karakeep API Endpoint",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
@@ -243,7 +255,7 @@
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkwarden_activate": "Зберігати статті до Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Ключ API Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden API Endpoint",
|
||||
"form.integration.linkwarden_endpoint": "Базова URL-адреса Linkwarden",
|
||||
"form.integration.matrix_bot_activate": "Перенесення нових статей в Матрицю",
|
||||
"form.integration.matrix_bot_chat_id": "Ідентифікатор кімнати Матриці",
|
||||
"form.integration.matrix_bot_password": "Пароль для користувача Matrix",
|
||||
@@ -252,10 +264,10 @@
|
||||
"form.integration.notion_activate": "Save entries to Notion",
|
||||
"form.integration.notion_page_id": "Notion Page ID",
|
||||
"form.integration.notion_token": "Notion Secret Token",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_activate": "Надсилати записи у ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
|
||||
"form.integration.ntfy_internal_links": "Використовувати внутрішні посилання при натисканні (необов’язково)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_topic": "Ntfy topic (default if not set in feed)",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
@@ -270,10 +282,6 @@
|
||||
"form.integration.pinboard_bookmark": "Відмічати закладку як непрочитану",
|
||||
"form.integration.pinboard_tags": "Теги для Pinboard",
|
||||
"form.integration.pinboard_token": "API ключ від Pinboard",
|
||||
"form.integration.pocket_access_token": "Pocket Access Token",
|
||||
"form.integration.pocket_activate": "Зберігати статті до Pocket",
|
||||
"form.integration.pocket_connect_link": "Підключити ваш обліковий запис Pocket",
|
||||
"form.integration.pocket_consumer_key": "Pocket Consumer Key",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
@@ -292,6 +300,7 @@
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
@@ -323,7 +332,8 @@
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"form.prefs.help.external_font_hosts": "Список дозволених зовнішніх хостів шрифтів, розділених пробілами. Наприклад: 'fonts.gstatic.com fonts.googleapis.com'.",
|
||||
"form.prefs.label.always_open_external_links": "Читати статті, відкриваючи зовнішні посилання",
|
||||
"form.prefs.label.categories_sorting_order": "Сортування за категоріями",
|
||||
"form.prefs.label.cjk_reading_speed": "Швидкість читання для китайської, корейської та японської мови (символів на хвилину)",
|
||||
"form.prefs.label.custom_css": "Спеціальний CSS",
|
||||
@@ -335,7 +345,7 @@
|
||||
"form.prefs.label.entry_order": "Стовпець сортування записів",
|
||||
"form.prefs.label.entry_sorting": "Сортування записів",
|
||||
"form.prefs.label.entry_swipe": "Увімкніть введення пальцем на сенсорних екранах",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.label.external_font_hosts": "Зовнішні хости шрифтів",
|
||||
"form.prefs.label.gesture_nav": "Жест для переходу між записами",
|
||||
"form.prefs.label.keyboard_shortcuts": "Увімкнути комбінації клавиш",
|
||||
"form.prefs.label.language": "Мова",
|
||||
@@ -344,6 +354,7 @@
|
||||
"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.media_playback_rate": "Швидкість відтворення аудіо/відео",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "Відкривати зовнішні посилання у новій вкладці (додає target=\"_blank\" до посилань)",
|
||||
"form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів",
|
||||
"form.prefs.label.theme": "Тема",
|
||||
"form.prefs.label.timezone": "Часовий пояс",
|
||||
@@ -404,6 +415,7 @@
|
||||
"page.about.build_date": "Дата побудови:",
|
||||
"page.about.credits": "Титри",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.global_config_options": "Параметри глобальної конфігурації",
|
||||
"page.about.go_version": "Версія Go:",
|
||||
"page.about.license": "Ліцензія:",
|
||||
@@ -620,4 +632,4 @@
|
||||
"time_elapsed.yesterday": "вчора",
|
||||
"tooltip.keyboard_shortcuts": "Комбінація клавіш: %s",
|
||||
"tooltip.logged_user": "Здійснено вхід як %s"
|
||||
}
|
||||
}
|
||||
@@ -6,40 +6,39 @@
|
||||
"action.import": "导入",
|
||||
"action.login": "登录",
|
||||
"action.or": "或",
|
||||
"action.remove": "删除",
|
||||
"action.remove_feed": "删除此源",
|
||||
"action.remove": "移除",
|
||||
"action.remove_feed": "移除此订阅源",
|
||||
"action.save": "保存",
|
||||
"action.subscribe": "订阅",
|
||||
"action.update": "更新",
|
||||
"alert.account_linked": "您的外部账号已关联!",
|
||||
"alert.account_unlinked": "您的外部帐户现已解除关联!",
|
||||
"alert.background_feed_refresh": "所有的订阅源都在后台刷新中。您可以继续使用Miniflux,同时此过程正在运行。",
|
||||
"alert.feed_error": "该源存在问题",
|
||||
"alert.no_bookmark": "目前没有收藏",
|
||||
"alert.no_category": "目前没有分类",
|
||||
"alert.no_category_entry": "该分类下没有文章",
|
||||
"alert.no_feed": "目前没有源",
|
||||
"alert.no_feed_entry": "该源中没有文章",
|
||||
"alert.no_feed_in_category": "没有该类别的源。",
|
||||
"alert.no_history": "目前没有历史",
|
||||
"alert.no_search_result": "该搜索没有结果",
|
||||
"alert.no_shared_entry": "没有分享文章。",
|
||||
"alert.no_tag_entry": "没有与此标签匹配的条目。",
|
||||
"alert.no_unread_entry": "目前没有未读文章",
|
||||
"alert.no_user": "您是目前仅有的用户",
|
||||
"alert.pocket_linked": "您的 Pocket 帐户现已关联",
|
||||
"alert.prefs_saved": "设置已存储!",
|
||||
"alert.account_unlinked": "您的外部帐户已解除关联!",
|
||||
"alert.background_feed_refresh": "所有订阅源正在后台刷新。您可以在刷新过程中继续使用 Miniflux。",
|
||||
"alert.feed_error": "此订阅源存在问题",
|
||||
"alert.no_bookmark": "没有收藏的条目。",
|
||||
"alert.no_category": "没有分类。",
|
||||
"alert.no_category_entry": "此分类下没有条目。",
|
||||
"alert.no_feed": "你没有任何订阅源。",
|
||||
"alert.no_feed_entry": "此订阅源中没有条目。",
|
||||
"alert.no_feed_in_category": "此分类中没有订阅源。",
|
||||
"alert.no_history": "当前没有历史记录。",
|
||||
"alert.no_search_result": "此搜索没有结果。",
|
||||
"alert.no_shared_entry": "没有已分享条目。",
|
||||
"alert.no_tag_entry": "没有匹配此标签的条目。",
|
||||
"alert.no_unread_entry": "没有未读条目。",
|
||||
"alert.no_user": "您是唯一的用户。",
|
||||
"alert.prefs_saved": "偏好设置已保存!",
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"多次触发订阅源更新,请等待 %d 分钟后重试。"
|
||||
"您触发了太多次订阅源刷新。请在 %d 分钟后重试。"
|
||||
],
|
||||
"confirm.loading": "执行中…",
|
||||
"confirm.loading": "进行中…",
|
||||
"confirm.no": "否",
|
||||
"confirm.question": "您确认吗?",
|
||||
"confirm.question.refresh": "您是否要强制刷新?",
|
||||
"confirm.question": "您确定吗?",
|
||||
"confirm.question.refresh": "您确定要强制刷新吗?",
|
||||
"confirm.yes": "是",
|
||||
"enclosure_media_controls.seek": "查找:",
|
||||
"enclosure_media_controls.seek": "查找:",
|
||||
"enclosure_media_controls.seek.title": "查找 %s 秒",
|
||||
"enclosure_media_controls.speed": "速度:",
|
||||
"enclosure_media_controls.speed": "速度:",
|
||||
"enclosure_media_controls.speed.faster": "快进",
|
||||
"enclosure_media_controls.speed.faster.title": "速度快进到 %sx",
|
||||
"enclosure_media_controls.speed.reset": "重置",
|
||||
@@ -56,162 +55,170 @@
|
||||
"需要 %d 分钟阅读"
|
||||
],
|
||||
"entry.external_link.label": "外部链接",
|
||||
"entry.save.completed": "完成",
|
||||
"entry.save.completed": "完成!",
|
||||
"entry.save.label": "保存",
|
||||
"entry.save.title": "保存这篇文章",
|
||||
"entry.save.toast.completed": "已保存文章",
|
||||
"entry.scraper.completed": "抓取完成",
|
||||
"entry.scraper.label": "抓取全文",
|
||||
"entry.scraper.title": "抓取全文内容",
|
||||
"entry.save.title": "保存此条目",
|
||||
"entry.save.toast.completed": "条目已保存",
|
||||
"entry.scraper.completed": "完成!",
|
||||
"entry.scraper.label": "下载",
|
||||
"entry.scraper.title": "获取原始内容",
|
||||
"entry.share.label": "分享",
|
||||
"entry.share.title": "分享这篇文章",
|
||||
"entry.share.title": "分享此条目",
|
||||
"entry.shared_entry.label": "分享",
|
||||
"entry.shared_entry.title": "打开公共链接",
|
||||
"entry.state.loading": "载入中…",
|
||||
"entry.shared_entry.title": "打开公开链接",
|
||||
"entry.state.loading": "加载中…",
|
||||
"entry.state.saving": "保存中…",
|
||||
"entry.status.mark_as_read": "标为已读",
|
||||
"entry.status.title": "更改状态",
|
||||
"entry.status.mark_as_unread": "标为未读",
|
||||
"entry.status.title": "更改条目状态",
|
||||
"entry.status.toast.read": "已标为已读",
|
||||
"entry.status.toast.unread": "已标为未读",
|
||||
"entry.status.mark_as_unread": "标为未读",
|
||||
"entry.tags.label": "标签:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"显示 %d 个更多标签"
|
||||
],
|
||||
"entry.unshare.label": "取消分享",
|
||||
"error.api_key_already_exists": "此 API 密钥已存在。",
|
||||
"error.bad_credentials": "用户名或密码无效",
|
||||
"error.category_already_exists": "分类已存在",
|
||||
"error.category_not_found": "该分类不存在或不属于该用户。",
|
||||
"error.bad_credentials": "用户名或密码无效。",
|
||||
"error.category_already_exists": "此分类已存在。",
|
||||
"error.category_not_found": "此分类不存在或不属于此用户。",
|
||||
"error.database_error": "数据库错误: %v。",
|
||||
"error.different_passwords": "两次输入的密码不同",
|
||||
"error.duplicate_fever_username": "Fever 用户名已被占用!",
|
||||
"error.duplicate_googlereader_username": "Google Reader 用户名已被占用!",
|
||||
"error.duplicate_linked_account": "该 Provider 已被关联!",
|
||||
"error.duplicated_feed": "该订阅源已经存在。",
|
||||
"error.empty_file": "该文件为空",
|
||||
"error.entries_per_page_invalid": "每页的文章数无效。",
|
||||
"error.feed_already_exists": "此源已存在。",
|
||||
"error.feed_category_not_found": "此类别不存在或不属于该用户。",
|
||||
"error.feed_format_not_detected": "无法解析订阅源格式: %v。",
|
||||
"error.different_passwords": "密码不一致。",
|
||||
"error.duplicate_fever_username": "已存在其他用户使用相同的 Fever 用户名!",
|
||||
"error.duplicate_googlereader_username": "已存在其他用户使用相同的 Google Reader 用户名!",
|
||||
"error.duplicate_linked_account": "已有人与该提供商关联!",
|
||||
"error.duplicated_feed": "此订阅源已经存在。",
|
||||
"error.empty_file": "此文件为空。",
|
||||
"error.entries_per_page_invalid": "每页的条目数无效。",
|
||||
"error.feed_already_exists": "此订阅源已存在。",
|
||||
"error.feed_category_not_found": "此分类不存在或不属于此用户。",
|
||||
"error.feed_format_not_detected": "无法解析订阅源格式:%v。",
|
||||
"error.feed_invalid_blocklist_rule": "阻止列表规则无效。",
|
||||
"error.feed_invalid_keeplist_rule": "保留列表规则无效。",
|
||||
"error.feed_mandatory_fields": "必须填写网址和分类",
|
||||
"error.feed_not_found": "该订阅源不存在或不属于该用户。",
|
||||
"error.feed_mandatory_fields": "必须填写 URL 和分类。",
|
||||
"error.feed_not_found": "此订阅源不存在或不属于此用户。",
|
||||
"error.feed_title_not_empty": "订阅源的标题不能为空。",
|
||||
"error.feed_url_not_empty": "订阅源的网址不能为空。",
|
||||
"error.fields_mandatory": "必须填写全部信息",
|
||||
"error.http_bad_gateway": "当前由于错误的网关导致该网站无法访问,问题不在Miniflux,请稍后重试。",
|
||||
"error.http_body_read": "无法读取HTTP主体: %v。",
|
||||
"error.http_client_error": "HTTP 客户端错误r: %v。",
|
||||
"error.http_empty_response": "HTTP响应内容为空,该网站可能正在使用机器人保护机制。",
|
||||
"error.http_empty_response_body": "HTTP响应主体为空。",
|
||||
"error.http_forbidden": "该网站被禁止访问,网站可能有机器人保护机制?",
|
||||
"error.http_gateway_timeout": "当前由于网关超时导致该网站无法访问,问题不在Miniflux,请稍后重试。",
|
||||
"error.http_internal_server_error": "当前由于服务器错误导致该网站无法访问,问题不在Miniflux,请稍后重试。",
|
||||
"error.http_not_authorized": "该网站访问未授权,可能用户名和密码错误。",
|
||||
"error.http_resource_not_found": "请求资源无法找到,请检查URL。",
|
||||
"error.http_response_too_large": "HTTP响应内容过大,您可以在全局设置中增加HTTP响应大小限制(需要服务器重新启动)。",
|
||||
"error.http_service_unavailable": "当前由于服务器内部错误导致该网站无法访问,问题不在Miniflux,请稍后重试。",
|
||||
"error.http_too_many_requests": "Miniflux 对该网站请求过多次数,请稍后重试或修改应用配置项。",
|
||||
"error.http_unexpected_status_code": "当前由于意外的HTTP状态码:%d 导致该网站无法访问,问题不在Miniflux,请稍后重试。",
|
||||
"error.invalid_default_home_page": "无效的默认主页!",
|
||||
"error.feed_url_not_empty": "订阅源的 URL 不能为空。",
|
||||
"error.fields_mandatory": "必须填写全部信息。",
|
||||
"error.http_bad_gateway": "由于网关错误,网站暂不可用。这不是 Miniflux 的问题,请稍后重试。",
|
||||
"error.http_body_read": "无法读取 HTTP 正文:%v。",
|
||||
"error.http_client_error": "HTTP 客户端错误:%v。",
|
||||
"error.http_empty_response": "HTTP 响应为空,该网站可能使用了反爬虫机制。",
|
||||
"error.http_empty_response_body": "HTTP 响应正文为空。",
|
||||
"error.http_forbidden": "禁止访问该网站。可能该网站使用了反爬虫机制?",
|
||||
"error.http_gateway_timeout": "由于网关超时,网站暂不可用。这不是 Miniflux 的问题,请稍后重试。",
|
||||
"error.http_internal_server_error": "由于服务器错误,网站暂不可用。这不是 Miniflux 的问题,请稍后重试。",
|
||||
"error.http_not_authorized": "未经授权访问此网站。可能是用户名或密码错误。",
|
||||
"error.http_resource_not_found": "未找到请求的资源。请检查 URL。",
|
||||
"error.http_response_too_large": "HTTP 响应过大。您可以在全局设置中增加 HTTP 响应大小限制(需重启服务器)。",
|
||||
"error.http_service_unavailable": "由于内部服务器错误,网站暂不可用。这不是 Miniflux 的问题,请稍后重试。",
|
||||
"error.http_too_many_requests": "Miniflux 向此网站生成了过多请求。请稍后重试或更改应用程序配置。",
|
||||
"error.http_unexpected_status_code": "由于意外的 HTTP 状态码 %d,网站暂不可用。这不是 Miniflux 的问题,请稍后重试。",
|
||||
"error.invalid_categories_sorting_order": "无效的分类排序顺序。",
|
||||
"error.invalid_default_home_page": "无效的默认主页!",
|
||||
"error.invalid_display_mode": "无效的网页应用显示模式。",
|
||||
"error.invalid_entry_direction": "无效的输入方向。",
|
||||
"error.invalid_feed_url": "订阅源的网址无效。",
|
||||
"error.invalid_gesture_nav": "手势导航无效。",
|
||||
"error.invalid_entry_direction": "无效的条目方向。",
|
||||
"error.invalid_entry_order": "无效的条目排序。",
|
||||
"error.invalid_feed_proxy_url": "无效的代理 URL。",
|
||||
"error.invalid_feed_url": "无效的订阅源 URL。",
|
||||
"error.invalid_gesture_nav": "无效的手势导航。",
|
||||
"error.invalid_language": "无效的语言。",
|
||||
"error.invalid_site_url": "源网站的网址无效。",
|
||||
"error.invalid_site_url": "无效的网站 URL。",
|
||||
"error.invalid_theme": "无效的主题。",
|
||||
"error.invalid_timezone": "无效的时区。",
|
||||
"error.network_operation": "Miniflux无法访问该网站由于网络错误: %v。",
|
||||
"error.network_timeout": "该网站响应过慢,请求超时: %v",
|
||||
"error.password_min_length": "请至少输入 6 个字符",
|
||||
"error.pocket_access_token": "无法从 Pocket 获取访问令牌!",
|
||||
"error.pocket_request_token": "无法从 Pocket 获取请求令牌!",
|
||||
"error.settings_block_rule_fieldname_invalid": "无效的阻止规则: 规则 #%d 缺少合法的字段名 (可选: %s)",
|
||||
"error.settings_block_rule_invalid_regex": "无效的阻止规则: 规则 #%d 的模式字符不是合法的正则表达式。",
|
||||
"error.settings_block_rule_regex_required": "无效的阻止规则: 规则 #%d 的模式字符没有提供。",
|
||||
"error.settings_block_rule_separator_required": "无效的阻止规则: 规则 #%d 的模式字符必须用‘=’分开。",
|
||||
"error.settings_invalid_domain_list": "域名列表无效。请提供一个用空格分隔的域名列表。",
|
||||
"error.settings_keep_rule_fieldname_invalid": "无效的保留规则: 规则 #%d 缺少合法的字段名 (可选: %s)",
|
||||
"error.settings_keep_rule_invalid_regex": "无效的保留规则: 规则 #%d 的模式字符不是合法的正则表达式。",
|
||||
"error.settings_keep_rule_regex_required": "无效的保留规则: 规则 #%d 的模式字符没有提供。",
|
||||
"error.settings_keep_rule_separator_required": "无效的保留规则: 规则 #%d 的模式字符必须用‘=’分开。",
|
||||
"error.settings_mandatory_fields": "必须填写用户名、主题、语言以及时区",
|
||||
"error.network_operation": "由于网络错误,Miniflux 无法访问此网站:%v。",
|
||||
"error.network_timeout": "该网站响应过慢,请求已超时:%v",
|
||||
"error.password_min_length": "密码长度至少为 6 个字符。",
|
||||
"error.proxy_url_not_empty": "代理 URL 不能为空。",
|
||||
"error.settings_block_rule_fieldname_invalid": "无效的阻止规则:规则 #%d 缺少合法的字段名(可选:%s)",
|
||||
"error.settings_block_rule_invalid_regex": "无效的阻止规则:规则 #%d 的模式字符不是合法的正则表达式",
|
||||
"error.settings_block_rule_regex_required": "无效的阻止规则:规则 #%d 的模式字符没有提供",
|
||||
"error.settings_block_rule_separator_required": "无效的阻止规则:规则 #%d 的模式字符必须用‘=’分开",
|
||||
"error.settings_invalid_domain_list": "无效的域名列表。请提供以空格分隔的域名列表。",
|
||||
"error.settings_keep_rule_fieldname_invalid": "无效的保留规则:规则 #%d 缺少合法的字段名(可选:%s)",
|
||||
"error.settings_keep_rule_invalid_regex": "无效的保留规则:规则 #%d 的模式字符不是合法的正则表达式",
|
||||
"error.settings_keep_rule_regex_required": "无效的保留规则:规则 #%d 的模式字符没有提供",
|
||||
"error.settings_keep_rule_separator_required": "无效的保留规则:规则 #%d 的模式字符必须用‘=’分开",
|
||||
"error.settings_mandatory_fields": "必须填写用户名、主题、语言以及时区。",
|
||||
"error.settings_media_playback_rate_range": "播放速度超出范围",
|
||||
"error.settings_reading_speed_is_positive": "阅读速度必须是正整数。",
|
||||
"error.site_url_not_empty": "源网站的网址不能为空。",
|
||||
"error.subscription_not_found": "找不到任何源",
|
||||
"error.title_required": "必须填写标题",
|
||||
"error.tls_error": "TLS 错误: %q。如果您愿意的话可以在订阅源设置里关闭TLS验证。",
|
||||
"error.site_url_not_empty": "站点 URL 不能为空。",
|
||||
"error.subscription_not_found": "无法找到任何订阅源。",
|
||||
"error.title_required": "必须填写标题。",
|
||||
"error.tls_error": "TLS 错误: %q。如果您愿意的话可以在订阅源设置里关闭 TLS 验证。",
|
||||
"error.unable_to_create_api_key": "无法创建此 API 密钥。",
|
||||
"error.unable_to_create_category": "无法建立这个分类",
|
||||
"error.unable_to_create_user": "无法创建此用户",
|
||||
"error.unable_to_detect_rssbridge": "无法使用RSS-Bridge去检测订阅源: %v。",
|
||||
"error.unable_to_parse_feed": "无法解析该订阅源: %v。",
|
||||
"error.unable_to_update_category": "无法更新该分类",
|
||||
"error.unable_to_update_feed": "无法更新此源",
|
||||
"error.unable_to_update_user": "无法更新此用户",
|
||||
"error.unable_to_create_category": "无法创建此分类。",
|
||||
"error.unable_to_create_user": "无法创建此用户。",
|
||||
"error.unable_to_detect_rssbridge": "无法使用 RSS-Bridge 检测订阅源:%v。",
|
||||
"error.unable_to_parse_feed": "无法解析此订阅源:%v。",
|
||||
"error.unable_to_update_category": "无法更新此分类。",
|
||||
"error.unable_to_update_feed": "无法更新此订阅源。",
|
||||
"error.unable_to_update_user": "无法更新此用户。",
|
||||
"error.unlink_account_without_password": "您必须设置密码,否则您将无法再次登录。",
|
||||
"error.user_already_exists": "用户已存在",
|
||||
"error.user_mandatory_fields": "必须填写用户名",
|
||||
"form.api_key.label.description": "API密钥标签",
|
||||
"form.category.hide_globally": "隐藏全局未读列表中的文章",
|
||||
"error.user_already_exists": "此用户已存在。",
|
||||
"error.user_mandatory_fields": "必须填写用户名。",
|
||||
"form.api_key.label.description": "API 密钥标签",
|
||||
"form.category.hide_globally": "在全局未读列表中隐藏条目",
|
||||
"form.category.label.title": "标题",
|
||||
"form.feed.fieldset.general": "通用",
|
||||
"form.feed.fieldset.general": "常规",
|
||||
"form.feed.fieldset.integration": "第三方服务",
|
||||
"form.feed.fieldset.network_settings": "网络设置",
|
||||
"form.feed.fieldset.rules": "规则",
|
||||
"form.feed.label.allow_self_signed_certificates": "允许自签名证书或无效证书",
|
||||
"form.feed.label.apprise_service_urls": "使用逗号分隔的 Apprise 服务 URL 列表",
|
||||
"form.feed.label.blocklist_rules": "阻止规则",
|
||||
"form.feed.label.category": "类别",
|
||||
"form.feed.label.cookie": "设置 Cookies",
|
||||
"form.feed.label.crawler": "抓取全文内容",
|
||||
"form.feed.label.block_filter_entry_rules": "条目屏蔽规则",
|
||||
"form.feed.label.blocklist_rules": "基于正则表达式的屏蔽过滤器",
|
||||
"form.feed.label.category": "分类",
|
||||
"form.feed.label.cookie": "设置 Cookie",
|
||||
"form.feed.label.crawler": "获取原始内容",
|
||||
"form.feed.label.description": "描述",
|
||||
"form.feed.label.disable_http2": "关闭 HTTP/2 避免记录指纹",
|
||||
"form.feed.label.disabled": "请勿刷新此源",
|
||||
"form.feed.label.feed_password": "源密码",
|
||||
"form.feed.label.disable_http2": "禁用 HTTP/2 以避免指纹识别",
|
||||
"form.feed.label.disabled": "不刷新此订阅",
|
||||
"form.feed.label.feed_password": "订阅源密码",
|
||||
"form.feed.label.feed_url": "订阅源 URL",
|
||||
"form.feed.label.feed_username": "源用户名",
|
||||
"form.feed.label.fetch_via_proxy": "通过代理获取",
|
||||
"form.feed.label.hide_globally": "隐藏全局未读列表中的文章",
|
||||
"form.feed.label.feed_username": "订阅源用户名",
|
||||
"form.feed.label.fetch_via_proxy": "使用在应用程序级别配置的代理",
|
||||
"form.feed.label.hide_globally": "在全局未读列表中隐藏条目",
|
||||
"form.feed.label.ignore_http_cache": "忽略 HTTP 缓存",
|
||||
"form.feed.label.keeplist_rules": "保留规则",
|
||||
"form.feed.label.no_media_player": "没有媒体播放器(音频/视频)",
|
||||
"form.feed.label.ntfy_activate": "推送条目到ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy默认优先级",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy高优先级",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy低优先级",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy最高优先级",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy最低优先级",
|
||||
"form.feed.label.ntfy_priority": "Ntfy优先级",
|
||||
"form.feed.label.ntfy_topic": "Ntfy主题(可选)",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "重写规则",
|
||||
"form.feed.label.keep_filter_entry_rules": "条目允许规则",
|
||||
"form.feed.label.keeplist_rules": "基于正则表达式的保留过滤器",
|
||||
"form.feed.label.no_media_player": "无媒体播放器(音频/视频)",
|
||||
"form.feed.label.ntfy_activate": "推送条目到 Ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy 默认优先级",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy 高优先级",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy 低优先级",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy 最高优先级",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy 最低优先级",
|
||||
"form.feed.label.ntfy_priority": "Ntfy 优先级",
|
||||
"form.feed.label.ntfy_topic": "Ntfy 主题(可选)",
|
||||
"form.feed.label.proxy_url": "代理 URL",
|
||||
"form.feed.label.pushover_activate": "推送条目到 Pushover",
|
||||
"form.feed.label.pushover_default_priority": "Pushover 默认优先级",
|
||||
"form.feed.label.pushover_high_priority": "Pushover 高优先级",
|
||||
"form.feed.label.pushover_low_priority": "Pushover 低优先级",
|
||||
"form.feed.label.pushover_max_priority": "Pushover 最高优先级",
|
||||
"form.feed.label.pushover_min_priority": "Pushover 最低优先级",
|
||||
"form.feed.label.pushover_priority": "Pushover 消息优先级",
|
||||
"form.feed.label.rewrite_rules": "内容重写规则",
|
||||
"form.feed.label.scraper_rules": "抓取规则",
|
||||
"form.feed.label.site_url": "源网站 URL",
|
||||
"form.feed.label.site_url": "站点 URL",
|
||||
"form.feed.label.title": "标题",
|
||||
"form.feed.label.urlrewrite_rules": "URL 重写规则",
|
||||
"form.feed.label.user_agent": "覆盖默认的用户代理",
|
||||
"form.feed.label.webhook_url": "Override webhook url",
|
||||
"form.feed.label.webhook_url": "覆盖 Webhook URL",
|
||||
"form.import.label.file": "OPML 文件",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.apprise_activate": "将新文章推送到 Apprise",
|
||||
"form.integration.apprise_activate": "将新条目推送到 Apprise",
|
||||
"form.integration.apprise_services_url": "使用逗号分隔的 Apprise 服务 URL 列表",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.betula_activate": "保存文章到 Betula",
|
||||
"form.integration.betula_token": "Betula 密钥",
|
||||
"form.integration.betula_url": "Betula 服务地址",
|
||||
"form.integration.cubox_activate": "保存文章到 Cubox",
|
||||
"form.integration.betula_activate": "保存条目到 Betula",
|
||||
"form.integration.betula_token": "Betula 令牌",
|
||||
"form.integration.betula_url": "Betula 服务端 URL",
|
||||
"form.integration.cubox_activate": "保存条目到 Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API 链接",
|
||||
"form.integration.discord_activate": "将新文章推送到 Discord",
|
||||
"form.integration.discord_webhook_link": "Discord Webhook link",
|
||||
"form.integration.espial_activate": "保存文章到 Espial",
|
||||
"form.integration.discord_activate": "推送条目到 Discord",
|
||||
"form.integration.discord_webhook_link": "Discord Webhook 链接",
|
||||
"form.integration.espial_activate": "保存条目到 Espial",
|
||||
"form.integration.espial_api_key": "Espial API 密钥",
|
||||
"form.integration.espial_endpoint": "Espial API 端点",
|
||||
"form.integration.espial_tags": "Espial 标签",
|
||||
@@ -220,294 +227,297 @@
|
||||
"form.integration.fever_password": "Fever 密码",
|
||||
"form.integration.fever_username": "Fever 用户名",
|
||||
"form.integration.googlereader_activate": "启用 Google Reader API",
|
||||
"form.integration.googlereader_endpoint": "Google Reader API 端点:",
|
||||
"form.integration.googlereader_endpoint": "Google Reader API 端点:",
|
||||
"form.integration.googlereader_password": "Google Reader 密码",
|
||||
"form.integration.googlereader_username": "Google Reader 用户名",
|
||||
"form.integration.instapaper_activate": "保存文章到 Instapaper",
|
||||
"form.integration.instapaper_activate": "保存条目到 Instapaper",
|
||||
"form.integration.instapaper_password": "Instapaper 密码",
|
||||
"form.integration.instapaper_username": "Instapaper 用户名",
|
||||
"form.integration.linkace_activate": "保存文章到 LinkAce",
|
||||
"form.integration.karakeep_activate": "保存条目到 Karakeep",
|
||||
"form.integration.karakeep_api_key": "Karakeep API 密钥",
|
||||
"form.integration.karakeep_url": "Karakeep API 端点",
|
||||
"form.integration.linkace_activate": "保存条目到 LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API 密钥",
|
||||
"form.integration.linkace_check_disabled": "关闭链接检查",
|
||||
"form.integration.linkace_endpoint": "LinkAce API URL",
|
||||
"form.integration.linkace_check_disabled": "禁用链接检查",
|
||||
"form.integration.linkace_endpoint": "LinkAce API 端点",
|
||||
"form.integration.linkace_is_private": "将链接标记为私有",
|
||||
"form.integration.linkace_tags": "LinkAce 标签",
|
||||
"form.integration.linkding_activate": "保存文章到 Linkding",
|
||||
"form.integration.linkding_activate": "保存条目到 Linkding",
|
||||
"form.integration.linkding_api_key": "Linkding API 密钥",
|
||||
"form.integration.linkding_bookmark": "标记为未读",
|
||||
"form.integration.linkding_bookmark": "将书签标记为未读",
|
||||
"form.integration.linkding_endpoint": "Linkding API 端点",
|
||||
"form.integration.linkding_tags": "Linkding 默认标签",
|
||||
"form.integration.linkwarden_activate": "保存文章到 Linkwarden",
|
||||
"form.integration.linkding_tags": "Linkding 标签",
|
||||
"form.integration.linkwarden_activate": "保存条目到 Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API 密钥",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden API 端点",
|
||||
"form.integration.matrix_bot_activate": "将新文章推送到 Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Matrix 聊天 ID",
|
||||
"form.integration.matrix_bot_password": "Matrix Bot 密码",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden 基本 URL",
|
||||
"form.integration.matrix_bot_activate": "推送新条目到 Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Matrix 房间 ID",
|
||||
"form.integration.matrix_bot_password": "Matrix 用户密码",
|
||||
"form.integration.matrix_bot_url": "Matrix 服务器 URL",
|
||||
"form.integration.matrix_bot_user": "Matrix Bot 用户名",
|
||||
"form.integration.notion_activate": "保存文章到 Notion",
|
||||
"form.integration.notion_page_id": "Notion 页面ID",
|
||||
"form.integration.notion_token": "Notion 密钥",
|
||||
"form.integration.ntfy_activate": "推送条目到ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API令牌(可选)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy图标URL(可选)",
|
||||
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy密码(可选)",
|
||||
"form.integration.ntfy_topic": "Ntfy主题(預設,如果未在此源中定義)",
|
||||
"form.integration.ntfy_url": "Ntfy URL(可选,默认为ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Ntfy用户名(可选)",
|
||||
"form.integration.nunux_keeper_activate": "保存文章到 Nunux Keeper",
|
||||
"form.integration.matrix_bot_user": "Matrix 用户名",
|
||||
"form.integration.notion_activate": "保存条目到 Notion",
|
||||
"form.integration.notion_page_id": "Notion 页面 ID",
|
||||
"form.integration.notion_token": "Notion 密钥令牌",
|
||||
"form.integration.ntfy_activate": "推送条目到 Ntfy",
|
||||
"form.integration.ntfy_api_token": "Ntfy API 令牌(可选)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy 图标 URL(可选)",
|
||||
"form.integration.ntfy_internal_links": "点击时使用内部链接(可选)",
|
||||
"form.integration.ntfy_password": "Ntfy 密码(可选)",
|
||||
"form.integration.ntfy_topic": "Ntfy 主题(如果订阅源中未设置则使用默认值)",
|
||||
"form.integration.ntfy_url": "Ntfy URL(可选,默认为 ntfy.sh)",
|
||||
"form.integration.ntfy_username": "Ntfy 用户名(可选)",
|
||||
"form.integration.nunux_keeper_activate": "保存条目到 Nunux Keeper",
|
||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper API 密钥",
|
||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API 端点",
|
||||
"form.integration.omnivore_activate": "保存文章到 Omnivore",
|
||||
"form.integration.omnivore_activate": "保存条目到 Omnivore",
|
||||
"form.integration.omnivore_api_key": "Omnivore API 密钥",
|
||||
"form.integration.omnivore_url": "Omnivore API 端点",
|
||||
"form.integration.pinboard_activate": "保存文章到 Pinboard",
|
||||
"form.integration.pinboard_bookmark": "标记为未读",
|
||||
"form.integration.pinboard_activate": "保存条目到 Pinboard",
|
||||
"form.integration.pinboard_bookmark": "将书签标记为未读",
|
||||
"form.integration.pinboard_tags": "Pinboard 标签",
|
||||
"form.integration.pinboard_token": "Pinboard API 密钥",
|
||||
"form.integration.pocket_access_token": "Pocket 访问密钥",
|
||||
"form.integration.pocket_activate": "将文章保存到 Pocket",
|
||||
"form.integration.pocket_connect_link": "连接您的 Pocket 帐户",
|
||||
"form.integration.pocket_consumer_key": "Pocket 用户密钥",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
"form.integration.pushover_token": "Pushover application API token",
|
||||
"form.integration.pushover_user": "Pushover user key",
|
||||
"form.integration.raindrop_activate": "保存文章到 Raindrop",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.raindrop_token": "(Test) 密钥",
|
||||
"form.integration.readeck_activate": "保存文章到 Readeck",
|
||||
"form.integration.pinboard_token": "Pinboard API 令牌",
|
||||
"form.integration.pushover_activate": "推送条目到 Pushover",
|
||||
"form.integration.pushover_device": "Pushover 设备(可选)",
|
||||
"form.integration.pushover_prefix": "Pushover URL 前缀(可选)",
|
||||
"form.integration.pushover_token": "Pushover 应用 API 令牌",
|
||||
"form.integration.pushover_user": "Pushover 用户密钥",
|
||||
"form.integration.raindrop_activate": "保存条目到 Raindrop",
|
||||
"form.integration.raindrop_collection_id": "集合 ID",
|
||||
"form.integration.raindrop_tags": "标签(逗号分隔)",
|
||||
"form.integration.raindrop_token": "(测试)令牌",
|
||||
"form.integration.readeck_activate": "保存条目到 Readeck",
|
||||
"form.integration.readeck_api_key": "Readeck API 密钥",
|
||||
"form.integration.readeck_endpoint": "Readeck API 端点",
|
||||
"form.integration.readeck_labels": "Readeck 默认标签",
|
||||
"form.integration.readeck_only_url": "仅发送 URL(而不是完整内容)",
|
||||
"form.integration.readwise_activate": "保存文章到 Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader 密钥",
|
||||
"form.integration.readwise_api_key_link": "获取你的 Readwise 密钥",
|
||||
"form.integration.readeck_labels": "Readeck 标签",
|
||||
"form.integration.readeck_only_url": "仅发送 URL(而非完整内容)",
|
||||
"form.integration.readwise_activate": "保存条目到 Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader 访问令牌",
|
||||
"form.integration.readwise_api_key_link": "获取你的 Readwise 访问令牌",
|
||||
"form.integration.rssbridge_activate": "添加订阅时检查 RSS-Bridge",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge 认证令牌",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge 服务器 URL",
|
||||
"form.integration.shaarli_activate": "保存文章到 Shaarli",
|
||||
"form.integration.shaarli_activate": "保存条目到 Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API 密钥",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shiori_activate": "保存文章到 Shiori",
|
||||
"form.integration.shiori_activate": "保存条目到 Shiori",
|
||||
"form.integration.shiori_endpoint": "Shiori API 端点",
|
||||
"form.integration.shiori_password": "Shiori 密码",
|
||||
"form.integration.shiori_username": "Shiori 用户名",
|
||||
"form.integration.slack_activate": "将新文章推送到 Slack",
|
||||
"form.integration.slack_webhook_link": "Slack Webhook link",
|
||||
"form.integration.telegram_bot_activate": "将新文章推送到 Telegram",
|
||||
"form.integration.telegram_bot_disable_buttons": "不展示按钮",
|
||||
"form.integration.slack_activate": "推送条目到 Slack",
|
||||
"form.integration.slack_webhook_link": "Slack Webhook 链接",
|
||||
"form.integration.telegram_bot_activate": "推送新条目到 Telegram 聊天",
|
||||
"form.integration.telegram_bot_disable_buttons": "禁用按钮",
|
||||
"form.integration.telegram_bot_disable_notification": "禁用通知",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "禁用网页预览",
|
||||
"form.integration.telegram_bot_token": "机器人令牌",
|
||||
"form.integration.telegram_chat_id": "聊天ID",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
"form.integration.wallabag_activate": "保存文章到 Wallabag",
|
||||
"form.integration.telegram_chat_id": "聊天 ID",
|
||||
"form.integration.telegram_topic_id": "主题 ID",
|
||||
"form.integration.wallabag_activate": "保存条目到 Wallabag",
|
||||
"form.integration.wallabag_client_id": "Wallabag 客户端 ID",
|
||||
"form.integration.wallabag_client_secret": "Wallabag 客户端 密钥",
|
||||
"form.integration.wallabag_endpoint": "Wallabag 基本 URL",
|
||||
"form.integration.wallabag_only_url": "仅发送 URL(而不是完整内容)",
|
||||
"form.integration.wallabag_client_secret": "Wallabag 客户端密钥",
|
||||
"form.integration.wallabag_endpoint": "Wallabag 基础 URL",
|
||||
"form.integration.wallabag_only_url": "仅发送 URL(而非完整内容)",
|
||||
"form.integration.wallabag_password": "Wallabag 密码",
|
||||
"form.integration.wallabag_username": "Wallabag 用户名",
|
||||
"form.integration.webhook_activate": "启用 Webhooks",
|
||||
"form.integration.webhook_secret": "Webhooks 密钥",
|
||||
"form.integration.webhook_url": "Default Webhook URL",
|
||||
"form.integration.webhook_url": "默认 Webhook URL",
|
||||
"form.prefs.fieldset.application_settings": "应用设置",
|
||||
"form.prefs.fieldset.authentication_settings": "用户认证设置",
|
||||
"form.prefs.fieldset.authentication_settings": "认证设置",
|
||||
"form.prefs.fieldset.global_feed_settings": "全局订阅源设置",
|
||||
"form.prefs.fieldset.reader_settings": "阅读器设置",
|
||||
"form.prefs.help.external_font_hosts": "允许外部字体托管的空格分隔列表。例如:\"fonts.gstatic.com fonts.googleapis.com\"。",
|
||||
"form.prefs.label.always_open_external_links": "打开外部链接阅读条目",
|
||||
"form.prefs.label.categories_sorting_order": "分类排序",
|
||||
"form.prefs.label.cjk_reading_speed": "中文、韩文和日文的阅读速度(每分钟字符数)",
|
||||
"form.prefs.label.custom_css": "自定义 CSS",
|
||||
"form.prefs.label.custom_js": "自定义 JavaScript",
|
||||
"form.prefs.label.default_home_page": "默认主页",
|
||||
"form.prefs.label.default_reading_speed": "其他语言的阅读速度(每分钟字数)",
|
||||
"form.prefs.label.display_mode": "渐进式网络应用程序 (PWA) 显示模式",
|
||||
"form.prefs.label.entries_per_page": "每页文章数",
|
||||
"form.prefs.label.entry_order": "文章排序依据",
|
||||
"form.prefs.label.entry_sorting": "文章排序",
|
||||
"form.prefs.label.entry_swipe": "在触摸屏上启用输入滑动",
|
||||
"form.prefs.label.external_font_hosts": "外部字体托管",
|
||||
"form.prefs.label.gesture_nav": "在条目之间导航的手势",
|
||||
"form.prefs.label.display_mode": "渐进式网络应用程序(PWA)显示模式",
|
||||
"form.prefs.label.entries_per_page": "每页条目数",
|
||||
"form.prefs.label.entry_order": "条目排序字段",
|
||||
"form.prefs.label.entry_sorting": "条目排序",
|
||||
"form.prefs.label.entry_swipe": "在触摸屏上启用条目滑动",
|
||||
"form.prefs.label.external_font_hosts": "外部字体主机",
|
||||
"form.prefs.label.gesture_nav": "在条目间导航的手势",
|
||||
"form.prefs.label.keyboard_shortcuts": "启用键盘快捷键",
|
||||
"form.prefs.label.language": "语言",
|
||||
"form.prefs.label.mark_read_manually": "手动标记条目为已读",
|
||||
"form.prefs.label.mark_read_on_media_completion": "仅当音频/视频播放完成90%%时标记为已读",
|
||||
"form.prefs.label.mark_read_on_media_completion": "仅当音频/视频播放完成 90%% 时标记为已读",
|
||||
"form.prefs.label.mark_read_on_view": "查看时自动将条目标记为已读",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "当浏览时标记条目为已读。对于音频/视频,当播放完成90%%时标记为已读",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "当浏览时标记条目为已读。对于音频/视频,当播放完成 90%% 时标记为已读",
|
||||
"form.prefs.label.media_playback_rate": "音频/视频的播放速度",
|
||||
"form.prefs.label.show_reading_time": "显示文章的预计阅读时间",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "在新标签页中打开外部链接(为链接添加 target=\"_blank\")",
|
||||
"form.prefs.label.show_reading_time": "显示条目的预计阅读时间",
|
||||
"form.prefs.label.theme": "主题",
|
||||
"form.prefs.label.timezone": "时区",
|
||||
"form.prefs.select.alphabetical": "按字母顺序",
|
||||
"form.prefs.select.alphabetical": "字母顺序",
|
||||
"form.prefs.select.browser": "浏览器",
|
||||
"form.prefs.select.created_time": "文章创建时间",
|
||||
"form.prefs.select.created_time": "条目创建时间",
|
||||
"form.prefs.select.fullscreen": "全屏",
|
||||
"form.prefs.select.minimal_ui": "最小",
|
||||
"form.prefs.select.none": "没有任何",
|
||||
"form.prefs.select.older_first": "旧->新",
|
||||
"form.prefs.select.publish_time": "文章发布时间",
|
||||
"form.prefs.select.publish_time": "条目发布时间",
|
||||
"form.prefs.select.recent_first": "新->旧",
|
||||
"form.prefs.select.standalone": "独立",
|
||||
"form.prefs.select.swipe": "滑动",
|
||||
"form.prefs.select.tap": "双击",
|
||||
"form.prefs.select.unread_count": "未读计数",
|
||||
"form.submit.loading": "载入中…",
|
||||
"form.submit.loading": "加载中…",
|
||||
"form.submit.saving": "保存中…",
|
||||
"form.user.label.admin": "管理员",
|
||||
"form.user.label.confirmation": "再次输入密码",
|
||||
"form.user.label.confirmation": "确认密码",
|
||||
"form.user.label.password": "密码",
|
||||
"form.user.label.username": "用户名",
|
||||
"menu.about": "关于",
|
||||
"menu.add_feed": "新增源",
|
||||
"menu.add_user": "新建用户",
|
||||
"menu.add_feed": "添加订阅源",
|
||||
"menu.add_user": "添加用户",
|
||||
"menu.api_keys": "API 密钥",
|
||||
"menu.categories": "分类",
|
||||
"menu.create_api_key": "创建一个新的 API 密钥",
|
||||
"menu.create_category": "新建分类",
|
||||
"menu.create_api_key": "创建新 API 密钥",
|
||||
"menu.create_category": "创建分类",
|
||||
"menu.edit_category": "编辑",
|
||||
"menu.edit_feed": "编辑",
|
||||
"menu.export": "导出",
|
||||
"menu.feed_entries": "文章",
|
||||
"menu.feeds": "源",
|
||||
"menu.flush_history": "清理历史",
|
||||
"menu.history": "历史",
|
||||
"menu.home_page": "首页",
|
||||
"menu.feed_entries": "条目",
|
||||
"menu.feeds": "订阅源",
|
||||
"menu.flush_history": "清除历史记录",
|
||||
"menu.history": "历史记录",
|
||||
"menu.home_page": "主页",
|
||||
"menu.import": "导入",
|
||||
"menu.integrations": "集成",
|
||||
"menu.logout": "登出",
|
||||
"menu.mark_all_as_read": "全部标为已读",
|
||||
"menu.mark_page_as_read": "标记为已读",
|
||||
"menu.preferences": "设置",
|
||||
"menu.refresh_all_feeds": "在后台更新全部源",
|
||||
"menu.refresh_feed": "更新",
|
||||
"menu.mark_page_as_read": "将此页标为已读",
|
||||
"menu.preferences": "偏好设置",
|
||||
"menu.refresh_all_feeds": "后台刷新所有订阅源",
|
||||
"menu.refresh_feed": "刷新",
|
||||
"menu.search": "搜索",
|
||||
"menu.sessions": "会话",
|
||||
"menu.settings": "设置",
|
||||
"menu.shared_entries": "已分享的文章",
|
||||
"menu.show_all_entries": "显示所有文章",
|
||||
"menu.show_only_starred_entries": "仅显示已收藏文章",
|
||||
"menu.show_only_unread_entries": "仅显示未读文章",
|
||||
"menu.shared_entries": "已共享的条目",
|
||||
"menu.show_all_entries": "显示所有条目",
|
||||
"menu.show_only_starred_entries": "仅显示已收藏条目",
|
||||
"menu.show_only_unread_entries": "仅显示未读条目",
|
||||
"menu.starred": "收藏",
|
||||
"menu.title": "菜单",
|
||||
"menu.unread": "未读",
|
||||
"menu.users": "用户",
|
||||
"page.about.author": "作者:",
|
||||
"page.about.build_date": "构建日期:",
|
||||
"page.about.credits": "版权",
|
||||
"page.about.db_usage": "数据库容量",
|
||||
"page.about.credits": "鸣谢",
|
||||
"page.about.db_usage": "数据库大小:",
|
||||
"page.about.git_commit": "Git 提交:",
|
||||
"page.about.global_config_options": "全局配置选项",
|
||||
"page.about.go_version": "Go 版本号:",
|
||||
"page.about.license": "协议:",
|
||||
"page.about.postgres_version": "Postgres 版本号:",
|
||||
"page.about.go_version": "Go 版本:",
|
||||
"page.about.license": "许可证:",
|
||||
"page.about.postgres_version": "Postgres 版本:",
|
||||
"page.about.title": "关于",
|
||||
"page.about.version": "版本号:",
|
||||
"page.add_feed.choose_feed": "选择一个源",
|
||||
"page.add_feed.label.url": "网址",
|
||||
"page.about.version": "版本:",
|
||||
"page.add_feed.choose_feed": "选择订阅源",
|
||||
"page.add_feed.label.url": "URL",
|
||||
"page.add_feed.legend.advanced_options": "高级选项",
|
||||
"page.add_feed.no_category": "没有类别,至少需要有一个类别",
|
||||
"page.add_feed.submit": "查找源",
|
||||
"page.add_feed.title": "新增源",
|
||||
"page.api_keys.never_used": "没用过",
|
||||
"page.add_feed.no_category": "没有分类。您必须至少有一个分类。",
|
||||
"page.add_feed.submit": "查找订阅源",
|
||||
"page.add_feed.title": "新建订阅源",
|
||||
"page.api_keys.never_used": "从未使用",
|
||||
"page.api_keys.table.actions": "操作",
|
||||
"page.api_keys.table.created_at": "创建日期",
|
||||
"page.api_keys.table.description": "描述",
|
||||
"page.api_keys.table.last_used_at": "最后使用",
|
||||
"page.api_keys.table.token": "密钥",
|
||||
"page.api_keys.table.token": "令牌",
|
||||
"page.api_keys.title": "API 密钥",
|
||||
"page.categories_count": [
|
||||
"%d 分类"
|
||||
],
|
||||
"page.categories.entries": "查看内容",
|
||||
"page.categories.entries": "条目",
|
||||
"page.categories.feed_count": [
|
||||
"有 %d 个源"
|
||||
"有 %d 个订阅源"
|
||||
],
|
||||
"page.categories.feeds": "查看源",
|
||||
"page.categories.no_feed": "没有源",
|
||||
"page.categories.feeds": "订阅源",
|
||||
"page.categories.no_feed": "无订阅源。",
|
||||
"page.categories.title": "分类",
|
||||
"page.categories_count": [
|
||||
"%d 个分类"
|
||||
],
|
||||
"page.category_label": "分类: %s",
|
||||
"page.edit_category.title": "编辑分类 : %s",
|
||||
"page.edit_category.title": "编辑分类:%s",
|
||||
"page.edit_feed.etag_header": "ETag 标题:",
|
||||
"page.edit_feed.last_check": "最后检查时间:",
|
||||
"page.edit_feed.last_modified_header": "最后修改的 Header:",
|
||||
"page.edit_feed.last_parsing_error": "最后一次解析错误",
|
||||
"page.edit_feed.no_header": "无 Header",
|
||||
"page.edit_feed.title": "编辑源 : %s",
|
||||
"page.edit_user.title": "编辑用户 : %s",
|
||||
"page.edit_feed.title": "编辑订阅源: %s",
|
||||
"page.edit_user.title": "编辑用户: %s",
|
||||
"page.entry.attachments": "附件",
|
||||
"page.feeds.error_count": [
|
||||
"%d 错误"
|
||||
],
|
||||
"page.feeds.last_check": "最后检查时间:",
|
||||
"page.feeds.next_check": "下次检查时间:",
|
||||
"page.feeds.read_counter": "已读文章数",
|
||||
"page.feeds.title": "源",
|
||||
"page.history.title": "历史",
|
||||
"page.feeds.last_check": "最后检查:",
|
||||
"page.feeds.next_check": "下次检查:",
|
||||
"page.feeds.read_counter": "已读条目数",
|
||||
"page.feeds.title": "订阅源",
|
||||
"page.history.title": "历史记录",
|
||||
"page.import.title": "导入",
|
||||
"page.integration.bookmarklet": "书签小应用",
|
||||
"page.integration.bookmarklet.help": "你可以打开这个特殊的书签来直接收藏网站",
|
||||
"page.integration.bookmarklet.instructions": "拖动这个链接到浏览器书签栏",
|
||||
"page.integration.bookmarklet.name": "收藏 Miniflux",
|
||||
"page.integration.bookmarklet.help": "此链接允许您通过浏览器书签直接订阅网站。",
|
||||
"page.integration.bookmarklet.instructions": "将此链接拖动到您的书签栏。",
|
||||
"page.integration.bookmarklet.name": "添加到 Miniflux",
|
||||
"page.integration.miniflux_api": "Miniflux API",
|
||||
"page.integration.miniflux_api_endpoint": "API 端点",
|
||||
"page.integration.miniflux_api_password": "密码",
|
||||
"page.integration.miniflux_api_password_value": "您账户的密码",
|
||||
"page.integration.miniflux_api_password_value": "您账号的密码",
|
||||
"page.integration.miniflux_api_username": "用户名",
|
||||
"page.integrations.title": "集成",
|
||||
"page.keyboard_shortcuts.close_modal": "关闭对话窗口",
|
||||
"page.keyboard_shortcuts.download_content": "抓取全文内容",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "转到底部项目",
|
||||
"page.keyboard_shortcuts.go_to_categories": "打开分类页面",
|
||||
"page.keyboard_shortcuts.go_to_feed": "转到源页面",
|
||||
"page.keyboard_shortcuts.go_to_feeds": "打开源页面",
|
||||
"page.keyboard_shortcuts.go_to_history": "打开历史页面",
|
||||
"page.keyboard_shortcuts.go_to_next_item": "下一文章",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "下一页",
|
||||
"page.keyboard_shortcuts.go_to_previous_item": "上一文章",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "上一页",
|
||||
"page.keyboard_shortcuts.go_to_search": "将焦点放在搜索表单上",
|
||||
"page.keyboard_shortcuts.go_to_settings": "打开设置页面",
|
||||
"page.keyboard_shortcuts.go_to_starred": "打开收藏页面",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "转到顶部项目",
|
||||
"page.keyboard_shortcuts.go_to_unread": "打开未读页面",
|
||||
"page.keyboard_shortcuts.mark_page_as_read": "标记当前页已读",
|
||||
"page.keyboard_shortcuts.download_content": "下载原始内容",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "跳转到最后一条",
|
||||
"page.keyboard_shortcuts.go_to_categories": "转到分类",
|
||||
"page.keyboard_shortcuts.go_to_feed": "转到订阅源",
|
||||
"page.keyboard_shortcuts.go_to_feeds": "转到订阅源列表",
|
||||
"page.keyboard_shortcuts.go_to_history": "转到历史记录",
|
||||
"page.keyboard_shortcuts.go_to_next_item": "转到下一条目",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "转到下一页",
|
||||
"page.keyboard_shortcuts.go_to_previous_item": "转到上一条目",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "转到上一页",
|
||||
"page.keyboard_shortcuts.go_to_search": "聚焦到搜索框",
|
||||
"page.keyboard_shortcuts.go_to_settings": "转到设置",
|
||||
"page.keyboard_shortcuts.go_to_starred": "转到收藏",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "转到第一条",
|
||||
"page.keyboard_shortcuts.go_to_unread": "转到未读",
|
||||
"page.keyboard_shortcuts.mark_page_as_read": "标记当前页为已读",
|
||||
"page.keyboard_shortcuts.open_comments": "打开评论链接",
|
||||
"page.keyboard_shortcuts.open_comments_same_window": "在当前标签页中打开评论链接",
|
||||
"page.keyboard_shortcuts.open_item": "打开选定的文章",
|
||||
"page.keyboard_shortcuts.open_item": "打开选定的条目",
|
||||
"page.keyboard_shortcuts.open_original": "打开原始链接",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "在当前标签页中打开原始链接",
|
||||
"page.keyboard_shortcuts.refresh_all_feeds": "在后台更新全部源",
|
||||
"page.keyboard_shortcuts.remove_feed": "删除此源",
|
||||
"page.keyboard_shortcuts.save_article": "保存文章",
|
||||
"page.keyboard_shortcuts.refresh_all_feeds": "在后台刷新全部订阅源",
|
||||
"page.keyboard_shortcuts.remove_feed": "移除此订阅源",
|
||||
"page.keyboard_shortcuts.save_article": "保存条目",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "滚动到顶部",
|
||||
"page.keyboard_shortcuts.show_keyboard_shortcuts": "显示快捷键帮助",
|
||||
"page.keyboard_shortcuts.subtitle.actions": "操作",
|
||||
"page.keyboard_shortcuts.subtitle.items": "文章导航",
|
||||
"page.keyboard_shortcuts.subtitle.items": "条目导航",
|
||||
"page.keyboard_shortcuts.subtitle.pages": "页面导航",
|
||||
"page.keyboard_shortcuts.subtitle.sections": "分区导航",
|
||||
"page.keyboard_shortcuts.title": "快捷键",
|
||||
"page.keyboard_shortcuts.subtitle.sections": "区域导航",
|
||||
"page.keyboard_shortcuts.title": "键盘快捷键",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "切换收藏状态",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "展开/折叠文章附件",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "切换已读/未读状态, 关注下一个",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "切换已读/未读状态, 关注前一个",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "切换展开/折叠条目附件",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "切换已读/未读状态,并切换到下一项",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "切换已读/未读状态,并切换到上一项",
|
||||
"page.login.google_signin": "使用 Google 登录",
|
||||
"page.login.oidc_signin": "使用 %s 登录",
|
||||
"page.login.title": "登录",
|
||||
"page.login.webauthn_login": "使用密码登录",
|
||||
"page.login.webauthn_login.error": "无法使用密码登录",
|
||||
"page.login.webauthn_login.help": "如果您正在使用安全密钥,请输入您的用户名。如果您正在使用通行密钥(可发现凭证),则无需输入用户名。",
|
||||
"page.login.webauthn_login": "使用通行密钥登录",
|
||||
"page.login.webauthn_login.error": "无法使用通行密钥登录",
|
||||
"page.login.webauthn_login.help": "如果您正在使用安全密钥,请输入您的用户名。如果您正在使用通行密钥(可发现凭证),则无需输入。",
|
||||
"page.new_api_key.title": "新的 API 密钥",
|
||||
"page.new_category.title": "新分类",
|
||||
"page.new_user.title": "新用户",
|
||||
"page.new_category.title": "新建分类",
|
||||
"page.new_user.title": "新建用户",
|
||||
"page.offline.message": "您已离线",
|
||||
"page.offline.refresh_page": "尝试刷新页面",
|
||||
"page.offline.title": "离线模式",
|
||||
"page.read_entry_count": [
|
||||
"%d 阅读文章"
|
||||
"%d 个已读条目"
|
||||
],
|
||||
"page.search.title": "搜索结果",
|
||||
"page.sessions.table.actions": "操作",
|
||||
@@ -516,52 +526,52 @@
|
||||
"page.sessions.table.ip": "IP 地址",
|
||||
"page.sessions.table.user_agent": "用户代理",
|
||||
"page.sessions.title": "会话",
|
||||
"page.settings.link_google_account": "关联我的 Google 账户",
|
||||
"page.settings.link_oidc_account": "关联我的 %s 账户",
|
||||
"page.settings.link_google_account": "关联我的 Google 账号",
|
||||
"page.settings.link_oidc_account": "关联我的 %s 账号",
|
||||
"page.settings.title": "设置",
|
||||
"page.settings.unlink_google_account": "解除 Google 账号关联",
|
||||
"page.settings.unlink_oidc_account": "解除 %s 账号关联",
|
||||
"page.settings.webauthn.actions": "操作",
|
||||
"page.settings.webauthn.added_on": "添加时间",
|
||||
"page.settings.webauthn.added_on": "添加于",
|
||||
"page.settings.webauthn.delete": [
|
||||
"删除 %d 个 Passkey"
|
||||
"删除 %d 个通行密钥"
|
||||
],
|
||||
"page.settings.webauthn.last_seen_on": "最后使用时间",
|
||||
"page.settings.webauthn.passkey_name": "Passkey 名称",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.register": "注册 Passkey",
|
||||
"page.settings.webauthn.register.error": "无法注册 Passkey",
|
||||
"page.settings.webauthn.last_seen_on": "最后使用",
|
||||
"page.settings.webauthn.passkey_name": "通行密钥名称",
|
||||
"page.settings.webauthn.passkeys": "通行密钥",
|
||||
"page.settings.webauthn.register": "注册通行密钥",
|
||||
"page.settings.webauthn.register.error": "无法注册通行密钥",
|
||||
"page.shared_entries.title": "已共享的条目",
|
||||
"page.shared_entries_count": [
|
||||
"%d 已分享的文章"
|
||||
],
|
||||
"page.shared_entries.title": "已分享的文章",
|
||||
"page.starred_entry_count": [
|
||||
"%d 收藏的文章"
|
||||
"%d 个共享条目"
|
||||
],
|
||||
"page.starred.title": "收藏",
|
||||
"page.total_entry_count": [
|
||||
"%d 文章总数"
|
||||
"page.starred_entry_count": [
|
||||
"%d 个收藏条目"
|
||||
],
|
||||
"page.unread_entry_count": [
|
||||
"%d 未读的文章"
|
||||
"page.total_entry_count": [
|
||||
"%d 个条目"
|
||||
],
|
||||
"page.unread.title": "未读",
|
||||
"page.unread_entry_count": [
|
||||
"%d 个未读条目"
|
||||
],
|
||||
"page.users.actions": "操作",
|
||||
"page.users.admin.no": "否",
|
||||
"page.users.admin.yes": "是",
|
||||
"page.users.is_admin": "管理员",
|
||||
"page.users.last_login": "最后登录时间",
|
||||
"page.users.never_logged": "从未登录",
|
||||
"page.users.last_login": "最后登录",
|
||||
"page.users.never_logged": "从未",
|
||||
"page.users.title": "用户",
|
||||
"page.users.username": "用户名",
|
||||
"page.webauthn_rename.title": "重命名 Passkey",
|
||||
"page.webauthn_rename.title": "重命名通行密钥",
|
||||
"pagination.first": "第一页",
|
||||
"pagination.last": "最后一页",
|
||||
"pagination.next": "下一页",
|
||||
"pagination.previous": "上一页",
|
||||
"search.label": "搜索",
|
||||
"search.placeholder": "搜索…",
|
||||
"search.submit": "查找",
|
||||
"search.submit": "搜索",
|
||||
"skip_to_content": "跳转至内容",
|
||||
"time_elapsed.days": [
|
||||
"%d 天前"
|
||||
@@ -584,6 +594,6 @@
|
||||
"%d 年前"
|
||||
],
|
||||
"time_elapsed.yesterday": "昨天",
|
||||
"tooltip.keyboard_shortcuts": "快捷键: %s",
|
||||
"tooltip.logged_user": "当前登录 %s"
|
||||
}
|
||||
"tooltip.keyboard_shortcuts": "键盘快捷键:%s",
|
||||
"tooltip.logged_user": "登录用户:%s"
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"alert.no_tag_entry": "沒有與此標籤相符的文章。",
|
||||
"alert.no_unread_entry": "目前沒有未讀文章",
|
||||
"alert.no_user": "您是唯一的使用者",
|
||||
"alert.pocket_linked": "您的 Pocket 帳戶已關聯",
|
||||
"alert.prefs_saved": "設定已儲存!",
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"您已觸發過太多次 Feed 更新,請等待 %d 分鐘後再嘗試。"
|
||||
@@ -70,11 +69,14 @@
|
||||
"entry.state.loading": "載入中…",
|
||||
"entry.state.saving": "儲存中…",
|
||||
"entry.status.mark_as_read": "標記為已讀",
|
||||
"entry.status.mark_as_unread": "標記為未讀",
|
||||
"entry.status.title": "更改狀態",
|
||||
"entry.status.toast.read": "已標記為已讀",
|
||||
"entry.status.toast.unread": "已標記為未讀",
|
||||
"entry.status.mark_as_unread": "標記為未讀",
|
||||
"entry.tags.label": "標籤:",
|
||||
"entry.tags.more_tags_label": [
|
||||
"還有 %d 個標籤"
|
||||
],
|
||||
"entry.unshare.label": "取消分享",
|
||||
"error.api_key_already_exists": "此 API 金鑰已存在。",
|
||||
"error.bad_credentials": "使用者名稱或密碼無效",
|
||||
@@ -112,9 +114,12 @@
|
||||
"error.http_service_unavailable": "此網站目前因內部問題無法使用,問題不在 Miniflux,請稍後重試。",
|
||||
"error.http_too_many_requests": "Miniflux 對此網站的請求過多,請稍後重試或調整程式設定。",
|
||||
"error.http_unexpected_status_code": "此網站回應了意外的 HTTP 狀態碼:%d,請稍後重試。",
|
||||
"error.invalid_categories_sorting_order": "無效的分類排序",
|
||||
"error.invalid_default_home_page": "預設主頁無效!",
|
||||
"error.invalid_display_mode": "無效的顯示模式。",
|
||||
"error.invalid_entry_direction": "無效的輸入方向。",
|
||||
"error.invalid_entry_order": "無效的文章排序依據。",
|
||||
"error.invalid_feed_proxy_url": "代理伺服器網址無效。",
|
||||
"error.invalid_feed_url": "訂閱網址無效。",
|
||||
"error.invalid_gesture_nav": "手勢導覽無效。",
|
||||
"error.invalid_language": "無效的語言。",
|
||||
@@ -124,8 +129,7 @@
|
||||
"error.network_operation": "Miniflux 無法連線到該網站,可能是網路問題:%v。",
|
||||
"error.network_timeout": "該網站回應過慢,請求逾時:%v。",
|
||||
"error.password_min_length": "請至少輸入 6 個字元",
|
||||
"error.pocket_access_token": "無法從 Pocket 取得存取金鑰!",
|
||||
"error.pocket_request_token": "無法從 Pocket 取得請求金鑰!",
|
||||
"error.proxy_url_not_empty": "代理伺服器網址不能為空。",
|
||||
"error.settings_block_rule_fieldname_invalid": "無效的封鎖規則:規則 #%d 缺少有效的欄位名稱 (可用選項:%s)",
|
||||
"error.settings_block_rule_invalid_regex": "無效的封鎖規則:規則 #%d 的模式不是合法的正規表示式",
|
||||
"error.settings_block_rule_regex_required": "無效的封鎖規則:規則 #%d 沒有提供正規表示式",
|
||||
@@ -162,7 +166,8 @@
|
||||
"form.feed.fieldset.rules": "規則",
|
||||
"form.feed.label.allow_self_signed_certificates": "允許自簽或無效的憑證",
|
||||
"form.feed.label.apprise_service_urls": "使用逗號分隔的 Apprise 服務網址列表",
|
||||
"form.feed.label.blocklist_rules": "過濾規則",
|
||||
"form.feed.label.block_filter_entry_rules": "條目封鎖規則",
|
||||
"form.feed.label.blocklist_rules": "基於正則表達式的封鎖過濾器",
|
||||
"form.feed.label.category": "類別",
|
||||
"form.feed.label.cookie": "設定 Cookies",
|
||||
"form.feed.label.crawler": "下載原文內容",
|
||||
@@ -172,10 +177,11 @@
|
||||
"form.feed.label.feed_password": "Feed 密碼",
|
||||
"form.feed.label.feed_url": "Feed 網址",
|
||||
"form.feed.label.feed_username": "Feed 使用者名稱",
|
||||
"form.feed.label.fetch_via_proxy": "透過代理取得",
|
||||
"form.feed.label.fetch_via_proxy": "使用應用程式層級設定的代理",
|
||||
"form.feed.label.hide_globally": "在全域未讀列表中隱藏文章",
|
||||
"form.feed.label.ignore_http_cache": "忽略 HTTP 快取",
|
||||
"form.feed.label.keeplist_rules": "保留規則",
|
||||
"form.feed.label.keep_filter_entry_rules": "條目允許規則",
|
||||
"form.feed.label.keeplist_rules": "基於正則表達式的保留過濾器",
|
||||
"form.feed.label.no_media_player": "無媒體播放器 (音訊/視訊)",
|
||||
"form.feed.label.ntfy_activate": "推送文章到 ntfy",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy 預設優先順序",
|
||||
@@ -185,27 +191,28 @@
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy 最低優先順序",
|
||||
"form.feed.label.ntfy_priority": "Ntfy 優先順序",
|
||||
"form.feed.label.ntfy_topic": "Ntfy topic (選填)",
|
||||
"form.feed.label.proxy_url": "代理URL",
|
||||
"form.feed.label.pushover_activate": "Push entries to pushover.net",
|
||||
"form.feed.label.pushover_default_priority": "Pushover default priority",
|
||||
"form.feed.label.pushover_high_priority": "Pushover high priority",
|
||||
"form.feed.label.pushover_low_priority": "Pushover low priority",
|
||||
"form.feed.label.pushover_max_priority": "Pushover max priority",
|
||||
"form.feed.label.pushover_min_priority": "Pushover min priority",
|
||||
"form.feed.label.pushover_priority": "Pushover message priority",
|
||||
"form.feed.label.rewrite_rules": "重寫規則",
|
||||
"form.feed.label.pushover_priority": "Pushover消息優先級",
|
||||
"form.feed.label.rewrite_rules": "內容重寫規則",
|
||||
"form.feed.label.scraper_rules": "抓取規則",
|
||||
"form.feed.label.site_url": "網站網址",
|
||||
"form.feed.label.title": "標題",
|
||||
"form.feed.label.urlrewrite_rules": "網址重寫規則",
|
||||
"form.feed.label.user_agent": "覆蓋預設的使用者代理",
|
||||
"form.feed.label.webhook_url": "Override webhook url",
|
||||
"form.feed.label.webhook_url": "覆蓋webhook URL",
|
||||
"form.import.label.file": "OPML 檔案",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.apprise_activate": "推送文章到 Apprise",
|
||||
"form.integration.apprise_services_url": "使用逗號分隔的 Apprise 服務網址列表",
|
||||
"form.integration.apprise_url": "Apprise API 網址",
|
||||
"form.integration.betula_activate": "儲存文章到 Betula",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.betula_token": "Betula令牌",
|
||||
"form.integration.betula_url": "Betula 伺服器網址",
|
||||
"form.integration.cubox_activate": "儲存文章到 Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API 連結",
|
||||
@@ -226,6 +233,9 @@
|
||||
"form.integration.instapaper_activate": "儲存文章到 Instapaper",
|
||||
"form.integration.instapaper_password": "Instapaper 密碼",
|
||||
"form.integration.instapaper_username": "Instapaper 使用者名稱",
|
||||
"form.integration.karakeep_activate": "儲存文章到 Karakeep",
|
||||
"form.integration.karakeep_api_key": "Karakeep API 金鑰",
|
||||
"form.integration.karakeep_url": "Karakeep API 端點",
|
||||
"form.integration.linkace_activate": "儲存文章到 LinkAce",
|
||||
"form.integration.linkace_api_key": "LinkAce API 金鑰",
|
||||
"form.integration.linkace_check_disabled": "停用連結檢查",
|
||||
@@ -239,7 +249,7 @@
|
||||
"form.integration.linkding_tags": "Linkding 標籤",
|
||||
"form.integration.linkwarden_activate": "儲存文章到 Linkwarden",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API 金鑰",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden API 端點",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden 基本 URL",
|
||||
"form.integration.matrix_bot_activate": "推送文章到 Matrix",
|
||||
"form.integration.matrix_bot_chat_id": "Matrix 房間 ID",
|
||||
"form.integration.matrix_bot_password": "Matrix 密碼",
|
||||
@@ -266,10 +276,6 @@
|
||||
"form.integration.pinboard_bookmark": "標記為未讀",
|
||||
"form.integration.pinboard_tags": "Pinboard 標籤",
|
||||
"form.integration.pinboard_token": "Pinboard API Token",
|
||||
"form.integration.pocket_access_token": "Pocket 存取金鑰",
|
||||
"form.integration.pocket_activate": "儲存文章到 Pocket",
|
||||
"form.integration.pocket_connect_link": "連線您的 Pocket 帳戶",
|
||||
"form.integration.pocket_consumer_key": "Pocket 使用者金鑰",
|
||||
"form.integration.pushover_activate": "Push entries to Pushover",
|
||||
"form.integration.pushover_device": "Pushover device (optional)",
|
||||
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
|
||||
@@ -288,6 +294,7 @@
|
||||
"form.integration.readwise_api_key": "Readwise Reader 存取金鑰",
|
||||
"form.integration.readwise_api_key_link": "取得您的 Readwise 存取金鑰",
|
||||
"form.integration.rssbridge_activate": "新增訂閱時檢查 RSS-Bridge",
|
||||
"form.integration.rssbridge_token": "RSS-Bridge authentication token",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge 伺服器的網址",
|
||||
"form.integration.shaarli_activate": "儲存文章到 Shaarli",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API 金鑰",
|
||||
@@ -320,6 +327,7 @@
|
||||
"form.prefs.fieldset.global_feed_settings": "全域 Feed 設定",
|
||||
"form.prefs.fieldset.reader_settings": "閱讀器設定",
|
||||
"form.prefs.help.external_font_hosts": "以空白分隔允許的外部字型來源。例如:「fonts.gstatic.com fonts.googleapis.com」。",
|
||||
"form.prefs.label.always_open_external_links": "Read articles by opening external links",
|
||||
"form.prefs.label.categories_sorting_order": "分類排序",
|
||||
"form.prefs.label.cjk_reading_speed": "中文、韓文和日文的閱讀速度(每分鐘字元數)",
|
||||
"form.prefs.label.custom_css": "自訂 CSS",
|
||||
@@ -340,6 +348,7 @@
|
||||
"form.prefs.label.mark_read_on_view": "檢視時自動將文章標記為已讀",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "檢視文章即標記為已讀;若是音訊/視訊則在 90% 播放完成時標記",
|
||||
"form.prefs.label.media_playback_rate": "音訊/視訊播放速度",
|
||||
"form.prefs.label.open_external_links_in_new_tab": "在新分頁中開啟外部連結(為連結加上 target=\"_blank\")",
|
||||
"form.prefs.label.show_reading_time": "顯示文章的預計閱讀時間",
|
||||
"form.prefs.label.theme": "主題",
|
||||
"form.prefs.label.timezone": "時區",
|
||||
@@ -400,6 +409,7 @@
|
||||
"page.about.build_date": "建構日期:",
|
||||
"page.about.credits": "版權",
|
||||
"page.about.db_usage": "Database size:",
|
||||
"page.about.git_commit": "Git Commit:",
|
||||
"page.about.global_config_options": "全域設定選項",
|
||||
"page.about.go_version": "Go 版本:",
|
||||
"page.about.license": "授權:",
|
||||
@@ -586,4 +596,4 @@
|
||||
"time_elapsed.yesterday": "昨天",
|
||||
"tooltip.keyboard_shortcuts": "快捷鍵:%s",
|
||||
"tooltip.logged_user": "目前登入 %s"
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
|
||||
func TestProxyFilterWithHttpDefault(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "http-only")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "http-only")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -39,8 +39,8 @@ func TestProxyFilterWithHttpDefault(t *testing.T) {
|
||||
|
||||
func TestProxyFilterWithHttpsDefault(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "http-only")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "http-only")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -63,7 +63,7 @@ func TestProxyFilterWithHttpsDefault(t *testing.T) {
|
||||
|
||||
func TestProxyFilterWithHttpNever(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "none")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "none")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -86,7 +86,7 @@ func TestProxyFilterWithHttpNever(t *testing.T) {
|
||||
|
||||
func TestProxyFilterWithHttpsNever(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "none")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "none")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -109,9 +109,9 @@ func TestProxyFilterWithHttpsNever(t *testing.T) {
|
||||
|
||||
func TestProxyFilterWithHttpAlways(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -134,9 +134,9 @@ func TestProxyFilterWithHttpAlways(t *testing.T) {
|
||||
|
||||
func TestProxyFilterWithHttpsAlways(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -159,9 +159,9 @@ func TestProxyFilterWithHttpsAlways(t *testing.T) {
|
||||
|
||||
func TestAbsoluteProxyFilterWithHttpsAlways(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -221,9 +221,9 @@ func TestAbsoluteProxyFilterWithCustomPortAndSubfolderInBaseURL(t *testing.T) {
|
||||
|
||||
func TestAbsoluteProxyFilterWithHttpsAlwaysAndAudioTag(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "audio")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "audio")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -246,9 +246,9 @@ func TestAbsoluteProxyFilterWithHttpsAlwaysAndAudioTag(t *testing.T) {
|
||||
|
||||
func TestProxyFilterWithHttpsAlwaysAndCustomProxyServer(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("PROXY_URL", "https://proxy-example/proxy")
|
||||
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()
|
||||
@@ -271,9 +271,9 @@ func TestProxyFilterWithHttpsAlwaysAndCustomProxyServer(t *testing.T) {
|
||||
|
||||
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")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
|
||||
os.Setenv("MEDIA_PROXY_CUSTOM_URL", "http://:8080example.com")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -321,8 +321,8 @@ func TestAbsoluteProxyFilterWithHttpsAlwaysAndCustomProxyServer(t *testing.T) {
|
||||
|
||||
func TestProxyFilterWithHttpInvalid(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "invalid")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "invalid")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -345,8 +345,8 @@ func TestProxyFilterWithHttpInvalid(t *testing.T) {
|
||||
|
||||
func TestProxyFilterWithHttpsInvalid(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "invalid")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "invalid")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -369,9 +369,9 @@ func TestProxyFilterWithHttpsInvalid(t *testing.T) {
|
||||
|
||||
func TestProxyFilterWithSrcset(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -394,9 +394,9 @@ func TestProxyFilterWithSrcset(t *testing.T) {
|
||||
|
||||
func TestProxyFilterWithEmptySrcset(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -419,9 +419,9 @@ func TestProxyFilterWithEmptySrcset(t *testing.T) {
|
||||
|
||||
func TestProxyFilterWithPictureSource(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -444,9 +444,9 @@ func TestProxyFilterWithPictureSource(t *testing.T) {
|
||||
|
||||
func TestProxyFilterOnlyNonHTTPWithPictureSource(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "https")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "https")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -469,8 +469,8 @@ func TestProxyFilterOnlyNonHTTPWithPictureSource(t *testing.T) {
|
||||
|
||||
func TestProxyWithImageDataURL(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -493,8 +493,8 @@ func TestProxyWithImageDataURL(t *testing.T) {
|
||||
|
||||
func TestProxyWithImageSourceDataURL(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -517,9 +517,9 @@ func TestProxyWithImageSourceDataURL(t *testing.T) {
|
||||
|
||||
func TestProxyFilterWithVideo(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "video")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "video")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -542,9 +542,9 @@ func TestProxyFilterWithVideo(t *testing.T) {
|
||||
|
||||
func TestProxyFilterVideoPoster(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("PROXY_OPTION", "all")
|
||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -567,9 +567,9 @@ func TestProxyFilterVideoPoster(t *testing.T) {
|
||||
|
||||
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")
|
||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image,video")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
@@ -589,3 +589,145 @@ func TestProxyFilterVideoPosterOnce(t *testing.T) {
|
||||
t.Errorf(`Not expected output: got %s`, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldProxifyURLWithMimeType(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
mediaURL string
|
||||
mediaMimeType string
|
||||
mediaProxyOption string
|
||||
mediaProxyResourceTypes []string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Empty URL should not be proxified",
|
||||
mediaURL: "",
|
||||
mediaMimeType: "image/jpeg",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"image"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Data URL should not be proxified",
|
||||
mediaURL: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
||||
mediaMimeType: "image/png",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"image"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "HTTP URL with all mode and matching MIME type should be proxified",
|
||||
mediaURL: "http://example.com/image.jpg",
|
||||
mediaMimeType: "image/jpeg",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"image"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "HTTPS URL with all mode and matching MIME type should be proxified",
|
||||
mediaURL: "https://example.com/image.jpg",
|
||||
mediaMimeType: "image/jpeg",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"image"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "HTTP URL with http-only mode and matching MIME type should be proxified",
|
||||
mediaURL: "http://example.com/image.jpg",
|
||||
mediaMimeType: "image/jpeg",
|
||||
mediaProxyOption: "http-only",
|
||||
mediaProxyResourceTypes: []string{"image"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "HTTPS URL with http-only mode should not be proxified",
|
||||
mediaURL: "https://example.com/image.jpg",
|
||||
mediaMimeType: "image/jpeg",
|
||||
mediaProxyOption: "http-only",
|
||||
mediaProxyResourceTypes: []string{"image"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "URL with none mode should not be proxified",
|
||||
mediaURL: "http://example.com/image.jpg",
|
||||
mediaMimeType: "image/jpeg",
|
||||
mediaProxyOption: "none",
|
||||
mediaProxyResourceTypes: []string{"image"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "URL with matching MIME type should be proxified",
|
||||
mediaURL: "http://example.com/video.mp4",
|
||||
mediaMimeType: "video/mp4",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"video"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "URL with non-matching MIME type should not be proxified",
|
||||
mediaURL: "http://example.com/video.mp4",
|
||||
mediaMimeType: "video/mp4",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"image"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "URL with multiple resource types and matching MIME type should be proxified",
|
||||
mediaURL: "http://example.com/audio.mp3",
|
||||
mediaMimeType: "audio/mp3",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"image", "audio", "video"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "URL with multiple resource types but non-matching MIME type should not be proxified",
|
||||
mediaURL: "http://example.com/document.pdf",
|
||||
mediaMimeType: "application/pdf",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"image", "audio", "video"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "URL with empty resource types should not be proxified",
|
||||
mediaURL: "http://example.com/image.jpg",
|
||||
mediaMimeType: "image/jpeg",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "URL with partial MIME type match should be proxified",
|
||||
mediaURL: "http://example.com/image.jpg",
|
||||
mediaMimeType: "image/jpeg",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"image"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "URL with audio MIME type and audio resource type should be proxified",
|
||||
mediaURL: "http://example.com/song.ogg",
|
||||
mediaMimeType: "audio/ogg",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"audio"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "URL with video MIME type and video resource type should be proxified",
|
||||
mediaURL: "http://example.com/movie.webm",
|
||||
mediaMimeType: "video/webm",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"video"},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := ShouldProxifyURLWithMimeType(tc.mediaURL, tc.mediaMimeType, tc.mediaProxyOption, tc.mediaProxyResourceTypes)
|
||||
if result != tc.expected {
|
||||
t.Errorf("Expected %v, got %v for URL: %s, MIME type: %s, proxy option: %s, resource types: %v",
|
||||
tc.expected, result, tc.mediaURL, tc.mediaMimeType, tc.mediaProxyOption, tc.mediaProxyResourceTypes)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func genericProxyRewriter(router *mux.Router, proxifyFunction urlProxyRewriter,
|
||||
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) {
|
||||
if shouldProxifyURL(srcAttrValue, proxyOption) {
|
||||
img.SetAttr("src", proxifyFunction(router, srcAttrValue))
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func genericProxyRewriter(router *mux.Router, proxifyFunction urlProxyRewriter,
|
||||
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) {
|
||||
if shouldProxifyURL(posterAttrValue, proxyOption) {
|
||||
video.SetAttr("poster", proxifyFunction(router, posterAttrValue))
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func genericProxyRewriter(router *mux.Router, proxifyFunction urlProxyRewriter,
|
||||
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) {
|
||||
if shouldProxifyURL(srcAttrValue, proxyOption) {
|
||||
audio.SetAttr("src", proxifyFunction(router, srcAttrValue))
|
||||
}
|
||||
}
|
||||
@@ -73,13 +73,13 @@ func genericProxyRewriter(router *mux.Router, proxifyFunction urlProxyRewriter,
|
||||
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) {
|
||||
if shouldProxifyURL(srcAttrValue, proxyOption) {
|
||||
video.SetAttr("src", proxifyFunction(router, srcAttrValue))
|
||||
}
|
||||
}
|
||||
|
||||
if posterAttrValue, ok := video.Attr("poster"); ok {
|
||||
if shouldProxy(posterAttrValue, proxyOption) {
|
||||
if shouldProxifyURL(posterAttrValue, proxyOption) {
|
||||
video.SetAttr("poster", proxifyFunction(router, posterAttrValue))
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func proxifySourceSet(element *goquery.Selection, router *mux.Router, proxifyFun
|
||||
imageCandidates := sanitizer.ParseSrcSetAttribute(srcsetAttrValue)
|
||||
|
||||
for _, imageCandidate := range imageCandidates {
|
||||
if shouldProxy(imageCandidate.ImageURL, proxyOption) {
|
||||
if shouldProxifyURL(imageCandidate.ImageURL, proxyOption) {
|
||||
imageCandidate.ImageURL = proxifyFunction(router, imageCandidate.ImageURL)
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,33 @@ func proxifySourceSet(element *goquery.Selection, router *mux.Router, proxifyFun
|
||||
element.SetAttr("srcset", imageCandidates.String())
|
||||
}
|
||||
|
||||
func shouldProxy(attrValue, proxyOption string) bool {
|
||||
return !strings.HasPrefix(attrValue, "data:") &&
|
||||
(proxyOption == "all" || !urllib.IsHTTPS(attrValue))
|
||||
// shouldProxifyURL checks if the media URL should be proxified based on the media proxy option and URL scheme.
|
||||
func shouldProxifyURL(mediaURL, mediaProxyOption string) bool {
|
||||
switch {
|
||||
case mediaURL == "":
|
||||
return false
|
||||
case strings.HasPrefix(mediaURL, "data:"):
|
||||
return false
|
||||
case mediaProxyOption == "all":
|
||||
return true
|
||||
case mediaProxyOption != "none" && !urllib.IsHTTPS(mediaURL):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldProxifyURLWithMimeType checks if the media URL should be proxified based on the media proxy option, URL scheme, and MIME type.
|
||||
func ShouldProxifyURLWithMimeType(mediaURL, mediaMimeType, mediaProxyOption string, mediaProxyResourceTypes []string) bool {
|
||||
if !shouldProxifyURL(mediaURL, mediaProxyOption) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, mediaType := range mediaProxyResourceTypes {
|
||||
if strings.HasPrefix(mediaMimeType, mediaType+"/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
+11
-17
@@ -5,28 +5,22 @@ package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/crypto"
|
||||
)
|
||||
|
||||
// APIKey represents an application API key.
|
||||
type APIKey struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
Token string
|
||||
Description string
|
||||
LastUsedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// NewAPIKey initializes a new APIKey.
|
||||
func NewAPIKey(userID int64, description string) *APIKey {
|
||||
return &APIKey{
|
||||
UserID: userID,
|
||||
Token: crypto.GenerateRandomString(32),
|
||||
Description: description,
|
||||
}
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Token string `json:"token"`
|
||||
Description string `json:"description"`
|
||||
LastUsedAt *time.Time `json:"last_used_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// APIKeys represents a collection of API Key.
|
||||
type APIKeys []*APIKey
|
||||
|
||||
// APIKeyCreationRequest represents the request to create a new API Key.
|
||||
type APIKeyCreationRequest struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
@@ -19,13 +19,12 @@ type SessionData struct {
|
||||
FlashErrorMessage string `json:"flash_error_message"`
|
||||
Language string `json:"language"`
|
||||
Theme string `json:"theme"`
|
||||
PocketRequestToken string `json:"pocket_request_token"`
|
||||
LastForceRefresh string `json:"last_force_refresh"`
|
||||
WebAuthnSessionData WebAuthnSession `json:"webauthn_session_data"`
|
||||
}
|
||||
|
||||
func (s *SessionData) String() string {
|
||||
return fmt.Sprintf(`CSRF=%q, OAuth2State=%q, OAuth2CodeVerifier=%q, FlashMsg=%q, FlashErrMsg=%q, Lang=%q, Theme=%q, PocketTkn=%q, LastForceRefresh=%s, WebAuthnSession=%q`,
|
||||
return fmt.Sprintf(`CSRF=%q, OAuth2State=%q, OAuth2CodeVerifier=%q, FlashMsg=%q, FlashErrMsg=%q, Lang=%q, Theme=%q, LastForceRefresh=%s, WebAuthnSession=%q`,
|
||||
s.CSRF,
|
||||
s.OAuth2State,
|
||||
s.OAuth2CodeVerifier,
|
||||
@@ -33,7 +32,6 @@ func (s *SessionData) String() string {
|
||||
s.FlashErrorMessage,
|
||||
s.Language,
|
||||
s.Theme,
|
||||
s.PocketRequestToken,
|
||||
s.LastForceRefresh,
|
||||
s.WebAuthnSessionData,
|
||||
)
|
||||
|
||||
@@ -19,16 +19,24 @@ func (c *Category) String() string {
|
||||
return fmt.Sprintf("ID=%d, UserID=%d, Title=%s", c.ID, c.UserID, c.Title)
|
||||
}
|
||||
|
||||
// CategoryRequest represents the request to create or update a category.
|
||||
type CategoryRequest struct {
|
||||
type CategoryCreationRequest struct {
|
||||
Title string `json:"title"`
|
||||
HideGlobally string `json:"hide_globally"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
}
|
||||
|
||||
// Patch updates category fields.
|
||||
func (cr *CategoryRequest) Patch(category *Category) {
|
||||
category.Title = cr.Title
|
||||
category.HideGlobally = cr.HideGlobally != ""
|
||||
type CategoryModificationRequest struct {
|
||||
Title *string `json:"title"`
|
||||
HideGlobally *bool `json:"hide_globally"`
|
||||
}
|
||||
|
||||
func (c *CategoryModificationRequest) Patch(category *Category) {
|
||||
if c.Title != nil {
|
||||
category.Title = *c.Title
|
||||
}
|
||||
|
||||
if c.HideGlobally != nil {
|
||||
category.HideGlobally = *c.HideGlobally
|
||||
}
|
||||
}
|
||||
|
||||
// Categories represents a list of categories.
|
||||
|
||||
+20
-33
@@ -7,9 +7,8 @@ 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.
|
||||
@@ -45,8 +44,18 @@ func (e *Enclosure) IsVideo() bool {
|
||||
|
||||
func (e *Enclosure) IsImage() bool {
|
||||
mimeType := strings.ToLower(e.MimeType)
|
||||
if strings.HasPrefix(mimeType, "image/") {
|
||||
return true
|
||||
}
|
||||
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")
|
||||
return strings.HasSuffix(mediaURL, ".jpg") || strings.HasSuffix(mediaURL, ".jpeg") || strings.HasSuffix(mediaURL, ".png") || strings.HasSuffix(mediaURL, ".gif")
|
||||
}
|
||||
|
||||
// ProxifyEnclosureURL modifies the enclosure URL to use the media proxy if necessary.
|
||||
func (e *Enclosure) ProxifyEnclosureURL(router *mux.Router, mediaProxyOption string, mediaProxyResourceTypes []string) {
|
||||
if mediaproxy.ShouldProxifyURLWithMimeType(e.URL, e.MimeType, mediaProxyOption, mediaProxyResourceTypes) {
|
||||
e.URL = mediaproxy.ProxifyAbsoluteURL(router, e.URL)
|
||||
}
|
||||
}
|
||||
|
||||
// EnclosureList represents a list of attachments.
|
||||
@@ -55,8 +64,10 @@ 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
|
||||
if enclosure.URL != "" {
|
||||
if enclosure.IsAudio() || enclosure.IsVideo() {
|
||||
return enclosure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,39 +76,15 @@ func (el EnclosureList) FindMediaPlayerEnclosure() *Enclosure {
|
||||
|
||||
func (el EnclosureList) ContainsAudioOrVideo() bool {
|
||||
for _, enclosure := range el {
|
||||
if strings.Contains(enclosure.MimeType, "audio/") || strings.Contains(enclosure.MimeType, "video/") {
|
||||
if enclosure.IsAudio() || enclosure.IsVideo() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (el EnclosureList) ProxifyEnclosureURL(router *mux.Router) {
|
||||
proxyOption := config.Opts.MediaProxyMode()
|
||||
|
||||
if 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
|
||||
}
|
||||
}
|
||||
func (el EnclosureList) ProxifyEnclosureURL(router *mux.Router, mediaProxyOption string, mediaProxyResourceTypes []string) {
|
||||
for _, enclosure := range el {
|
||||
enclosure.ProxifyEnclosureURL(router, mediaProxyOption, mediaProxyResourceTypes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"miniflux.app/v2/internal/config"
|
||||
)
|
||||
|
||||
func TestEnclosure_Html5MimeTypeGivesOriginalMimeType(t *testing.T) {
|
||||
@@ -26,8 +31,560 @@ func TestEnclosure_Html5MimeTypeReplaceStandardM4vByAppleSpecificMimeType(t *tes
|
||||
// tested at the time of this commit (06/2023) on latest Firefox & Vivaldi on this feed
|
||||
// https://www.florenceporcel.com/podcast/lfhdu.xml
|
||||
t.Fatalf(
|
||||
"HTML5 MimeType must be replaced by 'video/x-m4v' when originally video/m4v to ensure playbacks in brownser. Got '%s'",
|
||||
"HTML5 MimeType must be replaced by 'video/x-m4v' when originally video/m4v to ensure playbacks in browsers. Got '%s'",
|
||||
enclosure.Html5MimeType(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnclosure_IsAudio(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
mimeType string
|
||||
expected bool
|
||||
}{
|
||||
{"MP3 audio", "audio/mpeg", true},
|
||||
{"WAV audio", "audio/wav", true},
|
||||
{"OGG audio", "audio/ogg", true},
|
||||
{"Mixed case audio", "Audio/MP3", true},
|
||||
{"Video file", "video/mp4", false},
|
||||
{"Image file", "image/jpeg", false},
|
||||
{"Text file", "text/plain", false},
|
||||
{"Empty mime type", "", false},
|
||||
{"Audio with extra info", "audio/mpeg; charset=utf-8", true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
enclosure := &Enclosure{MimeType: tc.mimeType}
|
||||
if got := enclosure.IsAudio(); got != tc.expected {
|
||||
t.Errorf("IsAudio() = %v, want %v for mime type %s", got, tc.expected, tc.mimeType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnclosure_IsVideo(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
mimeType string
|
||||
expected bool
|
||||
}{
|
||||
{"MP4 video", "video/mp4", true},
|
||||
{"AVI video", "video/avi", true},
|
||||
{"WebM video", "video/webm", true},
|
||||
{"M4V video", "video/m4v", true},
|
||||
{"Mixed case video", "Video/MP4", true},
|
||||
{"Audio file", "audio/mpeg", false},
|
||||
{"Image file", "image/jpeg", false},
|
||||
{"Text file", "text/plain", false},
|
||||
{"Empty mime type", "", false},
|
||||
{"Video with extra info", "video/mp4; codecs=\"avc1.42E01E\"", true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
enclosure := &Enclosure{MimeType: tc.mimeType}
|
||||
if got := enclosure.IsVideo(); got != tc.expected {
|
||||
t.Errorf("IsVideo() = %v, want %v for mime type %s", got, tc.expected, tc.mimeType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnclosure_IsImage(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
mimeType string
|
||||
url string
|
||||
expected bool
|
||||
}{
|
||||
{"JPEG image by mime", "image/jpeg", "http://example.com/file", true},
|
||||
{"PNG image by mime", "image/png", "http://example.com/file", true},
|
||||
{"GIF image by mime", "image/gif", "http://example.com/file", true},
|
||||
{"Mixed case image mime", "Image/JPEG", "http://example.com/file", true},
|
||||
{"JPG file extension", "application/octet-stream", "http://example.com/photo.jpg", true},
|
||||
{"JPEG file extension", "text/plain", "http://example.com/photo.jpeg", true},
|
||||
{"PNG file extension", "unknown/type", "http://example.com/photo.png", true},
|
||||
{"GIF file extension", "binary/data", "http://example.com/photo.gif", true},
|
||||
{"Mixed case extension", "text/plain", "http://example.com/photo.JPG", true},
|
||||
{"Image mime and extension", "image/jpeg", "http://example.com/photo.jpg", true},
|
||||
{"Video file", "video/mp4", "http://example.com/video.mp4", false},
|
||||
{"Audio file", "audio/mpeg", "http://example.com/audio.mp3", false},
|
||||
{"Text file", "text/plain", "http://example.com/file.txt", false},
|
||||
{"No extension", "text/plain", "http://example.com/file", false},
|
||||
{"Other extension", "text/plain", "http://example.com/file.pdf", false},
|
||||
{"Empty values", "", "", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
enclosure := &Enclosure{MimeType: tc.mimeType, URL: tc.url}
|
||||
if got := enclosure.IsImage(); got != tc.expected {
|
||||
t.Errorf("IsImage() = %v, want %v for mime type %s and URL %s", got, tc.expected, tc.mimeType, tc.url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnclosureList_FindMediaPlayerEnclosure(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
enclosures EnclosureList
|
||||
expectedNil bool
|
||||
}{
|
||||
{
|
||||
name: "Returns first audio enclosure",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
|
||||
&Enclosure{URL: "http://example.com/video.mp4", MimeType: "video/mp4"},
|
||||
},
|
||||
expectedNil: false,
|
||||
},
|
||||
{
|
||||
name: "Returns first video enclosure",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{URL: "http://example.com/video.mp4", MimeType: "video/mp4"},
|
||||
&Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
|
||||
},
|
||||
expectedNil: false,
|
||||
},
|
||||
{
|
||||
name: "Skips image enclosure and returns audio",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{URL: "http://example.com/image.jpg", MimeType: "image/jpeg"},
|
||||
&Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
|
||||
},
|
||||
expectedNil: false,
|
||||
},
|
||||
{
|
||||
name: "Skips enclosure with empty URL",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{URL: "", MimeType: "audio/mpeg"},
|
||||
&Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
|
||||
},
|
||||
expectedNil: false,
|
||||
},
|
||||
{
|
||||
name: "Returns nil for no media enclosures",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{URL: "http://example.com/image.jpg", MimeType: "image/jpeg"},
|
||||
&Enclosure{URL: "http://example.com/doc.pdf", MimeType: "application/pdf"},
|
||||
},
|
||||
expectedNil: true,
|
||||
},
|
||||
{
|
||||
name: "Returns nil for empty list",
|
||||
enclosures: EnclosureList{},
|
||||
expectedNil: true,
|
||||
},
|
||||
{
|
||||
name: "Returns nil for all empty URLs",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{URL: "", MimeType: "audio/mpeg"},
|
||||
&Enclosure{URL: "", MimeType: "video/mp4"},
|
||||
},
|
||||
expectedNil: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := tc.enclosures.FindMediaPlayerEnclosure()
|
||||
if tc.expectedNil {
|
||||
if result != nil {
|
||||
t.Errorf("FindMediaPlayerEnclosure() = %v, want nil", result)
|
||||
}
|
||||
} else {
|
||||
if result == nil {
|
||||
t.Errorf("FindMediaPlayerEnclosure() = nil, want non-nil")
|
||||
} else if !result.IsAudio() && !result.IsVideo() {
|
||||
t.Errorf("FindMediaPlayerEnclosure() returned non-media enclosure: %s", result.MimeType)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnclosureList_ContainsAudioOrVideo(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
enclosures EnclosureList
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Contains audio",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{MimeType: "audio/mpeg"},
|
||||
&Enclosure{MimeType: "image/jpeg"},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Contains video",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{MimeType: "image/jpeg"},
|
||||
&Enclosure{MimeType: "video/mp4"},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Contains both audio and video",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{MimeType: "audio/mpeg"},
|
||||
&Enclosure{MimeType: "video/mp4"},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Contains only images",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{MimeType: "image/jpeg"},
|
||||
&Enclosure{MimeType: "image/png"},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Contains only documents",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{MimeType: "application/pdf"},
|
||||
&Enclosure{MimeType: "text/plain"},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Empty list",
|
||||
enclosures: EnclosureList{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Single audio enclosure",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{MimeType: "audio/wav"},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Single video enclosure",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{MimeType: "video/webm"},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := tc.enclosures.ContainsAudioOrVideo()
|
||||
if result != tc.expected {
|
||||
t.Errorf("ContainsAudioOrVideo() = %v, want %v", result, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnclosure_ProxifyEnclosureURL(t *testing.T) {
|
||||
// Initialize config for testing
|
||||
os.Clearenv()
|
||||
os.Setenv("BASE_URL", "http://localhost")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test-private-key")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Config parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
url string
|
||||
mimeType string
|
||||
mediaProxyOption string
|
||||
mediaProxyResourceTypes []string
|
||||
expectedURLChanged bool
|
||||
}{
|
||||
{
|
||||
name: "HTTP URL with audio type - proxy mode all",
|
||||
url: "http://example.com/audio.mp3",
|
||||
mimeType: "audio/mpeg",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"audio", "video"},
|
||||
expectedURLChanged: true,
|
||||
},
|
||||
{
|
||||
name: "HTTPS URL with video type - proxy mode all",
|
||||
url: "https://example.com/video.mp4",
|
||||
mimeType: "video/mp4",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"audio", "video"},
|
||||
expectedURLChanged: true,
|
||||
},
|
||||
{
|
||||
name: "HTTP URL with video type - proxy mode http-only",
|
||||
url: "http://example.com/video.mp4",
|
||||
mimeType: "video/mp4",
|
||||
mediaProxyOption: "http-only",
|
||||
mediaProxyResourceTypes: []string{"audio", "video"},
|
||||
expectedURLChanged: true,
|
||||
},
|
||||
{
|
||||
name: "HTTPS URL with video type - proxy mode http-only",
|
||||
url: "https://example.com/video.mp4",
|
||||
mimeType: "video/mp4",
|
||||
mediaProxyOption: "http-only",
|
||||
mediaProxyResourceTypes: []string{"audio", "video"},
|
||||
expectedURLChanged: false,
|
||||
},
|
||||
{
|
||||
name: "HTTP URL with image type - not in resource types",
|
||||
url: "http://example.com/image.jpg",
|
||||
mimeType: "image/jpeg",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"audio", "video"},
|
||||
expectedURLChanged: false,
|
||||
},
|
||||
{
|
||||
name: "HTTP URL with image type - in resource types",
|
||||
url: "http://example.com/image.jpg",
|
||||
mimeType: "image/jpeg",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"audio", "video", "image"},
|
||||
expectedURLChanged: true,
|
||||
},
|
||||
{
|
||||
name: "HTTP URL - proxy mode none",
|
||||
url: "http://example.com/audio.mp3",
|
||||
mimeType: "audio/mpeg",
|
||||
mediaProxyOption: "none",
|
||||
mediaProxyResourceTypes: []string{"audio", "video"},
|
||||
expectedURLChanged: false,
|
||||
},
|
||||
{
|
||||
name: "Empty URL",
|
||||
url: "",
|
||||
mimeType: "audio/mpeg",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"audio", "video"},
|
||||
expectedURLChanged: false,
|
||||
},
|
||||
{
|
||||
name: "Non-media MIME type",
|
||||
url: "http://example.com/doc.pdf",
|
||||
mimeType: "application/pdf",
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"audio", "video"},
|
||||
expectedURLChanged: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
enclosure := &Enclosure{
|
||||
URL: tc.url,
|
||||
MimeType: tc.mimeType,
|
||||
}
|
||||
|
||||
originalURL := enclosure.URL
|
||||
|
||||
// Call the method
|
||||
enclosure.ProxifyEnclosureURL(router, tc.mediaProxyOption, tc.mediaProxyResourceTypes)
|
||||
|
||||
// Check if URL changed as expected
|
||||
urlChanged := enclosure.URL != originalURL
|
||||
if urlChanged != tc.expectedURLChanged {
|
||||
t.Errorf("ProxifyEnclosureURL() URL changed = %v, want %v. Original: %s, New: %s",
|
||||
urlChanged, tc.expectedURLChanged, originalURL, enclosure.URL)
|
||||
}
|
||||
|
||||
// If URL should have changed, verify it's not empty
|
||||
if tc.expectedURLChanged && enclosure.URL == "" {
|
||||
t.Error("ProxifyEnclosureURL() resulted in empty URL when proxification was expected")
|
||||
}
|
||||
|
||||
// If URL shouldn't have changed, verify it's identical
|
||||
if !tc.expectedURLChanged && enclosure.URL != originalURL {
|
||||
t.Errorf("ProxifyEnclosureURL() URL changed unexpectedly from %s to %s", originalURL, enclosure.URL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnclosureList_ProxifyEnclosureURL(t *testing.T) {
|
||||
// Initialize config for testing
|
||||
os.Clearenv()
|
||||
os.Setenv("BASE_URL", "http://localhost")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test-private-key")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Config parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
enclosures EnclosureList
|
||||
mediaProxyOption string
|
||||
mediaProxyResourceTypes []string
|
||||
expectedChangedCount int
|
||||
}{
|
||||
{
|
||||
name: "Mixed enclosures with all proxy mode",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
|
||||
&Enclosure{URL: "https://example.com/video.mp4", MimeType: "video/mp4"},
|
||||
&Enclosure{URL: "http://example.com/image.jpg", MimeType: "image/jpeg"},
|
||||
&Enclosure{URL: "http://example.com/doc.pdf", MimeType: "application/pdf"},
|
||||
},
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"audio", "video"},
|
||||
expectedChangedCount: 2, // audio and video should be proxified
|
||||
},
|
||||
{
|
||||
name: "Mixed enclosures with http-only proxy mode",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
|
||||
&Enclosure{URL: "https://example.com/video.mp4", MimeType: "video/mp4"},
|
||||
&Enclosure{URL: "http://example.com/video2.mp4", MimeType: "video/mp4"},
|
||||
},
|
||||
mediaProxyOption: "http-only",
|
||||
mediaProxyResourceTypes: []string{"audio", "video"},
|
||||
expectedChangedCount: 2, // only HTTP URLs should be proxified
|
||||
},
|
||||
{
|
||||
name: "No media types in resource list",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
|
||||
&Enclosure{URL: "http://example.com/video.mp4", MimeType: "video/mp4"},
|
||||
},
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"image"},
|
||||
expectedChangedCount: 0, // no matching resource types
|
||||
},
|
||||
{
|
||||
name: "Proxy mode none",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
|
||||
&Enclosure{URL: "http://example.com/video.mp4", MimeType: "video/mp4"},
|
||||
},
|
||||
mediaProxyOption: "none",
|
||||
mediaProxyResourceTypes: []string{"audio", "video"},
|
||||
expectedChangedCount: 0,
|
||||
},
|
||||
{
|
||||
name: "Empty enclosure list",
|
||||
enclosures: EnclosureList{},
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"audio", "video"},
|
||||
expectedChangedCount: 0,
|
||||
},
|
||||
{
|
||||
name: "Enclosures with empty URLs",
|
||||
enclosures: EnclosureList{
|
||||
&Enclosure{URL: "", MimeType: "audio/mpeg"},
|
||||
&Enclosure{URL: "http://example.com/video.mp4", MimeType: "video/mp4"},
|
||||
},
|
||||
mediaProxyOption: "all",
|
||||
mediaProxyResourceTypes: []string{"audio", "video"},
|
||||
expectedChangedCount: 1, // only the non-empty URL should be processed
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Store original URLs
|
||||
originalURLs := make([]string, len(tc.enclosures))
|
||||
for i, enclosure := range tc.enclosures {
|
||||
originalURLs[i] = enclosure.URL
|
||||
}
|
||||
|
||||
// Call the method
|
||||
tc.enclosures.ProxifyEnclosureURL(router, tc.mediaProxyOption, tc.mediaProxyResourceTypes)
|
||||
|
||||
// Count how many URLs actually changed
|
||||
changedCount := 0
|
||||
for i, enclosure := range tc.enclosures {
|
||||
if enclosure.URL != originalURLs[i] {
|
||||
changedCount++
|
||||
// Verify that changed URLs are not empty (unless they were empty originally)
|
||||
if originalURLs[i] != "" && enclosure.URL == "" {
|
||||
t.Errorf("Enclosure %d: ProxifyEnclosureURL resulted in empty URL", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if changedCount != tc.expectedChangedCount {
|
||||
t.Errorf("ProxifyEnclosureURL() changed %d URLs, want %d", changedCount, tc.expectedChangedCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnclosure_ProxifyEnclosureURL_EdgeCases(t *testing.T) {
|
||||
// Initialize config for testing
|
||||
os.Clearenv()
|
||||
os.Setenv("BASE_URL", "http://localhost")
|
||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test-private-key")
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Config parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||
t.Run("Empty resource types slice", func(t *testing.T) {
|
||||
enclosure := &Enclosure{
|
||||
URL: "http://example.com/audio.mp3",
|
||||
MimeType: "audio/mpeg",
|
||||
}
|
||||
|
||||
originalURL := enclosure.URL
|
||||
enclosure.ProxifyEnclosureURL(router, "all", []string{})
|
||||
|
||||
// With empty resource types, URL should not change
|
||||
if enclosure.URL != originalURL {
|
||||
t.Errorf("URL should not change with empty resource types. Original: %s, New: %s", originalURL, enclosure.URL)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Nil resource types slice", func(t *testing.T) {
|
||||
enclosure := &Enclosure{
|
||||
URL: "http://example.com/audio.mp3",
|
||||
MimeType: "audio/mpeg",
|
||||
}
|
||||
|
||||
originalURL := enclosure.URL
|
||||
enclosure.ProxifyEnclosureURL(router, "all", nil)
|
||||
|
||||
// With nil resource types, URL should not change
|
||||
if enclosure.URL != originalURL {
|
||||
t.Errorf("URL should not change with nil resource types. Original: %s, New: %s", originalURL, enclosure.URL)
|
||||
}
|
||||
})
|
||||
t.Run("Invalid proxy mode", func(t *testing.T) {
|
||||
enclosure := &Enclosure{
|
||||
URL: "http://example.com/audio.mp3",
|
||||
MimeType: "audio/mpeg",
|
||||
}
|
||||
|
||||
originalURL := enclosure.URL
|
||||
enclosure.ProxifyEnclosureURL(router, "invalid-mode", []string{"audio"})
|
||||
|
||||
// With invalid proxy mode, the function still proxifies non-HTTPS URLs
|
||||
// because shouldProxifyURL defaults to checking URL scheme
|
||||
if enclosure.URL == originalURL {
|
||||
t.Errorf("URL should change for HTTP URL even with invalid proxy mode. Original: %s, New: %s", originalURL, enclosure.URL)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+43
-15
@@ -37,9 +37,10 @@ type Feed struct {
|
||||
ParsingErrorCount int `json:"parsing_error_count"`
|
||||
ScraperRules string `json:"scraper_rules"`
|
||||
RewriteRules string `json:"rewrite_rules"`
|
||||
Crawler bool `json:"crawler"`
|
||||
BlocklistRules string `json:"blocklist_rules"`
|
||||
KeeplistRules string `json:"keeplist_rules"`
|
||||
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
||||
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
||||
UrlRewriteRules string `json:"urlrewrite_rules"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Cookie string `json:"cookie"`
|
||||
@@ -52,13 +53,15 @@ type Feed struct {
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
DisableHTTP2 bool `json:"disable_http2"`
|
||||
PushoverEnabled bool `json:"pushover_enabled"`
|
||||
NtfyEnabled bool `json:"ntfy_enabled"`
|
||||
Crawler bool `json:"crawler"`
|
||||
AppriseServiceURLs string `json:"apprise_service_urls"`
|
||||
WebhookURL string `json:"webhook_url"`
|
||||
NtfyEnabled bool `json:"ntfy_enabled"`
|
||||
NtfyPriority int `json:"ntfy_priority"`
|
||||
NtfyTopic string `json:"ntfy_topic"`
|
||||
PushoverEnabled bool `json:"pushover_enabled,omitempty"`
|
||||
PushoverPriority int `json:"pushover_priority,omitempty"`
|
||||
PushoverPriority int `json:"pushover_priority"`
|
||||
ProxyURL string `json:"proxy_url"`
|
||||
|
||||
// Non-persisted attributes
|
||||
Category *Category `json:"category,omitempty"`
|
||||
@@ -116,9 +119,7 @@ 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, refreshDelayInMinutes int) {
|
||||
f.TTL = refreshDelayInMinutes
|
||||
|
||||
func (f *Feed) ScheduleNextCheck(weeklyCount int, refreshDelayInMinutes int) int {
|
||||
// Default to the global config Polling Frequency.
|
||||
intervalMinutes := config.Opts.SchedulerRoundRobinMinInterval()
|
||||
|
||||
@@ -132,12 +133,21 @@ func (f *Feed) ScheduleNextCheck(weeklyCount int, refreshDelayInMinutes int) {
|
||||
}
|
||||
}
|
||||
|
||||
// If the feed has a TTL or a Retry-After defined, we use it to make sure we don't check it too often.
|
||||
// Use the RSS TTL field, Retry-After, Cache-Control or Expires HTTP headers if defined.
|
||||
if refreshDelayInMinutes > 0 && refreshDelayInMinutes > intervalMinutes {
|
||||
intervalMinutes = refreshDelayInMinutes
|
||||
}
|
||||
|
||||
// Limit the max interval value for misconfigured feeds.
|
||||
switch config.Opts.PollingScheduler() {
|
||||
case SchedulerRoundRobin:
|
||||
intervalMinutes = min(intervalMinutes, config.Opts.SchedulerRoundRobinMaxInterval())
|
||||
case SchedulerEntryFrequency:
|
||||
intervalMinutes = min(intervalMinutes, config.Opts.SchedulerEntryFrequencyMaxInterval())
|
||||
}
|
||||
|
||||
f.NextCheckAt = time.Now().Add(time.Minute * time.Duration(intervalMinutes))
|
||||
return intervalMinutes
|
||||
}
|
||||
|
||||
// FeedCreationRequest represents the request to create a feed.
|
||||
@@ -154,13 +164,16 @@ type FeedCreationRequest struct {
|
||||
IgnoreHTTPCache bool `json:"ignore_http_cache"`
|
||||
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
DisableHTTP2 bool `json:"disable_http2"`
|
||||
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"`
|
||||
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
||||
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
||||
UrlRewriteRules string `json:"urlrewrite_rules"`
|
||||
DisableHTTP2 bool `json:"disable_http2"`
|
||||
ProxyURL string `json:"proxy_url"`
|
||||
}
|
||||
|
||||
type FeedCreationRequestFromSubscriptionDiscovery struct {
|
||||
@@ -180,8 +193,10 @@ type FeedModificationRequest struct {
|
||||
ScraperRules *string `json:"scraper_rules"`
|
||||
RewriteRules *string `json:"rewrite_rules"`
|
||||
BlocklistRules *string `json:"blocklist_rules"`
|
||||
KeeplistRules *string `json:"keeplist_rules"`
|
||||
UrlRewriteRules *string `json:"urlrewrite_rules"`
|
||||
KeeplistRules *string `json:"keeplist_rules"`
|
||||
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
|
||||
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
|
||||
Crawler *bool `json:"crawler"`
|
||||
UserAgent *string `json:"user_agent"`
|
||||
Cookie *string `json:"cookie"`
|
||||
@@ -195,6 +210,7 @@ type FeedModificationRequest struct {
|
||||
FetchViaProxy *bool `json:"fetch_via_proxy"`
|
||||
HideGlobally *bool `json:"hide_globally"`
|
||||
DisableHTTP2 *bool `json:"disable_http2"`
|
||||
ProxyURL *string `json:"proxy_url"`
|
||||
}
|
||||
|
||||
// Patch updates a feed with modified values.
|
||||
@@ -223,18 +239,26 @@ func (f *FeedModificationRequest) Patch(feed *Feed) {
|
||||
feed.RewriteRules = *f.RewriteRules
|
||||
}
|
||||
|
||||
if f.KeeplistRules != nil {
|
||||
feed.KeeplistRules = *f.KeeplistRules
|
||||
}
|
||||
|
||||
if f.UrlRewriteRules != nil {
|
||||
feed.UrlRewriteRules = *f.UrlRewriteRules
|
||||
}
|
||||
|
||||
if f.KeeplistRules != nil {
|
||||
feed.KeeplistRules = *f.KeeplistRules
|
||||
}
|
||||
|
||||
if f.BlocklistRules != nil {
|
||||
feed.BlocklistRules = *f.BlocklistRules
|
||||
}
|
||||
|
||||
if f.BlockFilterEntryRules != nil {
|
||||
feed.BlockFilterEntryRules = *f.BlockFilterEntryRules
|
||||
}
|
||||
|
||||
if f.KeepFilterEntryRules != nil {
|
||||
feed.KeepFilterEntryRules = *f.KeepFilterEntryRules
|
||||
}
|
||||
|
||||
if f.Crawler != nil {
|
||||
feed.Crawler = *f.Crawler
|
||||
}
|
||||
@@ -286,6 +310,10 @@ func (f *FeedModificationRequest) Patch(feed *Feed) {
|
||||
if f.DisableHTTP2 != nil {
|
||||
feed.DisableHTTP2 = *f.DisableHTTP2
|
||||
}
|
||||
|
||||
if f.ProxyURL != nil {
|
||||
feed.ProxyURL = *f.ProxyURL
|
||||
}
|
||||
}
|
||||
|
||||
// Feeds is a list of feed
|
||||
|
||||
@@ -144,6 +144,29 @@ func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayBelowMinInterval(t *test
|
||||
checkTargetInterval(t, feed, expectedInterval, timeBefore, "TestFeedScheduleNextCheckRoundRobinWithRefreshDelayBelowMinInterval")
|
||||
}
|
||||
|
||||
func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMaxInterval(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.SchedulerRoundRobinMaxInterval()+30)
|
||||
|
||||
if feed.NextCheckAt.IsZero() {
|
||||
t.Error(`The next_check_at must be set`)
|
||||
}
|
||||
|
||||
expectedInterval := config.Opts.SchedulerRoundRobinMaxInterval()
|
||||
checkTargetInterval(t, feed, expectedInterval, timeBefore, "TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMaxInterval")
|
||||
}
|
||||
|
||||
func TestFeedScheduleNextCheckRoundRobinMinInterval(t *testing.T) {
|
||||
minInterval := 1
|
||||
os.Clearenv()
|
||||
|
||||
@@ -41,9 +41,6 @@ type Integration struct {
|
||||
EspialTags string
|
||||
ReadwiseEnabled bool
|
||||
ReadwiseAPIKey string
|
||||
PocketEnabled bool
|
||||
PocketAccessToken string
|
||||
PocketConsumerKey string
|
||||
TelegramBotEnabled bool
|
||||
TelegramBotToken string
|
||||
TelegramBotChatID string
|
||||
@@ -90,9 +87,13 @@ type Integration struct {
|
||||
WebhookSecret string
|
||||
RSSBridgeEnabled bool
|
||||
RSSBridgeURL string
|
||||
RSSBridgeToken string
|
||||
OmnivoreEnabled bool
|
||||
OmnivoreAPIKey string
|
||||
OmnivoreURL string
|
||||
KarakeepEnabled bool
|
||||
KarakeepAPIKey string
|
||||
KarakeepURL string
|
||||
RaindropEnabled bool
|
||||
RaindropToken string
|
||||
RaindropCollectionID string
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user